> ## Documentation Index
> Fetch the complete documentation index at: https://developers.circle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How-to: Sign EIP-3009 payment authorizations

> Manually construct and sign EIP-3009 TransferWithAuthorization messages for Circle Gateway batched payments

## Overview

* Construct and sign EIP-3009 `TransferWithAuthorization` messages that
  authorize Circle Gateway to transfer USDC from your Gateway balance.
* Use this when integrating payment signing into a custom workflow, building a
  non-JavaScript client, or understanding the signing mechanism that the SDK
  handles automatically.

## Prerequisites

Before you begin, ensure you have:

* An EVM wallet with a private key for signing.
* Deposited USDC in a Gateway Wallet contract (see the
  [buyer quickstart](/gateway/nanopayments/quickstarts/buyer)).
* Familiarity with [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data
  signing.
* Installed the [viem](https://viem.sh/) library (`npm install viem`).

## Steps

### Step 1. Construct the EIP-712 domain

Gateway uses a custom EIP-712 domain named `GatewayWalletBatched`. This is
specific to Gateway's batching feature and is not the standard USDC domain.

```ts sign.ts theme={null}
const domain = {
  name: "GatewayWalletBatched",
  version: "1",
  chainId: 5042002, // Arc Testnet — replace with your target chain's EVM chain ID
  verifyingContract: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9", // GatewayWallet on Arc Testnet
};
```

The `verifyingContract` is the GatewayWallet contract address for the blockchain
you are transacting on. Find the address for your target chain in the
[EVM contract addresses](/gateway/references/contract-addresses) reference. You
can also retrieve it programmatically using `getVerifyingContract()` from the
SDK or from the `402` response's `accepts` array (in the
`extra.verifyingContract` field).

<Warning>
  The `chainId` must be the standard EVM chain ID for your target network (for
  example, `5042002` for Arc Testnet), not the Gateway domain identifier. Using
  the wrong chain ID causes the signature to fail silently.
</Warning>

### Step 2. Define the typed data

The `TransferWithAuthorization` type follows the
[EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) specification:

```ts sign.ts theme={null}
const types = {
  TransferWithAuthorization: [
    { name: "from", type: "address" },
    { name: "to", type: "address" },
    { name: "value", type: "uint256" },
    { name: "validAfter", type: "uint256" },
    { name: "validBefore", type: "uint256" },
    { name: "nonce", type: "bytes32" },
  ],
};
```

Populate the message fields.

USDC uses 6 decimal places: `$1.00` = `1000000`, `$0.01` = `10000`, `$0.001` =
`1000`. Always convert dollar amounts to base units before signing.

```ts sign.ts theme={null}
import { randomBytes } from "crypto";

const message = {
  from: "0xYOUR_ADDRESS", // Your wallet address (the payer)
  to: "0xSELLER_ADDRESS", // The seller's wallet address
  value: 10000n, // Amount in USDC base units (0.01 USDC = 10000)
  validAfter: 0n, // Signature is valid immediately
  validBefore: BigInt(Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 5), // Valid for 5 days
  nonce: `0x${randomBytes(32).toString("hex")}`, // Unique random nonce
};
```

<Warning>
  The `validBefore` timestamp must be at least 3 days in the future. Gateway
  rejects signatures with shorter validity periods to ensure there is enough time
  to include them in a settlement batch.
</Warning>

### Step 3. Sign the typed data

Use the `viem` library's `signTypedData` to produce the EIP-712 signature:

```ts sign.ts theme={null}
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

const signature = await account.signTypedData({
  domain,
  types,
  primaryType: "TransferWithAuthorization",
  message,
});

console.log("Signature:", signature);
```

### Step 4. Assemble and send the payment payload

Encode the payment payload as base64 JSON and attach it to your HTTP request in
the `Payment-Signature` header. The server-side facilitator settles this payment
through the [Settle x402 Payment](/api-reference/gateway/all/settle-x402payment)
API endpoint:

```ts sign.ts theme={null}
const paymentPayload = {
  x402Version: 2,
  payload: {
    authorization: {
      from: message.from,
      to: message.to,
      value: message.value.toString(),
      validAfter: message.validAfter.toString(),
      validBefore: message.validBefore.toString(),
      nonce: message.nonce,
    },
    signature,
  },
  resource: "...", // from 402 response
  accepted: {}, // the payment option chosen
};

const encoded = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");

const response = await fetch("http://localhost:3000/premium-data", {
  headers: {
    "Payment-Signature": encoded,
  },
});

console.log("Status:", response.status);
console.log("Body:", await response.json());
```

### Step 5. Use `BatchEvmScheme` for the x402 protocol (alternative)

If you are integrating with an existing x402 client (such as `@x402/core`), use
the `BatchEvmScheme` class instead of constructing the payload manually. It
handles domain construction, nonce generation, and payload encoding:

```ts sign-x402.ts theme={null}
import { BatchEvmScheme } from "@circle-fin/x402-batching/client";

const batchScheme = new BatchEvmScheme({
  address: account.address,
  signTypedData: async (params) => account.signTypedData(params),
});

// Get requirements from a 402 response
const res = await fetch(url);
const header = res.headers.get("PAYMENT-REQUIRED");
const { accepts } = JSON.parse(Buffer.from(header, "base64").toString());
const gatewayOption = accepts.find(
  (opt) => opt.extra?.name === "GatewayWalletBatched",
);

// Create the payment payload
const payload = await batchScheme.createPaymentPayload(2, gatewayOption);

// Retry with the payload
const finalResponse = await fetch(url, {
  headers: {
    "Payment-Signature": Buffer.from(
      JSON.stringify({ ...payload, accepted: gatewayOption }),
    ).toString("base64"),
  },
});
```

## Troubleshoot `invalid_signature`

Gateway returns `invalid_signature` for any EIP-712 domain or field mismatch
without specifying which field is wrong. If your signature is rejected, check
every item in this list:

* **Domain name** must be exactly `"GatewayWalletBatched"`. Common mistakes
  include `"GatewayWallet"`, `"Gateway"`, and `"USDC"`.
* **`verifyingContract`** must be the GatewayWallet contract address, not the
  USDC token address or GatewayMinter. See
  [EVM contract addresses](/gateway/references/contract-addresses) for the
  correct address on each chain.
* **`chainId`** must be the standard EVM chain ID (for example, `5042002` for
  Arc Testnet). Do not use the Gateway domain identifier.
* **`nonce`** must be a unique random 32-byte value for every payment. Reusing a
  nonce causes the same `invalid_signature` error.
* **`validBefore`** must be at least 3 days in the future. Shorter validity
  periods are rejected with `authorization_validity_too_short`.
* **`value`** must be in USDC base units (6 decimals). Passing a dollar amount
  instead of base units causes an amount mismatch.
* **`from`** must match the address derived from the private key that signed the
  message.

For the full list of error codes, see the
[error reference](/gateway/nanopayments/references/sdk#gateway-api-error-codes).

<Note>
  The EIP-712 domain for payment authorizations (`GatewayWalletBatched`) is
  different from the domain used for Gateway withdrawal and crosschain transfer
  operations (`GatewayWallet`). If you are building manual signing for both
  payments and withdrawals, use the correct domain for each operation. The SDK's
  `client.withdraw()` and `client.pay()` methods handle this automatically.
</Note>
