Skip to content

x402 Payments

The rine.x402 subpackage holds the client-side payment surface: wallet signing, spend policy, and the payer/payee helpers that build, sign, and reconcile the three x402 message types (rine.v1.x402_payment_required, rine.v1.x402_payment, rine.v1.x402_receipt). Settlement is peer-to-peer through the payee's configured facilitator — rine relays the encrypted payloads and never custodies funds.

Wallet signing requires the payments extra:

pip install rine[payments]

For the end-to-end flow see the Payments guide. Import every public symbol below from rine.x402:

from rine.x402 import prepare_payment, FacilitatorClient, fulfill, X402Error

Payer flow

rine.x402.flow.prepare_payment(config_dir, agent_id, payment_required, *, message_id=None, force=False, now=None, nonce=None, add_marker=True)

Payer: select + sign a payment and reserve its spend (fail-closed).

Loads the agent's spend policy (None → deny-by-default), wallet key, and today's spend from disk, selects an acceptable requirement, signs, and seals the rine.v1.x402_payment message. As its FINAL local act it calls :func:rine.x402.policy.reserve_spend, which re-enforces policy and debits the daily journal atomically under a cross-process lock — so the budget is reserved at signing, before transmit, and the debit stands regardless of send outcome.

message_id (the received payment_required message id) makes the debit idempotent per day: a second prepare for the same id raises already-paid unless force=True. It is ALSO stamped into the signed payload's extensions[X402_QUOTE_REF_KEY] so the payee can bind this payment to the ONE quote it answers; omitting it leaves the payment unbound and the payee then requires explicit expected terms to settle (fail closed). Raises :class:rine.x402.errors.X402Error if no requirement satisfies the policy, if the lock is contended (wallet-busy), or on a replay (already-paid).

add_marker threads through to the built message so this high-level flow can emit a fully-sealed rine.v1.x402_payment (no cleartext status marker), matching the payer control the TS SDK's fulfill/sendReceipt expose.

rine.x402.flow.PreparedPayment dataclass

The result of :func:prepare_payment — signed message + accounting facts.

auto_payable reflects the policy's autoPayThreshold: a caller honoring --auto-pay sends without a human prompt only when this is True.

rine.x402.pay.select_requirement(pr, policy, daily_spent_canonical)

Deterministically pick the first accepts[] entry that passes policy.

Iterates in the payee's advertised order (their preference wins). Deny-by- default: a None policy makes every candidate fail. Raises :class:X402Error no-acceptable-requirement (carrying the last per-candidate rejection reason) if none pass.

rine.x402.pay.build_payment_payload(pr, requirement, wallet_private_key, *, now=None, nonce=None, valid_after_skew_seconds=_DEFAULT_SKEW_SECONDS, quote_message_id=None)

Build and sign a PaymentPayload for a chosen requirement.

Echoes resource/extensions and the chosen requirement (accepted) verbatim per the x402 contract. now/nonce are injectable for tests.

When quote_message_id is supplied it is stamped into the SIGNED, E2EE extensions[X402_QUOTE_REF_KEY] so the payee binds this payment to the ONE x402_payment_required it authored. The quote's own extensions are cloned first and never mutated. Every rine payer surface passes the received quote's message id; a raw HTTP-402 flow (no rine quote message) omits it, and the payee then requires an explicit expected-terms override to settle (fail closed). Mirrors rine-core buildPaymentPayload opts.quoteMessageId.

Payee flow

rine.x402.payee.fulfill(client, payment, *, facilitator, agent=None, add_marker=True, expected=None) async

Reconcile-first payee flow: reconcile → verify → settle → receipt.

Quote binding (fail closed). Before contacting the facilitator the payee binds the payment to the ONE quote it answers (:func:resolve_payee_requirement): the payer stamped that quote's id into the signed extensions[X402_QUOTE_REF_KEY] and the payee re-loads THAT message (proving it is a payee-issued quote in this conversation), then reconciles pp['accepted'] against its accepts[]. That RECONCILED payee-owned requirement (never pp['accepted']) is forwarded to /verify + /settle. A no/forged/cross-quote ref — or a malformed payload — is REFUSED with a success=False receipt and no settle; pass expected to authorise an out-of-band quote (it wins over the ref). On a valid verification /settle runs and its response is the receipt verbatim; otherwise /settle is skipped and a failure receipt (payer/network off the SIGNED payload) threads.

Parameters:

Name Type Description Default
client RineClient

Async :class:~rine.RineClient used to load the quote + reply.

