# Protected Payment Lifecycle

**Status: LOCAL SANDBOX.** The app presents local API and ledger state only. It is not a wallet transaction or chain receipt.

## State and role sequence

| Step | Authorized persona | Existing operation | Evidence shown |
|---|---|---|---|
| Create offer | Payee provider | `POST /v1/payments` | Payment ID and terms hash |
| Accept terms | Buyer and merchant | `POST /v1/payments/{id}/accept` | Both acceptances |
| Authorize payment | Buyer | `POST /v1/payments/{id}/authorize` | Authorization bound to terms hash |
| Bind coverage | Owning provider | `POST /v1/payments/{id}/reserve` | Unique reservation and reduced available pool |
| Confirm simulated settlement | Payer provider's local signer | `POST /v1/provider-messages` via the local harness | Signed local provider result |
| Credit accounts | Protocol/SQLite ledger | Confirmed settlement transition | Buyer debit and merchant credit |

The browser obtains no provider private key. The local harness creates a signed test message inside the server and submits it through the ordinary provider API handler. A payment remains unprotected unless both reserve and signed settlement succeed.

## Idempotency and failure

Each mutating API request has a stable key per local payment/action. The backend persists the request fingerprint and original response. An identical retry is safe; a changed body under the same key is rejected. A payment ID is not a reusable transaction identifier. An insufficient reserve fails before buyer/merchant settlement; the UI presents the API error instead of treating the offer as protected.

## Boundaries

The SQLite transition and the separate local Hardhat vault are not atomically coupled. There is no chain transaction journal, wallet signing, canonical event cursor, Base Sepolia confirmation or reorg recovery in this app. See [Known Technical Limits](known-technical-limits.md).
