x402 payment contracts
currentThese two contracts let a client pay for a single HTTP request rather than holding an account or pre-funding an allowance, following the x402 pattern that revives the HTTP 402 Payment Required response. A paywall verifies a signed payment and grants time-bound access; a facilitator settles such payments for a provider and takes a fee. This page is for contracted integrators building pay-per-request services on the Citrate Network.
What it is
In the x402 pattern a server answers a request with 402 Payment Required, the client signs an authorization
to move payment, and the server grants access once that authorization settles. On the Citrate Network the
payment is carried by Wrapped SALT, the EIP-3009 wrapper documented in
economics contracts: the client signs a transferWithAuthorization over wSALT, and
the server submits it. The client never needs to pre-approve an allowance, so the request can be paid for
with a single signature.
| Surface | Contract | What it does |
|---|---|---|
| SC-x402-paywall | X402Paywall | verify a signed payment for a resource and grant time-bound access |
| SC-x402-facilitator | X402Facilitator | settle signed payments on a provider's behalf, single or batched, taking a fee |
Both contracts are tier commercial. Their bytecode and interfaces are public on chain, but the settlement and fee logic is written for contracted builders. The paywall is explicitly a reference: a production provider should review the access and replay properties against its own needs before relying on it.
This is the contract layer. The Citrate Network also accelerates x402 signature checks below the contract
layer, in the node's x402 payment precompiles at addresses 0x0200 to 0x0209, which verify EIP-712 and
EIP-3009 payloads about nine times more cheaply than the equivalent Solidity and fail closed by returning
the zero address on a bad signature. The contracts here do not call those precompiles directly; they settle
through wSALT's EIP-3009 functions, and the precompiles are the faster path for a provider that verifies a
payload off the contract path. For the precompile surface see chain precompiles, and
for how pay-per-request fits the compute marketplace see Citrate Market.
How to use it
The direct paywall path needs no facilitator. A provider runs it like this.
- Answer the client's request with 402 Payment Required, naming the resource and its price.
- The client signs a wSALT
transferWithAuthorizationto the provider's address for at least the price, with a fresh nonce and a validity window. - The provider calls
X402Paywall.verifyAndGrant(resourceId, from, value, validAfter, validBefore, nonce, salt, v, r, s), where thenoncemust equalkeccak256(abi.encode(resourceId, salt))(the C047 nonce-binding check). On success the payment settles to the provider and access is granted untilnow + accessTTL. - The provider serves the resource. For later requests it checks
hasAccess(payer, resourceId)and serves without a new payment until the grant expires.
To collect through a facilitator that takes a fee instead, the facilitator holds FACILITATOR_ROLE and
calls X402Facilitator.settlePayment(...) with the client's signed authorization; the gross value is split
into the net amount to the provider and the fee to the treasury inside wSALT, in one authorization.
Reference
Every function below is read from the cited .sol file at the audited SHA.
X402Facilitator
contracts/src/X402Facilitator.sol, is AccessControl, ReentrancyGuard. Constructor
(wSALT, treasury, feeBps), with feeBps capped at 1000, ten percent. It settles signed payments and
routes a fee to the treasury. The role FACILITATOR_ROLE may settle; DEFAULT_ADMIN_ROLE may change the
fee and treasury.
| Function | Access | Purpose |
|---|---|---|
settlePayment(from, to, value, validAfter, validBefore, nonce, v, r, s) | FACILITATOR_ROLE | settle one payment; consumes a single signed authorization for the gross value and splits net plus fee inside wSALT |
batchSettle(PaymentAuthorization[] payments) | FACILITATOR_ROLE | settle a batch, one authorization each, emitting a BatchSettled summary |
setFacilitatorFee(uint256 newFeeBps) | DEFAULT_ADMIN_ROLE | change the fee, capped at 1000 bps |
setTreasury(address newTreasury) | DEFAULT_ADMIN_ROLE | change the fee recipient |
Settlement calls wSALT.transferWithFeeAuthorization, which consumes one EIP-3009 authorization for the
gross value and splits it internally, so no separate ERC-20 approval is needed and the single-signature
property holds. Public state: wSALT, immutable, treasury, feeBps, and the FACILITATOR_ROLE
identifier. The PaymentAuthorization struct is (from, to, value, validAfter, validBefore, nonce, v, r, s). Events: PaymentSettled(from, to, value, fee, nonce), BatchSettled(count, totalValue, totalFees),
FeeUpdated, TreasuryUpdated.
X402Paywall
contracts/src/X402Paywall.sol. A reference resource gate. Constructor (wSALT, resourcePrice); the
deployer becomes the provider. Access is time-bound: accessGrantedUntil[key] holds the expiry timestamp
and accessTTL is applied at grant time, defaulting to DEFAULT_ACCESS_TTL = 1 days.
| Function | Access | Purpose |
|---|---|---|
verifyAndGrant(resourceId, from, value, validAfter, validBefore, nonce, salt, v, r, s) | any caller | require value >= resourcePrice, nonce == keccak256(abi.encode(resourceId, salt)), and that current access has expired, run the wSALT transfer to the provider, and grant access for now + accessTTL |
hasAccess(address payer, bytes32 resourceId) | view | true while the grant's expiry is in the future |
setPrice(uint256 newPrice) | the provider | change the resource price |
setAccessTTL(uint256 newTTL) | the provider | change the access window; must be greater than zero |
Public state: wSALT and provider, both immutable, resourcePrice, accessGrantedUntil, accessTTL,
and DEFAULT_ACCESS_TTL. The access key is keccak256(abi.encodePacked(from, resourceId)). Events:
AccessGranted(payer, resourceId, amount, expiresAt), PriceUpdated, AccessTtlUpdated.
A direct paywall purchase reads as follows.
1. Client GET /resource -> 402 Payment Required (price, resourceId)
2. Client signs wSALT transferWithAuthorization to the provider
3. Provider calls X402Paywall.verifyAndGrant(resourceId, from, value, ...)
4. On success the payment settles; access is valid for accessTTLDesign rationale
The split between a paywall and a facilitator follows the two ways a provider actually collects. A small provider gating its own endpoint wants no intermediary, so the paywall settles straight to the provider and records nothing but an expiry. A provider that wants someone else to handle settlement, or that batches many payments, uses the facilitator, which is why the fee leg lives there and not in the paywall. Both lean on wSALT's fee-bearing authorization so the client signs once for the gross amount and the contract does the routing; the alternative, a separate allowance and a second transfer, would have meant either an unbounded approval or a settlement that could half-complete. Access is time-bound rather than permanent so a paid grant expires and the same resource can be sold again, which is the normal shape of a metered service.
Failure modes
These contracts collect payment, so they are written to fail closed.
verifyAndGrantreverts if the payment is below the price, and refuses to grant while an existing grant is still active, so a single signed authorization cannot be replayed into a second window. The wSALT transfer itself rejects a reused nonce or an expired validity window.- The facilitator's fee is capped at ten percent in both the constructor and
setFacilitatorFee, and onlyFACILITATOR_ROLEmay settle, so an arbitrary caller cannot push payments through it. - The fee split happens inside wSALT against a digest that binds
(treasury, fee), so a submitter cannot redirect the fee, and the authorization is marked used once, so a settlement cannot be repeated. - As a reference, the paywall leaves one judgment to the integrator: its access window is a single
per-
(payer, resourceId)expiry, which is correct for a metered resource but should be reviewed for a service that needs per-request rather than per-window gating.
Access and canon
Tier commercial. These contracts are the network's pay-per-request settlement path; the bytecode and interfaces are public on chain, but the settlement and fee detail is written for contracted builders. The paywall is a reference implementation for documentation and testing, not a turnkey production gate. No keys, mnemonics, or private endpoints appear on this page or are needed to read these contracts.
Source and verification
- Source repo:
citrate-chainat SHA9d5959e. - Files:
contracts/src/X402Facilitator.solandcontracts/src/X402Paywall.sol. - Related: the wSALT wrapper these settle through is in
contracts/src/WrappedSALT.sol, documented in economics contracts; the node-level x402 precompiles are at0x0200to0x0209, documented in chain precompiles. - Addresses: both contracts are in the canonical registry
contracts/addresses/40204.jsonfor chain 40204, asX402FacilitatorandX402Paywall. Resolve from there and confirm witheth_getCodebefore sending value. - Status: Implemented, pre-audit. Both contracts run on testnet 40204 and carry SOL-01 and SOL-12 remediations in their source comments, but no external third-party audit has been completed. Treat as experimental.