# The decisions log, explained

> Why each LOPW rule exists: the twenty-two recorded decisions that refine the specification, in plain language, with what they changed and why.

*LOPW documentation · Deep dive.* Canonical page: https://lopw.com/docs/deep-dive/decisions.html · This Markdown twin: https://lopw.com/docs/deep-dive/decisions.md · Index of all pages: https://lopw.com/llms.txt · Everything in one file: https://lopw.com/llms-full.txt

The specification left open questions and a few gaps. Each was settled with a recorded decision. Where the specification and a decision differ, the decision wins. Here is each one, with the reason, in plain words.

## The decisions, one by one

### D-001 Two contracts: Bond Registry and Claims

**Decision.** Stake custody lives in a small registry contract; claim logic lives in a separate contract, with one more contract per validator.

**Why.** The code that holds money stays short and easy to audit, and claim logic can evolve (new predicates, new evidence rules) without redeploying the contract that holds stake. Only the Claims contract may call `slash`.

### D-002 A Pending state

**Decision.** A newly registered service is `Pending` until deposits reach `min_stake`.

**Why.** Registration and deposit are separate calls, and the draft had no state for the gap. A Pending service issues no offers, so no receipt can exist against it, so its stake can be withdrawn immediately without an unbonding delay.

### D-003 Partial withdrawal keeps the service Active

**Decision.** `request_unbond` records a pending withdrawal; if the remainder stays at or above the minimum the service stays Active, otherwise the whole stake exits and the service is Unbonding.

**Why.** The purpose of the delay is only to keep money slashable for open receipts, not to stop an honest seller from trimming surplus. The pending amount stays slashable until withdrawn; `cancel_unbond` restores Active or, if below the minimum, Suspended.

### D-004 The unbonding delay covers every terms version

**Decision.** `unbonding_delay = largest claim window ever registered + 1 h`; terms cannot change while Unbonding.

**Why.** Claim windows can change with terms. A receipt issued under an older, longer window must still be claimable after a seller shortens its window and exits.

### D-005 Seller self-drain is rate-limited

**Decision.** Within one epoch (one unbonding delay) total slashes may not exceed 50% of the stake at the epoch start; the first slash of an epoch is always allowed.

**Why.** The contract never checks that a payment happened, so a seller could sign receipts for invented payments to its own addresses and claim them: an instant withdrawal that skips the delay and races honest buyers. The draft’s note that this “costs the seller its own money” was wrong, since the money returns to the seller. With the cap, stake leaves no faster than a normal withdrawal, so honest claims compete for real stake. The trade-off is that a genuine mass breach pays out over more than one epoch.

### D-006 Rotated keys have a retirement time

**Decision.** `rotate_key` stamps the old key with `retired_at`; a receipt is accepted only if `served_at` is before it.

**Why.** A rotated key that stayed valid forever would let a leaked old key keep signing receipts with fresh timestamps. Old receipts stay claimable; the old key cannot produce new ones.

### D-006a Typed errors everywhere

**Decision.** All public registry functions return `Result` instead of panicking.

**Why.** Generated clients, mobile apps and agents receive the named error code (`EpochCapReached`, `StillLocked`) rather than an opaque host failure.

### D-007 min_stake is a service parameter, not part of the terms

**Decision.** Terms (predicates, parameters, claim window) are hashed and versioned; `min_stake` is set at registration outside the hash.

**Why.** Changing the stake requirement must never invalidate offers or receipts a buyer already holds.

### D-008 P3 does not apply when data_as_of is 0

**Decision.** The P3 validator returns NotProven for `data_as_of == 0` and for `data_as_of > served_at`.

**Why.** 0 means “not applicable”, but `served_at − 0` would always exceed any limit and prove a breach. A P3 service reporting 0 is instead caught by the buyer SDK as an untrusted response. Negative ages are not breaches either.

### D-009 P6 uses the oracle's price, exactly

**Decision.** The breach test is `|reported_value − oracle_price| × 10000 > max_deviation_bps × oracle_price`, with `oracle_price` read from the oracle, not from the receipt.

**Why.** The draft’s division form rounds down, so a 50.99 bps deviation would pass a 50 bps limit; a unit test caught it. The receipt’s own price field is informational. A zero or negative oracle price is NotVerifiable.

### D-010 Claims verifies P1's second receipt

**Decision.** For P1 the Claims contract checks the signature of the receipt in `extra` with the registry key before calling the validator.

**Why.** `validate()` is not given the seller’s keys. Doing the check in Claims keeps validators pure functions of their inputs.

### D-011 One stake token per registry deployment

**Decision.** The registry is initialised with a single SEP-41 token: USDC.

**Why.** Any SEP-41 token works technically; one token per deployment keeps payouts and floors in one unit.

### D-012 Protocol floors

**Decision.** Minimum `min_stake` 100 USDC; maximum claim window 24 h; unbond margin 1 h; slash epoch cap 50%.

**Why.** Adopted as the draft proposed, configured at registry construction so a deployment can choose differently.

