Almanac
guestPublic

x402 payment contracts

current
source: citrate-chain/contracts/src/{X402Facilitator,X402Paywall}.sol

These 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.

SurfaceContractWhat it does
SC-x402-paywallX402Paywallverify a signed payment for a resource and grant time-bound access
SC-x402-facilitatorX402Facilitatorsettle 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.

  1. Answer the client's request with 402 Payment Required, naming the resource and its price.
  2. The client signs a wSALT transferWithAuthorization to the provider's address for at least the price, with a fresh nonce and a validity window.
  3. The provider calls X402Paywall.verifyAndGrant(resourceId, from, value, validAfter, validBefore, nonce, salt, v, r, s), where the nonce must equal keccak256(abi.encode(resourceId, salt)) (the C047 nonce-binding check). On success the payment settles to the provider and access is granted until now + accessTTL.
  4. 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.

FunctionAccessPurpose
settlePayment(from, to, value, validAfter, validBefore, nonce, v, r, s)FACILITATOR_ROLEsettle one payment; consumes a single signed authorization for the gross value and splits net plus fee inside wSALT
batchSettle(PaymentAuthorization[] payments)FACILITATOR_ROLEsettle a batch, one authorization each, emitting a BatchSettled summary
setFacilitatorFee(uint256 newFeeBps)DEFAULT_ADMIN_ROLEchange the fee, capped at 1000 bps
setTreasury(address newTreasury)DEFAULT_ADMIN_ROLEchange 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.

FunctionAccessPurpose
verifyAndGrant(resourceId, from, value, validAfter, validBefore, nonce, salt, v, r, s)any callerrequire 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)viewtrue while the grant's expiry is in the future
setPrice(uint256 newPrice)the providerchange the resource price
setAccessTTL(uint256 newTTL)the providerchange 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 accessTTL

Design 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.

  • verifyAndGrant reverts 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 only FACILITATOR_ROLE may 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-chain at SHA 9d5959e.
  • Files: contracts/src/X402Facilitator.sol and contracts/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 at 0x0200 to 0x0209, documented in chain precompiles.
  • Addresses: both contracts are in the canonical registry contracts/addresses/40204.json for chain 40204, as X402Facilitator and X402Paywall. Resolve from there and confirm with eth_getCode before 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.