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

# Process x402 agent payments

> Sign ERC-3009 pull-payment authorizations for AI agents using a DFNS wallet, and settle gasless USDC payments from the merchant's DFNS wallet.

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.

<Card title="Get the code" icon="github" href="https://github.com/dfns/dfns-solutions/tree/m/x402-ai-payments">
  dfns/dfns-solutions: x402-ai-payments
</Card>

## 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

```mermaid theme={null}
sequenceDiagram
    participant Agent as AI Agent
    participant Merchant
    participant Signer as DFNS (Signer)
    participant Facilitator as DFNS (Facilitator)
    participant Chain as Blockchain

    Agent->>Merchant: GET /item
    Merchant-->>Agent: 402 + PaymentRequirement

    Agent->>Signer: signPayment(requirement)
    Signer->>Signer: Evaluate policy (cap, asset, recipient)
    Signer->>Signer: wallets.generateSignature (EIP-712)
    Signer-->>Agent: ERC-3009 signature

    Agent->>Merchant: GET /item + X-PAYMENT header
    Merchant->>Merchant: ethers.verifyTypedData
    Merchant->>Facilitator: settle(signed payload)
    Facilitator->>Chain: wallets.broadcastTransaction (receiveWithAuthorization)
    Chain-->>Facilitator: txHash
    Merchant-->>Agent: 200 OK + txHash
```

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

| Component  | Technology                                                         |
| ---------- | ------------------------------------------------------------------ |
| Signing    | DFNS KMS via `@dfns/sdk` (EIP-712)                                 |
| Settlement | DFNS `wallets.broadcastTransaction` from the merchant wallet       |
| Standard   | ERC-3009 `receiveWithAuthorization` on Circle USDC (`FiatTokenV2`) |
| Scripts    | TypeScript via `ts-node`, `ethers` v6                              |
| Network    | Ethereum Sepolia (Chain ID `11155111`)                             |

## What you'll need

* A [DFNS account](https://app.dfns.io)
* A [service account](/guides/developers/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](https://faucet.circle.com/))
  * A **merchant** wallet funded with testnet ETH for gas
* Node.js v22+

## Configuration

<Steps>
  <Step title="Clone and install">
    ```bash theme={null}
    git clone https://github.com/dfns/dfns-solutions.git
    cd dfns-solutions/x402-ai-payments
    npm install
    ```
  </Step>

  <Step title="Set up environment variables">
    ```bash theme={null}
    cp .env.example .env
    ```

    ```bash .env theme={null}
    # 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
    ```

    | Variable                  | Description                                                                    |
    | ------------------------- | ------------------------------------------------------------------------------ |
    | `DFNS_API_URL`            | DFNS API base URL (`api.dfns.io`)                                              |
    | `DFNS_ORG_ID`             | Your DFNS organization ID. See [how to find it](/guides/find-organization-id). |
    | `DFNS_AUTH_TOKEN`         | Service account auth token                                                     |
    | `DFNS_CRED_ID`            | Service account credential ID                                                  |
    | `DFNS_PRIVATE_KEY`        | Service account private key (PEM)                                              |
    | `DFNS_CUSTOMER_WALLET_ID` | Payer wallet. Signs the EIP-712 authorization.                                 |
    | `DFNS_MERCHANT_WALLET_ID` | Payee wallet. Broadcasts the settlement.                                       |
    | `MERCHANT_ADDRESS`        | On-chain address of the merchant wallet (must match `DFNS_MERCHANT_WALLET_ID`) |
    | `USDC_CONTRACT_ADDRESS`   | USDC contract on the target chain                                              |
    | `CHAIN_ID`                | Numeric chain ID (`11155111` for Sepolia)                                      |

    To find your wallet IDs:

    ```bash theme={null}
    npm run wallets:list
    ```
  </Step>
</Steps>

## Run the demo

```bash theme={null}
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](https://sepolia.etherscan.io) to confirm settlement.

## Two DFNS wallets, two roles

| Role                               | Wallet                    | Responsibility                                                                                              |
| ---------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Payer** (customer)               | `DFNS_CUSTOMER_WALLET_ID` | Holds USDC. Signs the EIP-712 authorization via `wallets.generateSignature`. Never broadcasts.              |
| **Payee + Facilitator** (merchant) | `DFNS_MERCHANT_WALLET_ID` | Verifies 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

```ts theme={null}
// 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](/core-concepts/policies) 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

```ts theme={null}
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.

## Related

<CardGroup cols={2}>
  <Card title="Define treasury policies" icon="shield-check" href="/solutions/define-treasury-policies">
    Spending limits and approval quorums
  </Card>

  <Card title="Build programmable approval policies" icon="code" href="/solutions/build-programmable-approval-policies">
    Service accounts that decode call data
  </Card>

  <Card title="Automate payments" icon="money-bill-transfer" href="/solutions/automate-payments">
    Policy-gated outbound transfers
  </Card>

  <Card title="Wallets API" icon="wallet" href="/api-reference/wallets">
    `generateSignature` and `broadcastTransaction` reference
  </Card>
</CardGroup>
