Protocol

tab is an x402 payment scheme called tab-vault. It keeps the shape of x402: a seller answers 402 with what it accepts, the buyer retries with a signed payment in a header, and a facilitator verifies and settles. The difference is where the money sits. It sits in a vault the owner controls, and the agent signs with a key whose policy the vault enforces.

Parties

The 402 response

A seller that wants payment returns status 402 with a JSON body. The accepts list follows x402. The tab-specific fields are in extra.

{
  "x402Version": 1,
  "error": "Payment required",
  "accepts": [{
    "scheme": "tab-vault",
    "network": "eip155:4663",
    "maxAmountRequired": "10000",
    "resource": "https://example.com/research",
    "description": "",
    "mimeType": "application/json",
    "payTo": "0xSeller",
    "maxTimeoutSeconds": 60,
    "asset": "0xUSDG",
    "extra": {
      "vault": "0xVault",
      "name": "TabVault",
      "version": "1",
      "facilitator": "https://tab.example/x402"
    }
  }]
}

Amounts are strings in the token's base units. 10000 is 0.01 USDG.

The buyer retries the same request with an X-PAYMENT header: base64 of this JSON.

{
  "x402Version": 1,
  "scheme": "tab-vault",
  "network": "eip155:4663",
  "payload": {
    "owner": "0xOwner",
    "agent": "0xAgentKey",
    "token": "0xUSDG",
    "to": "0xSeller",
    "amount": "10000",
    "nonce": "0x…32 bytes…",
    "deadline": 1757200000,
    "signature": "0x…65 bytes…"
  }
}

The nonce is random and scoped to the owner. The deadline is a unix time after which the vault refuses the payment. The seller answers a successful paid request with X-PAYMENT-RESPONSE, base64 of the settlement result below.

Facilitator API

Three routes, the same names x402 facilitators use.

GET /x402/supported

{ "kinds": [{ "scheme": "tab-vault", "network": "eip155:4663" }] }

POST /x402/verify

Body: { paymentPayload, paymentRequirements }, the decoded header and the accepts entry the seller sent. The facilitator checks, in order:

{ "isValid": true, "payer": "0xOwner" }
{ "isValid": false, "invalidReason": "OverDailyCap" }

POST /x402/settle

Same body. The facilitator runs verify again, then submits pay() to the vault and waits for the receipt.

{ "success": true, "txHash": "0x…", "networkId": "eip155:4663", "payer": "0xOwner" }
{ "success": false, "errorReason": "NonceUsed" }

Verify is a promise the facilitator makes with its own reputation. Settle is the chain's answer. A seller that serves before settling takes a small credit risk on the facilitator for the time in between, which is why the middleware settles first by default.

Signing

Payments are EIP-712 typed data. The domain is the vault.

domain = { name: "TabVault", version: "1", chainId: 4663, verifyingContract: vault }

Pay(
  address owner,
  address agent,
  address token,
  address to,
  uint256 amount,
  bytes32 nonce,
  uint256 deadline
)

The signer must be the agent address in the message. A signature over a different amount, payee or owner fails inside the contract, not only at the facilitator.

Contract

One contract, TabVault, holds every owner's balances and policies.

FunctionWhoWhat
deposit(token, amount)ownerPulls tokens with transferFrom and credits the owner's balance.
withdraw(token, amount)ownerSends tokens back to the owner.
setAgent(agent, token, maxPerPayment, dailyCap, expiry, restrictPayees)ownerCreates or replaces a key's policy. The daily window survives a replacement.
setPayees(agent, list, allowed)ownerAdds or removes addresses the key may pay when restrictPayees is set.
revokeAgent(agent)ownerDeactivates the key.
pay(owner, agent, token, to, amount, nonce, deadline, signature)anyoneSettles a signed payment. Charges the fee on top from the owner's balance. Sends amount to the payee.
available(owner, agent)viewThe most the key can pay right now: the smaller of the per-payment limit, what is left of today's cap, and the balance after fee.

Reverts are named so a facilitator can pass them through: Expired, NotActive, WrongToken, OverMaxPerPayment, OverDailyCap, PayeeNotAllowed, NonceUsed, BadSignature, InsufficientBalance.

The daily window opens at the first payment and lasts 24 hours. The cap counts amounts, not fees. The admin can set a fee up to 500 basis points and collects it as an internal balance, withdrawn like any owner.

SDK

One module, sdk/index.js, imported as tab. It depends on viem for signing and address checks and on nothing else.

ExportWhat it does
createPayFetch({ agentKey, owner, maxAmount?, chainId?, fetch? })Returns a fetch that answers a 402 by signing and retrying once. Refuses offers above maxAmount or on another chain. The response carries paid and payment.
createPaywall({ facilitator, payTo, asset, vault, chainId, price, settle?, description?, maxTimeoutSeconds? })Returns check, middleware and requirements. price is a base-unit string or a function of the request. settle is before by default.
paywall.check({ header, resource, req })The framework-free core. Returns a 402 description, or { ok, payment, settle() } once the facilitator has verified.
paywall.middleware(handler)Express-style wrapper around check. Sets req.payment and the X-PAYMENT-RESPONSE header.
signPayment({ agentKey, owner, chainId, vault, token, to, amount, deadline, nonce? })Builds and signs one payment. Generates a random nonce when none is given.
encodePayment(obj), decodePayment(header)Base64 JSON in both directions. decodePayment returns null on anything malformed.
validPayload(payment)Structural check of a decoded header. Returns a reason string or null.
VAULT_ABI, ERC20_ABI, PAY_TYPES, vaultDomain(chainId, vault)The pieces a custom client or facilitator needs to talk to the contract directly.

Running a facilitator

A facilitator is a small HTTP service with an RPC connection, the vault address, and a key that holds enough ETH for gas. It does not hold anyone's tokens. What it must get right:

The reference facilitator is server/facilitator.js. It runs inside the site server when VAULT_ADDRESS and FACILITATOR_PRIVATE_KEY are set, keeps its ledger in SQLite, and exposes the three routes above plus /api/ledger?owner= and /api/facilitator. A public instance runs at https://tab-sb5f.onrender.com/x402.

Trust model