Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dfns.co/llms.txt

Use this file to discover all available pages before exploring further.

An AI agent calls a paywalled API. The merchant responds with HTTP 402 Payment Required and a payment requirement (amount, asset, recipient, nonce). The agent forwards the requirement to a Dfns-powered signer, which validates it against your policy and produces an EIP-712 signature for ERC-3009 ReceiveWithAuthorization. The agent retries the call with the signature in an X-PAYMENT header. The merchant verifies the signature and broadcasts settlement from its own Dfns wallet, paying the gas itself. The X402 protocol revives HTTP 402 as a wire format for agent ↔ merchant payments. Dfns provides the two wallets the flow needs: a payer wallet that signs the authorization without ever exposing the key, and a merchant wallet that broadcasts the settlement. Policies run before signing, so agents cannot exceed the spending limits you set.

Get the code

dfns/dfns-solutions: x402-ai-payments

When to use this

  • Agent-driven micropayments: AI agents that pay per API call, per article, per inference
  • Gasless UX for agents: agents hold only the asset they spend (USDC), not native gas
  • Policy-gated autonomy: enforce per-payment caps, recipient allowlists, and asset whitelists before any signature is produced

How it works

The two Dfns roles share the same API client in the reference implementation: only the walletId differs between the generateSignature call (payer wallet) and the broadcastTransaction call (merchant wallet). In production, the Signer typically runs on the platform that hosts the agent, while the Facilitator runs on the merchant.

Reference implementation

ComponentTechnology
SigningDfns KMS via @dfns/sdk (EIP-712)
SettlementDfns wallets.broadcastTransaction from the merchant wallet
StandardERC-3009 receiveWithAuthorization on Circle USDC (FiatTokenV2)
ScriptsTypeScript via ts-node, ethers v6
NetworkEthereum Sepolia (Chain ID 11155111)

What you’ll need

  • A Dfns account
  • A service account with these permissions:
    • Wallets:GenerateSignature on the customer (payer) wallet
    • Wallets:BroadcastTransaction on the merchant wallet
    • Wallets:Read on both
  • Two Dfns wallets on Ethereum Sepolia:
    • A customer wallet funded with testnet USDC (Circle faucet)
    • A merchant wallet funded with testnet ETH for gas
  • Node.js v22+

Configuration

1

Clone and install

git clone https://github.com/dfns/dfns-solutions.git
cd dfns-solutions/x402-ai-payments
npm install
2

Set up environment variables

cp .env.example .env
.env
# Dfns API service account
DFNS_API_URL=https://api.dfns.io
DFNS_ORG_ID=or-xxx-xxx
DFNS_AUTH_TOKEN=eyJ...
DFNS_CRED_ID=cred-xxx
DFNS_PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----"

# Wallets
DFNS_CUSTOMER_WALLET_ID=wa-xxx-xxx
DFNS_MERCHANT_WALLET_ID=wa-yyy-yyy

# On-chain
MERCHANT_ADDRESS=0x...
USDC_CONTRACT_ADDRESS=0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
CHAIN_ID=11155111
VariableDescription
DFNS_API_URLDfns API base URL (api.dfns.io)
DFNS_ORG_IDYour Dfns organization ID. See how to find it.
DFNS_AUTH_TOKENService account auth token
DFNS_CRED_IDService account credential ID
DFNS_PRIVATE_KEYService account private key (PEM)
DFNS_CUSTOMER_WALLET_IDPayer wallet. Signs the EIP-712 authorization.
DFNS_MERCHANT_WALLET_IDPayee wallet. Broadcasts the settlement.
MERCHANT_ADDRESSOn-chain address of the merchant wallet (must match DFNS_MERCHANT_WALLET_ID)
USDC_CONTRACT_ADDRESSUSDC contract on the target chain
CHAIN_IDNumeric chain ID (11155111 for Sepolia)
To find your wallet IDs:
npm run wallets:list

Run the demo

npm start
You’ll see the console walk through each step:
  1. The agent hits the merchant and receives 402 with a PaymentRequirement
  2. The signer validates the requirement against its policy cap, then calls wallets.generateSignature with kind: 'Eip712'
  3. The agent retries with the signature in an X-PAYMENT header
  4. The merchant verifies the signature locally with ethers.verifyTypedData
  5. The facilitator encodes receiveWithAuthorization and calls wallets.broadcastTransaction from the merchant wallet
  6. The transaction hash is printed. Paste it into sepolia.etherscan.io to confirm settlement.

Two Dfns wallets, two roles

RoleWalletResponsibility
Payer (customer)DFNS_CUSTOMER_WALLET_IDHolds USDC. Signs the EIP-712 authorization via wallets.generateSignature. Never broadcasts.
Payee + Facilitator (merchant)DFNS_MERCHANT_WALLET_IDVerifies the signature, broadcasts receiveWithAuthorization via wallets.broadcastTransaction, pays gas.
The merchant wallet’s on-chain address must equal MERCHANT_ADDRESS. USDC enforces msg.sender == payee inside receiveWithAuthorization, so only the merchant can settle.

