# Fixed-quote payment profile

`1.2.0-draft.3` · `fixed-quote-v1` · 17 September 2026

This is a next-draft **pure contract and transition implementation**, isolated in `work/odexa-next`. It is not a paid HTTP service, payment-rail adapter, production financial integration or release candidate. The published draft-2 free reference remains unchanged. The decisions below resolve ambiguities in native draft-1 section 11 and extend the selected trust decisions under a new version; they are not claimed to be wire-compatible with draft 2.

The implementation never initiates a charge, opens checkout, contacts a provider or verifies a signature. Its caller must supply authenticated facts, resolve current origin/delegation authority, enforce transaction uniqueness and persist results atomically. A `confirmed` result means **the authenticated provider asserts that the exact quote obligation is satisfied under its documented status mapping**. It does not mean checkout was merely created, cash is irrevocably settled, a bank was independently inspected or reversal is impossible. A real adapter must document what its mapped statuses establish and test that mapping.

## Six machine contracts

`profiles/payment.schema.json` has closed `$defs` for `quote`, `accepted_binding`, `mandate`, `check`, `verification` and `free`. The five typed envelopes use exact `protocol_version`, `profile` and `record_type` fields; free bypass is exactly `{"required":false}`. The profile reuses bounded native UTF-8 JSON, canonical HTTPS references, integer-token rules and exact UTC timestamps. Bodies are at most 128 KiB. Unknown fields are rejected. JSON Schema remains a structural superset: equality, arithmetic, lexical numbers, calendars, authority and state require the Python checks and trusted integration.

### Quote identity and fees

