Docs / Protocol reference
Offer, receipt and terms
Three structures carry the whole protocol: the terms a service registers, the offer it signs before payment and the receipt it signs after. All three are contract types; their canonical encoding is ScVal XDR, which is what gets hashed and signed.
JSON Schemas derived from the contract types: terms.json, offer.json, receipt.json. In JSON, 32-byte values are lowercase hex, i128 amounts are decimal strings, u64 times are Unix seconds, and addresses are strkeys.
Terms
A service’s registered warranty terms. Hashed (terms_hash) and versioned; every offer pins one version. min_stake is deliberately not part of the terms, so changing the stake requirement never invalidates offers or receipts a buyer holds.
| Field | Type | Meaning |
|---|---|---|
predicates | Vec<u32> | Predicate IDs this warranty covers: 1 to 6, non-empty, no duplicates |
claim_window | u64 | Seconds after served_at during which a claim may be filed |
max_data_age | u64 | P3: most seconds allowed between data_as_of and served_at |
allowed_status | Vec<u32> | P4: HTTP statuses the seller may charge for |
max_response_bytes | u32 | P5: largest response body in bytes |
oracle | OracleWarranty | P6 parameters: None, or Some(OracleTerms). Required when P6 is listed |
OracleTerms
| Field | Type | Meaning |
|---|---|---|
oracle_contract | Address | The SEP-40 oracle the warranty is measured against |
oracle_asset | OracleAsset | Stellar(Address) or Other(Symbol), as that oracle names the asset |
max_deviation_bps | u32 | Allowed difference, in basis points of the oracle price |
max_oracle_age | u64 | Most seconds allowed between the oracle observation and served_at |
Validity rules
The registry rejects terms with InvalidTerms unless all of the following hold:
predicatesis non-empty, each ID is 1 to 6, and no ID repeats1 ≤ claim_window ≤ max_claim_window(86,400 s on the first deployment)- P3 listed ⇒
max_data_age > 0; P4 listed ⇒allowed_statusnon-empty; P5 listed ⇒max_response_bytes > 0 - P6 listed ⇒
claim_window ≤ max_oracle_claim_window(3,600 s) andoracle = Some(o)witho.max_oracle_age > 0
{
"predicates": [3, 4, 6], "claim_window": 3600, "max_data_age": 30, "allowed_status": [200], "max_response_bytes": 4096,
"oracle": { "oracle_contract": "C…", "oracle_asset": { "Other": "EUR" }, "max_deviation_bps": 50, "max_oracle_age": 600 }
}Offer
The seller’s signed promise for one call, shown with the 402 response. It does not repeat the warranty conditions: those live on-chain as the registered terms, and terms_hash pins the exact version the buyer is paying under. The offer is the signed body; the signature travels beside it in SignedOffer { offer, signature }.
| Field | Type | Meaning |
|---|---|---|
version | u32 | Protocol version, 1. Any other value fails a claim with UnsupportedVersion |
service_id | BytesN<32> | Assigned by the Bond Registry at registration |
registry | Address | Bond Registry contract that holds the stake. A claim requires it to be the registry the Claims contract serves |
terms_hash | BytesN<32> | Hash of the registered terms for this service. The buyer SDK requires it to be the service’s current terms |
price | i128 | Price of this call, in the asset’s smallest unit. The buyer SDK requires it to equal the x402 amount |
asset | Address | Token contract of the payment asset |
max_claim | i128 | Most a single receipt can pay out |
valid_until | u64 | Unix seconds after which the offer must not be paid |
offer_nonce | BytesN<32> | Random, unique per offer; makes every offer hash unique |
key_version | u32 | Which registered signing key signed this offer |
signature (in SignedOffer) | BytesN<64> | Ed25519 signature over offer_hash by the service signing key |
The middleware sets valid_until = now + offerValiditySeconds (default 120 s), max_claim from its configuration, price and asset from the matching x402 requirement, and key_version from the service’s current key.
Receipt
The seller’s signed statement of what it served and under which offer. Every field is a claim the seller can be held to. Signed body; the signature travels beside it in SignedReceipt { receipt, signature }.
| Field | Type | Binds the receipt to |
|---|---|---|
version | u32 | Protocol version, 1 |
service_id | BytesN<32> | The registered service |
offer_hash | BytesN<32> | The exact warranty offer |
terms_hash | BytesN<32> | The terms the seller says it served under (P2 compares it with the offer’s) |
buyer | Address | The payer, who receives any claim payout |
payment_reference | BytesN<32> | The settlement transaction hash |
request_hash | BytesN<32> | Method, path with query, and body of the request |
response_hash | BytesN<32> | The response body as sent |
response_bytes | u32 | Size of the response body (P5) |
status | u32 | HTTP status served (P4) |
served_at | u64 | Unix seconds when the response was sent; starts the claim window |
data_as_of | u64 | Unix seconds the data was true as of; 0 if not applicable (P3) |
oracle | OracleEvidenceOpt | None, or Some(OracleEvidence) for P6 services |
key_version | u32 | Which signing key signed this receipt |
signature (in SignedReceipt) | BytesN<64> | Ed25519 signature over receipt_hash by the service signing key |
OracleEvidence
| Field | Type | Meaning |
|---|---|---|
oracle_contract | Address | The SEP-40 oracle named in the terms |
oracle_asset | OracleAsset | The asset or pair, as the oracle names it |
oracle_timestamp | u64 | The timestamp the oracle returned for the observation used |
oracle_price | i128 | The oracle price the seller saw. Informational: the validator reads the oracle itself |
reported_value | i128 | The value the seller served, in the oracle’s decimals |
The contract does not check that the payment happened. The seller’s signature over payment_reference and buyer is its admission that it did, and any payout comes from the seller’s own stake. The middleware takes buyer and payment_reference from the facilitator’s settlement result and only issues receipts for offers it signed itself.
Registry records
Service
| Field | Type | Meaning |
|---|---|---|
seller | Address | The account that registered the service and may manage it |
state | ServiceState | Pending 0, Active 1, Suspended 2, Unbonding 3, Closed 4 |
stake | i128 | Stake currently held |
min_stake | i128 | Stake below which the service is Suspended; at least the protocol floor |
terms_hash | BytesN<32> | Current terms version |
key_version | u32 | Current signing key version |
max_claim_window | u64 | Largest claim window of any terms version ever registered; sets the unbonding delay |
pending_withdrawal | i128 | Stake requested for withdrawal and still slashable |
unlock_at | u64 | When the pending withdrawal may be paid out |
full_exit | bool | True when the pending withdrawal closes the service |
epoch_start, epoch_base_stake, epoch_slashed | u64, i128, i128 | Slash rate limit bookkeeping |
fee_accrued_at | u64 | Time up to which the annual stake fee has been collected |
SigningKey
| Field | Type | Meaning |
|---|---|---|
public_key | BytesN<32> | Ed25519 public key |
registered_at | u64 | When this version was registered |
retired_at | u64 | 0 while current; otherwise the time it was rotated out |
ProtocolConfig, LaunchLimits, FeeConfig
| Type | Fields | Set |
|---|---|---|
ProtocolConfig | min_stake_floor i128, max_claim_window u64, max_oracle_claim_window u64, unbond_margin u64, slash_epoch_cap_bps u32 | Once, at registry construction |
LaunchLimits | max_stake_per_service i128, max_total_stake i128, max_payout i128, allowlist_only bool (0 means no limit) | Tightening immediate; loosening time-locked |
FeeConfig | treasury Address, registration_fee i128, stake_fee_bps_per_year u32 | Changes time-locked |
Validator types
| Type | Definition |
|---|---|
ClaimExtra | None | SecondReceipt(SignedReceipt): predicate-specific evidence; the second receipt for P1, nothing for P2 to P6 |
ValidationError | NotProven = 1 | NotVerifiable = 2: why a validator did not prove a breach |
PriceData | { price: i128, timestamp: u64 }: a SEP-40 price record as the oracle returns it |
Sep40Oracle | price(asset, timestamp) -> Option<PriceData>, decimals() -> u32: what the P6 validator reads |