required
payment DecryptedMessage

The decrypted rine.v1.x402_payment message to fulfil.

required
facilitator FacilitatorClient

Caller-owned FacilitatorClient (external HTTP, never rine).

required
agent str | None

The acting payee agent (handle/UUID) for multi-agent orgs.

None
add_marker bool

Emit the {status, rail} cleartext marker (default True).

True
expected PaymentRequirements | None

Explicit expected terms for an out-of-band quote; the authoritative single-entry quote when supplied. Absent both a ref and expected, fulfil fails closed.

None

Raises:

Type Description
X402Error

If payment is the wrong type or cannot be decrypted (a malformed body or a reconciliation mismatch are graceful receipts).

rine.x402.payee.send_receipt(client, payment, settlement, *, agent=None, add_marker=True) async

Thread a receipt (a facilitator SettlementResponse) to the payer.

Parity with TS sendReceipt: wraps the settlement verbatim via :func:receipt_from_settlement (unknown keys preserved) and replies in the payment's thread — the settle-after-delivery flow where verify/settle already ran separately.

rine.x402.payee.FulfillResult dataclass

Outcome of a payee :func:fulfill.

settlement is None on the failed-verification/refusal branch (settle was skipped); receipt is the sent rine.v1.x402_receipt message. matched_quote reports which payee-issued quote the settled payment answered — its id + reconciled amount/asset/network — so an integration can confirm the paid quote corresponds to the good it delivers; it is None on a refusal (no quote was matched). Mirrors the TS FulfillResult.

rine.x402.payee_settle.verify(payment, *, facilitator, expected=None) async

Reconciling standalone verify (no broadcast) — settle-after-delivery.

Binds the payment to the payee-owned expected terms FIRST; with no expected (no thread context here) it fails closed with NO_QUOTE rather than trusting the payer's echoed accepted. The RECONCILED requirement — never pp['accepted'] — is what reaches the facilitator, so a raw FacilitatorClient caller still settles only payee-authored terms. Mirrors the TS-SDK payee verify.

rine.x402.payee_settle.settle(payment, *, facilitator, expected=None) async

Reconciling standalone settle (broadcast). See :func:verify — same binding.

rine.x402.payee_settle.resolve_payee_requirement(client, pp, conversation_id, *, expected, agent) async

Resolve the payee-owned requirement to settle against + its provenance.

An explicit expected override wins (operator intent, no ref needed); else the payment's referenced quote is loaded via :func:load_referenced_quote and reconciled. Returns the matched payee-authored requirement (forwarded to the facilitator, never pp['accepted']) and its :class:MatchedQuote, or raises a typed :class:X402Error (no-quote / requirement-mismatch) — fail closed.

rine.x402.payee_settle.resolve_requirement(pp, expected)

Resolve the payee-owned requirement with NO thread context (standalone path).

Reconciles the payment against the operator's expected terms ONLY; without them it fails closed with NO_QUOTE rather than trusting the payer's echoed accepted. Mirrors the TS-SDK resolveRequirement(pp, [], expected) used by the settle-after-delivery verify/settle primitives.

rine.x402.payee_settle.MatchedQuote dataclass

Provenance of the reconciled quote so an integration binds good⇄quote.

quote_message_id is the referenced x402_payment_required id the payment answered, or None for an operator expected-terms override. amount is the reconciled requirement's atomic amount. Mirrors the TS MatchedQuote.

rine.x402.reconcile.reconcile_payment(payment, quote_accepts)

Return the payee-authored requirement the payment legitimately answers, or raise.

A quote entry matches iff ALL hold: same scheme; same network; same asset (case-insensitive); same payTo (case-insensitive); and the tier the payer's OWN accepted block NAMES (its exact atomic amount) equals the quote entry's atomic amount.

Matching on the EXACT named amount (not the first tier the amount happens to satisfy) is load-bearing: a multi-tier same-asset quote — accepts=[{1.0},{5.0}] on one asset/network/payTo — must return the tier the 5.0 payer selected, never the cheaper 1.0 entry a >= first-match would pick. Binding to the NAMED tier keeps the settled amount and matchedQuote aligned with the quote the payer answered.

Defense-in-depth: the SIGNED EIP-3009 payload.authorization.value — not the payer-controlled echoed accepted.amount — must cover the quoted atomic amount, so a payment whose signed value underfunds the tier it names is refused LOCALLY even if a non-conformant facilitator would settle it. Overpayment (a signed value above the tier) is still allowed. Amounts compare in the token's own atomic units — asset+network already match, so no decimal scaling is needed and unknown-decimal assets don't spuriously block here.

