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
fromargument of thetransfercall 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.