Skip to main content
Rail402 settles from both classic keypairs and custom __check_auth contract accounts, and the upto scheme composes with a Stellar smart-account spending policy to keep an agent inside a budget. This page covers both: the facilitator is address-agnostic, and a spending policy reconciles an agent’s budget on the ledger using the upto settlement hook.

The facilitator is address-agnostic

A buyer address can be a classic G… keypair or a C… contract account, and the facilitator settles from both with no special-casing. The difference is only in who produces the signature: a keypair signs its authorization entries directly, while a contract account’s __check_auth decides whether to authorize according to whatever logic it holds, whether a session key, a multisig, or a spending policy. This is proven on chain for both schemes from a C… account: exact at 168929e9… and upto at 0d78d7cf…, both through the same /verify and /settle a keypair uses.

Composition with OpenZeppelin

Shipping our own account cryptography was never the goal, so the smart-account path is built on OpenZeppelin’s audited stellar-accounts. The division of labor is deliberate: Rail402’s policy implements OpenZeppelin’s Policy trait, so the audited core keeps everything cryptographic and Rail402 owns only the budget math. The verifier and the policy are shared singletons, meaning one deployment serves every account, which is what makes per-agent smart accounts affordable (about 0.002 XLM to instantiate one).
OpenZeppelin’s own spending_limit policy refuses settle and approve calls, because it is built for plain transfers, so an x402-aware policy is required. OpenZeppelin’s docs name that as a first-class extension point, and Rail402’s policy is that x402-aware budget.

Reserve, then reconcile

A budget an agent cannot exceed must be checked on the ledger, not in the client. The policy does that with a reserve-then-reconcile pattern that pairs with the upto contract’s settlement hook:
1

enforce reserves the ceiling

When the authorization is created, the policy’s enforce reserves the full ceiling (max_amount) against a rolling per-period budget and records a Reservation keyed by the settlement nonce. The worst case is booked up front, so a second concurrent request cannot double-spend the same budget.
2

release reconciles to the actual charge

After the transfer, the upto contract calls the policy’s release(from, nonce, actual). The policy refunds reserved - actual back to the budget and removes the reservation. The budget ends at the real charge, not the ceiling.
Because enforce reserves the ceiling and only release can lower it, a budget that ends at the actual charge is conclusive proof the hook ran. The 0d78d7cf… settlement authorized a 2,000,000 ceiling and settled 750,000, and the on-ledger budget afterwards reads 750,000, not 2,000,000. Two safety details make the reconciliation trustworthy:
  • Only the right contract can release. release calls reservation.settlement_contract.require_auth(), so a budget can only be reconciled by the settlement contract the ceiling was authorized against, not by any caller.
  • approve is allowed but not budgeted. The auth tree’s approve sub-invocation is permitted by the policy but not counted against the budget, so a ceiling is never double-charged.
An over-budget payment is refused on the ledger by the policy itself, and the facilitator reports it as invalid_exact_stellar_payload_account_policy_refused, a coded rejection rather than a crash. The oz-account canary proves this end to end, including the refusal.

Integration traps worth knowing

These are Stellar smart-account specifics that are not obvious from OpenZeppelin’s docs, and each cost real debugging.
  • Signers do not sign the raw __check_auth payload. OpenZeppelin binds the chosen context rules into the digest: auth_digest = sha256(signature_payload || context_rule_ids.to_xdr()). Signing the raw payload fails with Error(Auth, InvalidAction).
  • The buyer signs each entry via the { signatureScVal, address } callback form of authorizeEntry, not by handing over a secret key. The account never exposes one.
  • The policy must not sit on the administration rule. It fails closed on any non-payment call, so an account whose only rule carried it could never be reconfigured. The owner’s rule carries no policy; the agent’s scoped CallContract rules do.
  • One context-rule id per auth context. upto produces two auth contexts (settle on the upto contract and approve on the token), so the account needs a two-rule layout.
  • Re-cost after signing, and raise the fee ceiling. Signed entries are larger than the unsigned ones the first simulation priced, and a smart-account payment cross-calls a verifier and a policy, so it costs 7 to 9 times a keypair payment and needs MAX_TRANSACTION_FEE_STROOPS raised. Passing the payload to the facilitator sidesteps the re-cost, because the facilitator re-sources and re-simulates the transaction itself.

Where it lives

Next steps

The upto scheme

The settlement hook the policy plugs into.

Fee sponsorship and ceilings

Why a smart-account settlement costs more.

Verify it yourself

The C-account settlements on chain.

Buyer smart accounts

Using a smart account as a buyer.