The RETURNED value is the payee's own requirement; callers forward THAT to the facilitator, never payment['accepted'].

Raises:

Type Description
X402Error

no-quote when quote_accepts is empty (fail closed); requirement-mismatch when no entry matches or the signed value underfunds the named tier; invalid-requirement when the signed value is missing / non-integer.

rine.x402.quote_ref.load_referenced_quote(client, payment, conversation_id, agent) async

Resolve extensions[X402_QUOTE_REF_KEY] to the payee's OWN issued quote.

Fetches the referenced message by id, proves it is (a) an x402_payment_required, (b) issued by THIS payee (from_agent_id == payee — the real binding), (c) in the SAME conversation, decrypts it via the payee self-seal, and returns its accepts[]. Fails closed with a typed :class:X402Error (no-quote) on a missing/blank ref, a GET failure, a type/from/conversation mismatch, a missing self-seal, an invalid sender signature, or a decrypt/parse failure.

rine.x402.quote_ref.ReferencedQuote dataclass

The referenced quote's message id and its payee-authored accepts[].

Facilitator

rine.x402.facilitator.FacilitatorClient

Async facilitator client (verify/settle) over the x402 HTTP API.

verify(payment, requirement) async

Verify a signed authorization (signature + balance; no broadcast).

settle(payment, requirement) async

Broadcast the authorization on-chain, returning a SettlementResponse.

aclose() async

Close the underlying HTTP client.

rine.x402.facilitator.FacilitatorPreset dataclass

A named facilitator: its base URL and whether it needs a self-hosted URL.

Messages

rine.x402.messages.build_payment_required(accepts, *, resource=None, error=None, extensions=None, add_marker=True)

Payee quote: build a PaymentRequired (the 402 challenge, accepts[]).

rine.x402.messages.payment_message(payment_payload, *, add_marker=True)

Payer: wrap a signed PaymentPayload (from build_payment_payload).

rine.x402.messages.build_receipt(*, success, transaction, network, payer, error_reason=None, amount=None, extensions=None, add_marker=True)

Payee receipt: build a SettlementResponse (marker completed/failed).

rine.x402.messages.receipt_from_settlement(settlement, *, add_marker=True)

Wrap a facilitator SettlementResponse verbatim as a receipt message.

Preferred over :func:build_receipt when relaying a facilitator's /settle result — it preserves unknown keys (extensions etc.) byte-for-byte.

rine.x402.messages.parse_x402_payload(plaintext)

Parse a decrypted x402 message plaintext into its verbatim object dict.

Accepts the raw decrypted JSON string (as the inbox stores it) or an already parsed mapping, and returns the object UNCHANGED (unknown keys preserved) so parse(build(x)) == x byte-for-byte. A body that is not a JSON object raises a typed X402Error(MALFORMED_PAYLOAD) — never a bare ValueError — so callers get a structured, catchable refusal.