A quote names a UUID `quote_id`, UUID `offer_id`, policy `origin`, origin-scoped agreement `service_id`, origin-scoped `verification_service_id`, provider `provider_id` (the verification service's issuer), exact `verification_endpoint`, payer, currency, scale, ordered items, total and issue/expiry times.

The next draft replaces the old ambiguous `status_url` with `verification_endpoint`. It must match the authoritative verification descriptor's `base_url + "verify"`; the pure engine compares the resolver's result and does not fetch a caller-selected URL. Agreement-service authority and payment-verifier authority are separate capabilities.

An item has unique bounded `item_id`, `kind`, explicit `payee_id`, `amount_minor` and description. Kinds are `resource_license` and `provider_service`. At least one resource-license line is required, including an explicit zero resource price when only a provider fee is payable. A provider cannot silently charge its originator subscription to an agent. The payer separately authorizes the exact quote, including every fee/payee.

Amounts are unsigned canonical decimal **strings**, at most 18 digits; scale is an integer token from 0 to 9. The positive total must equal the exact item sum and remain below `10^18`. Currency is a three-uppercase-letter identifier; this parser does not claim a live currency registry or infer a currency's scale. The adapter must support the quoted identifier/scale explicitly. There are no floats, exchange-rate conversions, metering, partial payments or arbitrary quantity debits.

`register_quote(registry, raw_quote_bytes)` keeps immutable identity `(provider_id, quote_id)` against the exact byte digest. Changed whitespace or items under that identity conflict. `bind_quote_to_agreement` additionally permits only one agreement for that quote. The caller must persist this registry and agreement/offer uniqueness in the same transaction as creating payment state; in-memory helpers alone do not provide distributed idempotency.

### Accepted terms are not spending authority

`accepted_binding` is a trusted adapter projection of a **verified accepted agreement**, not a substitute signature or client-supplied assertion. It contains agreement/principal/origin/service/offer IDs, exact `offer_digest`, exact `quote_digest`, accepted `request`, acceptance time and accepted access/use expiry times. The full request has exactly `url`, `actions`, `purposes` and explicit `supported_obligations`; it must belong to the quote origin and use known vocabulary.

The HTTP adapter must verify that these values actually appear in the original accepted offer/receipt before calling `start_payment`. That function only validates the supplied binding. It cannot infer those facts from the offer digest alone. Acceptance must have occurred while the quote was valid, and accepted access must end after acceptance and no later than use expiry.

A separate `payment.mandate` is mandatory for the paid path. It binds its own UUID, agreement, exact quote digest/ID, origin, service, provider, payer, currency, scale, total, issue/expiry and the exact authorization `pay_exact_quote_once`. `authorize_payment` requires a separately authenticated payer ID matching the quote payer. The accepting principal may differ from the payer; assent does not prove authority to spend that payer's money. Mandate replacement is rejected in this bounded profile; an identical byte retry changes nothing. Issuing a mandate is not a charge.

### Read-only verification

`verification_request(state, check_id)` creates the `payment.check` body for authenticated **read-only** `POST verify`. It includes check ID, quote identity/digest, accepted offer digest, accepted request digest, agreement, mandate, origin, verification service, provider, payer, currency, scale and total. Request digest is SHA-256 of `contracts.json_bytes(accepted_binding.request)`; object keys are sorted, arrays retain their accepted order. Do not silently reconstruct a different ordered request.

A `payment.verification` response binds all those fields plus positive monotonic `sequence`, one of `pending/confirmed/failed/reversed`, provider transaction reference, `effective_at` and `checked_at`. Confirmed/reversed require a nonsecret bounded provider reference. After first confirmation/reversal, that reference cannot change. `effective_at` must not predate mandate issuance or move backwards. Confirmation must have become effective before mandate expiry. A delayed check after mandate expiry may confirm a payment that became effective inside the authorized interval.

Caller authentication of provider responses is mandatory; the intended signed response type is `odexa-payment-status+jws`, checked by the actual adapter. This module does not perform that signature check. HTTPS, credential handling, provider signing and response parsing at the transport boundary remain integration work.

`apply_verification(state, raw_response, expected_check_id=..., authority=full_resolver_result, now=...)` requires:

- An explicit `decision == "allow"`; matching identity fields in a denial are insufficient.
- Exact policy origin, verification service, issuer/provider, capability `verify_payments`, endpoint and authorized payment-status key use.
- `authority.operation_id == check_id` and `authority.payment_request_digest` equal to the digest of this exact canonical payment-check body.
- `authority.request_digest` equal to the accepted resource/action/purpose/support request digest. A decision for resource B cannot authorize payment processing for agreement A.
- A current authority observation no more than five seconds old, matching the delegation contract. Newly applied provider responses must have `checked_at` no more than 60 seconds old and not in the future.

The resolver's authority object is trusted adapter input. These comparisons do not authenticate arbitrary JSON or prevent a malicious caller from fabricating it. The caller must use the resolver's result directly after establishing its HTTPS/current-authority observation; these helpers are not a trust bootstrap.

## State and time decisions

`PaymentState` is immutable internal Python state, not a public wire payload. It keeps retained quote/acceptance/mandate bytes, separate payment and access states, check identities/digests and sequence history. Exact response bytes and authenticated verification material must also be durably retained by the caller for evidence/export.

| Current payment state | Allowed next provider states |
| --- | --- |
| pending | pending, failed, confirmed, reversed |
| failed | pending, failed, confirmed, reversed |
| confirmed | confirmed, reversed |
| reversed | reversed only |

A failed attempt does not assert the quote can never be satisfied: a later authenticated newer provider state may confirm the same quote under the same valid one-payment mandate. This read-only transition never retries a debit. A reversal may be received before a local confirmation was observed; it still blocks access. A confirmed state cannot degrade to failed/pending; the provider must express a reversal.

Matching confirmation changes an otherwise eligible pending agreement to active. Pending/failed do not grant access. Reversal sets future access to revoked. Expired or administratively revoked access remains terminal even when later financial information arrives; a financial confirmation can be retained while access stays expired/revoked. A new agreement is needed to restore access. Past acceptance/use rights and evidence are not rewritten, and delivered bytes cannot be recalled.

**Quote expiry applies to new acceptance, not settlement of a timely accepted quote.** The accepted agreement's windows and payer mandate remain authoritative. New confirmation after accepted access expiry records the financial assertion without granting new access. `expire` and `revoke` implement the payment-related access gate only. An `active` result is not a policy grant or access token: the service must still perform every current authority, policy/agreement and gateway authorization check.

An exact authenticated check-ID/response-byte replay returns the current state, including after reversal; it cannot restore the earlier state. Changed bytes under the same check ID, stale sequence, reversed time or mismatched context fail without mutating input. The bounded helper retains 256 check identities; at capacity it fails closed. Production history pagination/storage is not implemented. The caller serializes concurrent changes, atomically stores original responses and never lets two workers replace a newer state with an older snapshot.

## Free path and omitted functions

`free_payment({"required":false})` creates only a satisfied **payment gate**. It makes no provider discovery/request and rejects mandates or verification. No paid code, provider configuration, checkout or payment account is needed for a free agreement. Paid-profile implementation is optional for deployers; its optionality does not turn payment-gate success into permission to access an asset.

No automatic debit, bank/card data, traffic-to-charge mapping, refund initiation, dispute processing, tax calculation, irrevocable-settlement claim or production payment adapter is supplied. Reversal is an authenticated status fact, not a refund command. Implementing this profile must not silently standardize those omitted provider functions.

## Recorded focused verification

From `work/odexa-next`:

```sh
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p test_payment_profile.py -v
```

Observed result: **24 tests passed**. Coverage includes exact-byte quote identity, fee sums, wrong payer, separate mandate, scope/check/authority substitution, denied authority, malformed amounts/JSON, quote/mandate/access time boundaries, stale/out-of-order responses, retry conflicts, reversal, immutable inputs and free bypass. One test also checks the semantic classifications of all **27 machine-readable corpus cases** in `profiles/fixtures/payment-cases.json`.

The corpus supplies `$defs` names, values and independent structural/semantic expected results; some semantically invalid values intentionally pass structural JSON Schema. Independent AJV and cross-profile resolver integration are run separately by the coordinating agent. These focused results alone do not claim either of those results, HTTP integration, durable financial handling or RC conformance.

## Additional integration evidence

The coordinating workbench now records independent AJV checks, five actual-resolver/payment contract tests, a durable local SQLite ledger and a signed-response adapter. See [the current report](../verification/REPORT.md), [ledger](PAYMENT-LEDGER.md) and [signature boundary](PAYMENT-AUTHENTICATION.md). These additions do not turn the pure contract into a complete provider HTTP service.
