# Offer, receipt and terms

> Field-by-field reference for the LOPW Terms, Offer, Receipt and oracle types, exactly as the contract types define them, with validity rules and JSON Schemas.

*LOPW documentation · Protocol reference.* Canonical page: https://lopw.com/docs/protocol/offer-and-receipt.html · This Markdown twin: https://lopw.com/docs/protocol/offer-and-receipt.md · Index of all pages: https://lopw.com/llms.txt · Everything in one file: https://lopw.com/llms-full.txt

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](https://lopw.com/docs/schemas/terms.json), [offer.json](https://lopw.com/docs/schemas/offer.json), [receipt.json](https://lopw.com/docs/schemas/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](https://lopw.com/docs/protocol/hashing-and-signing.html#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:

- `predicates` is non-empty, each ID is 1 to 6, and no ID repeats
- `1 ≤ claim_window ≤ max_claim_window` (86,400 s on the first deployment)
- P3 listed ⇒ `max_data_age > 0`; P4 listed ⇒ `allowed_status` non-empty; P5 listed ⇒ `max_response_bytes > 0`
- P6 listed ⇒ `claim_window ≤ max_oracle_claim_window` (3,600 s) and `oracle = Some(o)` with `o.max_oracle_age > 0`

*Terms from the shared test vectors*

```json
{
  "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`, `decimals() -> u32`: what the P6 validator reads |