Parser-parity note: the TS parseX402Payload(type, plaintext) takes the message type and folds the per-type structural checks inline. The Python parser is intentionally single-argument (type-agnostic JSON-object validation only); the equivalent per-type checks live at the type-aware call sites — a payment's accepted/authorization in :func:rine.x402.extract.extract_payment_payload, a quote's accepts[] in :func:rine.x402.quote_ref.load_referenced_quote. This split is deliberate and consistent (it keeps this function's single-argument public contract) — the same malformed body is a typed refusal on both SDKs, only the check's location differs.

Raises:

Type Description
X402Error

malformed-payload when the body is not a JSON object.

rine.x402.messages.X402Message dataclass

A ready-to-send x402 message: payload + type + content_type + marker.

payload is the verbatim x402 object (encrypted by send/reply). metadata carries the optional cleartext marker (empty when add_marker is False). Spread the fields into the client call::

msg = build_payment_required(accepts=[...])
client.send(to, msg.payload, message_type=msg.message_type,
            content_type=msg.content_type, metadata=msg.metadata)

Wallet

rine.x402.create_wallet(config_dir, agent_id)

Create a wallet for an agent, refusing to clobber an existing key.

Returns:

Type Description
str

The derived EIP-55 checksummed EVM address.

rine.x402.get_wallet_address(config_dir, agent_id)

Derive the EVM address for the agent's stored wallet key.

rine.x402.wallet_key_exists(config_dir, agent_id)

Whether a wallet key exists on disk for the agent.

rine.x402.load_wallet_key(config_dir, agent_id)

Load the wallet private key from disk (32 raw bytes).

Spend policy

rine.x402.types.PaymentPolicy dataclass

Client-side spend policy, enforced at signing.

All cap fields are decimal major-unit strings (e.g. "10.00") interpreted in canonical 6-decimal stablecoin units. allowed_assets entries are symbols ("EURC") or 0x contract addresses; allowed_networks are CAIP-2 ids. Absence of a policy is a hard deny (deny-by-default).

On-disk JSON uses the camelCase keys of the TS reference so the file is interoperable across SDKs: perTxCap, dailyCap, allowedAssets, allowedNetworks, autoPayThreshold.

to_json()

Serialize to the interoperable camelCase on-disk shape.

from_json(data) classmethod

Parse the interoperable camelCase on-disk shape.

rine.x402.policy.load_policy(config_dir, agent_id)

Load the agent's policy, or None if unset/unreadable (deny-by-default).

rine.x402.policy.save_policy(config_dir, agent_id, policy)

Validate and persist the agent's policy atomically (0o600).

rine.x402.policy.validate_policy(policy)

Structurally validate a policy before persisting it.

rine.x402.policy.enforce_policy(policy, req, daily_spent_canonical)

Enforce the spend policy for a single requirement.

Deny-by-default: a None policy always raises. Raises a typed :class:X402Error on any violation; returns silently when payable.

Parameters:

Name Type Description Default
policy PaymentPolicy | None

The active policy, or None (hard deny).

required
req PaymentRequirements

The candidate PaymentRequirements.

required
daily_spent_canonical int

Already-spent amount today, in canonical micro-units.

required

rine.x402.policy.is_auto_payable(policy, req)

Whether a requirement is at/below the auto-pay threshold.

Daily journal

rine.x402.journal.read_daily_spent(config_dir, agent_id, now_seconds=None)

Today's spend in canonical micro-units (0 if the journal is a prior day).

rine.x402.journal.record_spend(config_dir, agent_id, canonical_amount, now_seconds=None)

Add a spend to today's bucket, atomic write, preserving paidIds.

Day-rollover resets the bucket. Retained for status displays and tests; no flow calls this — the pay path reserves via :func:reserve_spend, which is the only cap-enforcing writer.

rine.x402.journal.reserve_spend(config_dir, agent_id, policy, req, *, message_id=None, force=False, now_seconds=None)

Enforce policy and debit the daily journal atomically, under a lock (S2 fix).

This is the LAST local act before a caller transmits; the debit is permanent (fail-closed, no refund path). Under the cross-process lock the journal is re-read fresh (UTC rollover applies), a replay is refused (message_id already in paidIds and not forcealready-paid), the full :func:enforce_policy re-runs against the freshly read spend, then spent + value is written and message_id recorded.

Returns the canonical micro-unit value reserved. Raises :class:rine.x402.errors.X402Error (wallet-busy, already-paid, or any policy code) with the journal untouched on refusal.

Status marker

rine.x402.types.X402Status

The four — and only four — coarse lifecycle states in the marker.

rine.x402.types.X402Mode

Settlement-timing modes advertised in card terms / stated in the quote.

rine.x402.marker.build_marker(status)

Build the {status, rail} marker object for the given status enum.

rine.x402.marker.marker_metadata(message_type, *, success=True)

Build the metadata fragment carrying the marker for a message type.

rine.x402.marker.read_marker(metadata)

Extract a valid x402 marker from message metadata, or None.

Fail-closed: an entry with a status outside the four-value enum is ignored.

rine.x402.marker.status_for_message_type(message_type, *, success=True)

The marker status a sender sets for a given x402 message type.

Parameters:

Name Type Description Default
message_type str

One of the three rine.v1.x402_* types.

required
success bool

For a receipt, whether settlement succeeded (else payment-failed).

True

Raises:

Type Description
ValueError

If message_type is not an x402 payment type.

Errors

x402 helpers raise X402Error, which is separate from the RineError hierarchy — catch it alongside the core SDK errors.

rine.x402.errors.X402Error

Bases: Exception

A structured, typed error (never a bare string) for all x402 refusals.

Parameters:

Name Type Description Default
code str

One of the :class:X402ErrorCode string constants.

required
message str

Human-readable explanation.

required
details dict[str, object] | None

Optional structured context (amounts, caps, asset ids).

None

rine.x402.errors.X402ErrorCode

Stable string codes carried on every :class:X402Error.