### D-013 Every API is built for third-party, mobile and agent clients

**Decision.** Versioned paths with the network in them, an OpenAPI document, JSON with `snake_case` fields, decimal-string amounts, problem+json errors, cursor pagination, open CORS on reads, ETags, advertised rate limits, agent discovery files and a Server-Sent Events stream.

**Why.** So other applications, mobile apps and autonomous agents can use the APIs without special handling, and so that paid reference services remain plain x402 endpoints any agent can call.

### D-014 Reflector oracle findings and demo pairs

**Decision.** The fiat-rates oracle quotes USD per unit with 14 decimals, ticks every 5 minutes and keeps 24 hours of history. `price(asset, t)` rounds `t` down to the previous tick and returns that tick’s timestamp; it returns nothing beyond retention. The validator applies the age check to the returned timestamp. The test-network demo uses EUR/USD; the FX service takes the asset from configuration. No operator-run mirror oracle.

**Why.** Requiring the cited timestamp to equal a tick would let a seller dodge claims by citing an off-tick time. An oracle the operator controls would undermine the point of P6: evidence the seller does not control. A contract-to-contract read carried no charge beyond the normal transaction fee on the first live claim.

### D-015 Exact byte format for hashing and signing

**Decision.** Signed body and signature are separate types; the message is `SHA-256(tag || network_id || ScVal-XDR(registry) || ScVal-XDR(body))`; optional blocks are None/Some enum variants; `request_hash` is fixed; invalid signatures trap; a shared TypeScript package and test vectors pin the bytes.

**Why.** So the off-chain code and the contracts hash identical bytes, and so the seller middleware and buyer SDK cannot drift apart. Soroban’s signature check cannot return an error, so a bad signature fails as a host error with the same outcome: rejected, no penalty.

### D-016 Rejected claims are recorded; precondition failures are errors

**Decision.** `claim` returns `Err` when evidence or preconditions fail (nothing recorded) and `Ok(Rejected(reason))` when the evidence is valid but the breach is not proven (event recorded).

**Why.** A transaction that returns an error is rolled back, events included, so if every non-payout were an error the `ClaimRejected` event could never exist and monitoring could not see rejected claims. New error codes beyond the draft: `KeyRetired`, `UnsupportedVersion`, `ValidatorNotSet`, `SlashRateLimited`.

### D-017 x402 placement

**Decision.** The offer travels in `PaymentRequired.extensions.lopw` and is echoed by the buyer; the receipt rides in `PAYMENT-RESPONSE.extensions.lopw` rather than a custom header; handlers report freshness and oracle evidence through two response headers.

**Why.** Checked against the current x402 v2 implementation. The receipt then travels in the standard envelope every client already decodes. x402’s own offer-receipt extension signs generic JWS/EIP-712 payloads without terms, hashes or oracle evidence, and a contract cannot cheaply verify JWS, so LOPW keeps a format that is verifiable on-chain; both can be served side by side.

### D-018 Admin changes are time-locked

**Decision.** The first wiring applies immediately; every later change to which contract may slash or which validator decides a predicate is a proposal applied only after 26 hours, cancellable until then, announced by events. Contracts have no upgrade function.

**Why.** A compromised admin key could otherwise point the registry at a contract that slashes every service to itself in one block. 26 hours exceeds every possible unbonding delay (at most 25 hours), so every seller can withdraw first.

### D-019 Protocol fees

**Decision.** A one-off registration fee and an annual stake fee in basis points, accrued per second, collected whenever stake moves or on demand, paid to a treasury; both may be zero; changes time-locked.

**Why.** To fund the protocol without touching payouts. The fee can suspend a service at its minimum, so sellers should stake above it. The first deployment runs 1 USDC and 100 bps to show the mechanism; 10 USDC and 100 bps are proposed for a production launch.

### D-020 Guarded launch: limits, pause, allowlist

**Decision.** Launch limits on stake per service, total stake and any single payout, plus an allowlist switch; tightening is immediate, loosening is time-locked; a pause stops registrations and deposits but never claims, unbonding or withdrawals. Storage-lifetime extension functions were added for a keeper.

**Why.** A contract bug can then lose at most `max_payout` per claim and `max_total_stake` overall. The pause is an emergency brake that can never trap funds.

### D-021 Operational records and exchange rates

**Decision.** The API offers `GET /v1/rates`, an hourly exchange-rate snapshot with exact cross rates for later currency calculations. Visitor records of the public hosts are private and operator-only.

**Why.** Currency conversions for displays and reports; operational insight that is never exposed publicly.

### D-022 Scheduled scenario runs with plain-language notes

**Decision.** A catalogue of named scenarios runs against the live reference services on a schedule with real x402 payments; each run is judged against the expected outcome and recorded with its transactions, payout and stake before and after; payouts are recycled into the seller’s stake.

**Why.** So the demo and proofs pages show what the protocol does, continuously and verifiably, in words a reader without protocol knowledge can follow.
