Skip to main content
x402Escrow

Architecture Overview

+---------------+       HTTP 402        +---------------+
|    Client /   |<--------------------->| Facilitator / |
|    AI Agent   |    X-PAYMENT header   | Service API   |
+-------+-------+                       +-------+-------+
       |                                        |
       | signs EIP-3009                         | calls settle() 
       | authorization                          | / release()
       |                                        |
       |            +--------------+            |
       +----------->|  x402Escrow  |<-----------+
                    |  (on-chain)  |
                    +------+-------+
                           |
                    +------+-------+
                    |  USDC Token  |
                    |  (EIP-3009)  |
                    +--------------+

Facilitator Integration

The facilitator is the service operator — the side that accepts payment and delivers work.

1. Receive the Client’s Payment Authorization

In the x402 flow, the client sends an HTTP request with an X-PAYMENT header containing the signed EIP-3009 authorization. Parse the header to extract:
interface PaymentAuthorization {
  client: string;       // client wallet address
  maxAmount: bigint;    // max USDC (6 decimals)
  validAfter: bigint;   // unix timestamp
  validBefore: bigint;  // unix timestamp
  nonce: string;        // bytes32
  v: number;
  r: string;
  s: string;
}

2. Lock Funds with settle()

Call settle() to pull USDC from the client into escrow:
import { ethers } from "ethers";

const escrow = new ethers.Contract(ESCROW_ADDRESS, X402_ESCROW_ABI, facilitatorSigner);

const tx = await escrow.settle(
  auth.client,
  auth.maxAmount,
  auth.validAfter,
  auth.validBefore,
  auth.nonce,
  auth.v,
  auth.r,
  auth.s
);

const receipt = await tx.wait();
const depositedLog = receipt.logs.find(
  (log) => log.address.toLowerCase() === ESCROW_ADDRESS.toLowerCase()
);
const escrowId = escrow.interface.parseLog(depositedLog).args.escrowId;
Store escrowId — you’ll need it for release.

3. Execute the Service

Perform the metered work (AI inference, API calls, data processing, etc.) and track the actual cost.

4. Release with Actual Cost

const actualCost = ethers.parseUnits("2.50", 6); // actual USDC cost

const tx = await escrow.release(escrowId, actualCost);
await tx.wait();
// facilitator receives actualCost
// client receives (maxAmount - actualCost) automatically
If the service failed or no cost was incurred, release with zero:
await escrow.release(escrowId, 0); // full refund to client

Error Handling

ScenarioAction
settle() reverts with TransferMismatchThe client’s USDC balance may be insufficient, or a non-standard token is in use.
settle() reverts with TimeoutExpiredAuthorization expired before you submitted it. Ask the client for a fresh signature.
release() reverts with EscrowNotFoundEscrow was already released or refunded (timeout). Check getEscrow() first.
Service crashes mid-executionYou have until refundAt to call release(). Monitor active escrows.

Client Integration

The client is the payer — typically an AI agent or application that consumes a metered service.

1. Sign the EIP-3009 Authorization

Construct and sign a ReceiveWithAuthorization message:
import { ethers } from "ethers";

const domain = {
  name: "USD Coin",              // USDC token name
  version: "2",                  // USDC token version
  chainId: 8453,                 // Base
  verifyingContract: USDC_ADDRESS
};

const types = {
  ReceiveWithAuthorization: [
    { name: "from", type: "address" },
    { name: "to", type: "address" },
    { name: "value", type: "uint256" },
    { name: "validAfter", type: "uint256" },
    { name: "validBefore", type: "uint256" },
    { name: "nonce", type: "bytes32" }
  ]
};

const message = {
  from: clientAddress,
  to: ESCROW_ADDRESS,          // must be the escrow contract
  value: ethers.parseUnits("10.00", 6),  // max USDC to lock
  validAfter: 0,               // valid immediately
  validBefore: Math.floor(Date.now() / 1000) + 300, // expires in 5 min
  nonce: ethers.hexlify(ethers.randomBytes(32))
};

const signature = await clientSigner.signTypedData(domain, types, message);
const { v, r, s } = ethers.Signature.from(signature);
The to field must be the escrow contract address. This prevents anyone else from using your signature.

2. Send with the HTTP Request

Include the authorization in the X-PAYMENT header per the x402 protocol:
const response = await fetch("https://api.example.com/inference", {
  method: "POST",
  headers: {
    "X-PAYMENT": JSON.stringify({
      client: clientAddress,
      maxAmount: message.value.toString(),
      validAfter: message.validAfter.toString(),
      validBefore: message.validBefore.toString(),
      nonce: message.nonce,
      v, r: r, s: s
    }),
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ prompt: "..." })
});

3. Monitor the Escrow (Optional)

Check the escrow status on-chain:
const escrowId = ethers.keccak256(
  ethers.solidityPacked(["address", "bytes32"], [clientAddress, message.nonce])
);

const view = await escrow.getEscrow(escrowId);
console.log({
  amount: view.amount,
  canRefund: view.canRefund,
  timeUntilRefund: view.timeUntilRefund
});

4. Claim Timeout Refund (If Needed)

If the escrow was settled but the facilitator never called release(), claim a refund after the timeout:
if (view.canRefund) {
  await escrow.refundAfterTimeout(escrowId);
  // full amount returned to client
}
This is permissionless — any address can trigger it, and funds always go to the original client.

Escrow ID Computation

The escrow ID is deterministic and can be computed off-chain before the transaction:
escrowId = keccak256(abi.encodePacked(client, nonce))
In ethers.js:
const escrowId = ethers.keccak256(
  ethers.solidityPacked(["address", "bytes32"], [clientAddress, nonce])
);
In Python (web3.py):
from web3 import Web3

escrow_id = Web3.keccak(
    Web3.to_bytes(hexstr=client_address) + nonce_bytes
)

Event Monitoring

Subscribe to escrow events for off-chain tracking:
escrow.on("Deposited", (escrowId, client, amount) => {
  console.log(`Escrow ${escrowId}: ${amount} USDC locked by ${client}`);
});

escrow.on("Released", (escrowId, facilitator, toFacilitator, toClient) => {
  console.log(`Escrow ${escrowId}: ${toFacilitator} to facilitator, ${toClient} refunded`);
});

escrow.on("Refunded", (escrowId, client, amount) => {
  console.log(`Escrow ${escrowId}: ${amount} USDC refunded to ${client}`);
});