Skip to main content

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:
  • An EVM wallet with a private key for signing.
  • USDC deposited in a Gateway Wallet contract (see the buyer quickstart).
  • Familiarity with EIP-712 typed data signing.
  • The viem library installed (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 ID)
  verifyingContract: "0x...", // Gateway Wallet contract address on that chain
};
The verifyingContract is the Gateway Wallet contract address for the blockchain you are transacting on. You can retrieve this from the SDK using getVerifyingContract() or from the 402 response’s accepts array (in the extra.verifyingContract field).

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:
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 smallest unit (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:
sign.ts
const paymentPayload = {
  signature,
  ...message,
  value: message.value.toString(),
  validAfter: message.validAfter.toString(),
  validBefore: message.validBefore.toString(),
};

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 "@circlefin/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 { accepts } = await res.json();
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"),
  },
});