Skip to main content

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.

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).
  • Familiarity with EIP-712 typed data signing.
  • Installed the viem 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.
sign.ts
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 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).
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.

Step 2. Define the typed data

The TransferWithAuthorization type follows the EIP-3009 specification:
sign.ts
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.
sign.ts
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
};
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.

Step 3. Sign the typed data

Use the viem library’s signTypedData to produce the EIP-712 signature:
sign.ts
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 endpoint:
sign.ts
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:
sign-x402.ts
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 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.
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.