Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

x402 protocol mechanics

sorocharge-x402 implements x402 protocol v2’s exact scheme on Stellar — the scheme Stellar’s own x402 implementation (exact on stellar:pubnet / stellar:testnet) uses. Only this scheme and only these two networks are supported; the wire formats below are copied from the spec’s specs/schemes/exact/scheme_exact_stellar.md, not inferred.

The flow

Client                                            Server                 Facilitator
   │  GET /resource                                   │                        │
   ├──────────────────────────────────────────────────>                       │
   │  402 Payment Required                             │                        │
   │  PAYMENT-REQUIRED: <base64 PaymentRequired>        │                        │
   <──────────────────────────────────────────────────┤                        │
   │  (build + sign a charge entry)                     │                        │
   │  GET /resource                                     │                        │
   │  PAYMENT-SIGNATURE: <base64 PaymentPayload>         │                        │
   ├──────────────────────────────────────────────────>│  POST /verify           │
   │                                                     ├───────────────────────>│
   │                                                     │  POST /settle          │
   │                                                     ├───────────────────────>│
   │  200 OK                                            │                        │
   │  PAYMENT-RESPONSE: <base64 SettlementResponse>      │                        │
   <──────────────────────────────────────────────────┤                        │

sorocharge-x402::Facilitator implements the right-hand side (verify/settle) as plain async functions — it does not bind them to real /verify//settle HTTP routes. Wiring that is the resource server’s job, using whatever framework it already runs (see examples/resource_server_x402.rs for a complete reference implementation using axum).

The payload

PaymentPayload.payload for the Stellar exact scheme is not a bare signed authorization entry — it’s the base64-encoded XDR of a full TransactionEnvelope with exactly one invokeHostFunction operation (the SEP-41 transfer call) carrying the signed entry as its sole auth entry:

{
  "transaction": "AAAAAgAAAABriIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1AAAAAAAAAAAAAAAAAAAAA..."
}

The transaction’s source account is the all-zeros account (GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF), fee 0, and sequence number 0 — all three are placeholders the facilitator replaces entirely at settlement, per the spec’s extra.areFeesSponsored: true (currently always true; the spec does not yet define a non-sponsored flow).

Credential type

The client defaults to the legacy (non-AddressV2) credential type for the broadest facilitator compatibility, even though the spec permits AddressV2 too — its own words: “recording-mode simulation still returns the legacy arm as of Protocol 28.” The facilitator accepts both. Neither side will ever emit or accept a delegated-signer credential (sorobanCredentialsAddressWithDelegates); the spec explicitly forbids it.

Facilitator safety

Because the facilitator signs the settlement transaction as fee sponsor, it has to defend itself against a client trying to trick it into paying for, or becoming a party to, an unintended transfer. Before settling, Facilitator checks:

  • the transaction’s source account is not the facilitator’s own address
  • the operation’s source account (if set) is not the facilitator’s own address
  • the from argument of the transfer call is not the facilitator’s own address
  • the facilitator’s address does not appear in any authorization entry
  • a settlement-time re-simulation shows only the expected balance change — not a second transfer, a mint, a burn, a clawback, or a balance change on any other contract (sorocharge_signer::verify_transfer_effects)

Expiration

The authorization entry’s expiration ledger is derived from maxTimeoutSeconds in the payment requirements: currentLedger + ceil(maxTimeoutSeconds / 5). The facilitator enforces this as a hard upper bound, not just a lower one — an entry that’s valid longer than the requirements asked for is rejected (ExpirationExceedsAllowance), exactly like an entry that’s already expired.