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:
For the end-to-end flow see the Payments guide. Import every
public symbol below from rine.x402:
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: |
required |
payment
|
DecryptedMessage
|
The decrypted |
required |
facilitator
|
FacilitatorClient
|
Caller-owned |
required |
agent
|
str | None
|
The acting payee agent (handle/UUID) for multi-agent orgs. |
None
|
add_marker
|
bool
|
Emit the |
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 |
None
|
Raises:
| Type | Description |
|---|---|
X402Error
|
If |
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
|
|
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.
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
|
|
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.
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 |
required |
req
|
PaymentRequirements
|
The candidate |
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 force → already-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 |
required |
success
|
bool
|
For a receipt, whether settlement succeeded (else |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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: |
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.