How the signer works

// Per-payment cap. Pair with a Dfns Wallets:Sign policy for server-side enforcement.
const MAX_PAYMENT_AMOUNT = 5_000_000n; // 5.00 USDC

async signPayment(req: PaymentRequirement): Promise<PaymentSignature> {
  if (BigInt(req.amount) > MAX_PAYMENT_AMOUNT) {
    throw new Error(`Payment exceeds per-payment cap of ${MAX_PAYMENT_AMOUNT}`);
  }

  const message = {
    from: ethers.getAddress(this.customerWalletAddress),
    to: ethers.getAddress(req.recipient),
    value: req.amount,
    validAfter: req.validAfter,
    validBefore: req.validBefore,
    nonce: req.nonce,
  };

  let sigResult = await this.dfnsApi.wallets.generateSignature({
    walletId: this.customerWalletId,
    body: {
      kind: 'Eip712',
      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' },
        ],
      },
      domain: {
        name: 'USDC',
        version: '2',
        chainId: req.chainId,
        verifyingContract: ethers.getAddress(req.contract),
      },
      message,
    },
  });

  while (sigResult.status !== 'Signed') {
    if (sigResult.status === 'Failed' || sigResult.status === 'Rejected') {
      throw new Error(`Signature ${sigResult.status}: ${sigResult.reason}`);
    }
    await new Promise((r) => setTimeout(r, 2000));
    sigResult = await this.dfnsApi.wallets.getSignature({
      walletId: this.customerWalletId,
      signatureId: sigResult.id,
    });
  }

  const signedData = sigResult.signedData ?? sigResult.signature?.encoded;
  return { protocol: 'EIP-3009', signature: signedData!, message };
}
The in-process cap is your first line of defense. For production, layer a Dfns policy on Wallets:Sign covering the customer wallet. It runs server-side at Dfns and cannot be bypassed even if the signer service is compromised.

How the facilitator works

async settlePayment(contractAddress: string, payment: PaymentSignature): Promise<string> {
  const selector = '0xef55bec6'; // receiveWithAuthorization(...)
  const sig = ethers.Signature.from(payment.signature);

  const params = ethers.AbiCoder.defaultAbiCoder().encode(
    ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes32', 'uint8', 'bytes32', 'bytes32'],
    [
      payment.message.from,
      payment.message.to,
      payment.message.value,
      payment.message.validAfter,
      payment.message.validBefore,
      payment.message.nonce,
      sig.v, sig.r, sig.s,
    ],
  );

  const result = await this.dfnsApi.wallets.broadcastTransaction({
    walletId: this.merchantWalletId,
    body: {
      kind: 'Eip1559',
      to: contractAddress,
      data: selector + params.slice(2),
      value: '0',
      gasLimit: '200000',
      maxFeePerGas: '5000000000',
      maxPriorityFeePerGas: '1000000000',
    },
  });

  return result.txHash!;
}
The merchant wallet is the only entity authorized to call receiveWithAuthorization. USDC’s contract enforces msg.sender == payee. This is the security model that makes pull payments safe: a signed authorization is worthless to anyone other than the named payee.

Design considerations

Enforce policy before signing

A signed authorization is irrevocable until validBefore expires. Validate the requirement (amount, recipient, asset, chain) before calling wallets.generateSignature. The reference implementation enforces a per-payment cap; production should also check recipient allowlists, per-merchant caps, and a time-windowed spend budget.

Layer with a Dfns policy

In-process checks live in the same process as the signer. If that process is compromised, the checks are bypassed. A Dfns Wallets:Sign policy on the customer wallet runs on Dfns infrastructure and cannot be bypassed by your service. Use both: the in-process check is fast and expressive; the Dfns policy is the safety net.

Unique nonces per requirement

ERC-3009 uses the nonce to prevent replay. The merchant must generate a fresh random nonce per 402 response, and the customer’s wallet should refuse to sign two requirements with the same nonce. The reference implementation uses ethers.randomBytes(32).

Pin the verifying contract

A signature for ReceiveWithAuthorization is bound to a verifyingContract in the EIP-712 domain. Treat that field as an allowlist: the signer should refuse to sign for any contract address it doesn’t recognize. Otherwise an attacker who controls the 402 response can redirect signatures to a contract they control.

Treat gas as an operational cost

The merchant pays gas for every settlement. In a high-volume X402 deployment, this is non-trivial: budget it explicitly, monitor it per merchant, and consider passing it through in pricing the same way card networks pass through interchange.

Define treasury policies

Spending limits and approval quorums

Build programmable approval policies

Service accounts that decode call data

Automate payments

Policy-gated outbound transfers

Wallets API

generateSignature and broadcastTransaction reference
Last modified on May 28, 2026