# How to Sign a Gasless Stablecoin Payment | CoinCircuit Blog

> Code examples for signing gasless stablecoin payments with EIP-3009 (USDC on Base, Arbitrum), Permit2 (USDC, USDT on Base, Arbitrum, BSC), and Solana partial transaction signing.

Source: https://coincircuit.io/blog/how-to-sign-gasless-payment/

[Back to Blog](https://coincircuit.io/blog/)

Developer Guide

# How to Sign a Gasless Stablecoin Payment

Chidubem Ogbuefi

Mar 21, 2026

Less than 1 min read

*How to Sign a Gasless Stablecoin Payment*

## Three Schemes, One Result

CoinCircuit settles gasless stablecoin payments across EVM blockchains and Solana. The sender signs a payment authorization off-chain. CoinCircuit submits it on-chain and covers gas.

This guide shows how to build the signed payload for each scheme. You need ethers.js v6 for EVM chains or @solana/web3.js for Solana.

## EIP-3009: USDC on EVM Chains

USDC implements `transferWithAuthorization` (EIP-3009). You sign an EIP-712 typed data message authorizing a token transfer from your wallet. Anyone can submit the signed message on-chain. The signer pays zero gas.

```
import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const usdcAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

// Query the contract for EIP-712 domain fields
const usdc = new ethers.Contract(usdcAddress, [
  'function name() view returns (string)',
  'function version() view returns (string)',
], provider);

const domain = {
  name: await usdc.name(),       // "USD Coin" on Base mainnet
  version: await usdc.version(), // "2"
  chainId: 8453,
  verifyingContract: usdcAddress,
};

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' },
  ],
};

const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY');
const message = {
  from: wallet.address,
  to: '0xDepositAddress...',
  value: '1000000',                                        // 1.00 USDC
  validAfter: '0',
  validBefore: String(Math.floor(Date.now() / 1000) + 3600),
  nonce: ethers.hexlify(ethers.randomBytes(32)),
};

const signature = await wallet.signTypedData(domain, types, message);
```

Two things to get right:

The `domain` must match what the USDC contract expects. Always query `name()` and `version()` from the contract itself. Base mainnet returns "USD Coin" / "2". Base Sepolia returns "USDC" / "2". Arbitrum returns "USD Coin" / "2". A mismatched domain produces an invalid signature that reverts on-chain.

The `nonce` is a random 32-byte hex value. The USDC contract tracks used nonces per sender address. Each value can only be used once.

## Permit2: USDC, USDT, and Other ERC-20 Tokens

USDC, USDT, and tokens without built-in gasless support use Uniswap's Permit2. Two steps: a one-time on-chain approval, then off-chain signing for each payment.

**One-time setup:** approve the Permit2 contract to spend your tokens.

```
const usdt = new ethers.Contract(usdtAddress, [
  'function approve(address spender, uint256 amount)',
], signer);

await usdt.approve(
  '0x000000000022D473030F116dDEE9F6B43aC78BA3', // Permit2
  ethers.MaxUint256,
);
```

**Per-payment signing:**

```
import { ethers } from 'ethers';

const PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3';
const usdtAddress = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'; // USDT on Arbitrum

const domain = {
  name: 'Permit2',
  chainId: 42161,
  verifyingContract: PERMIT2,
};

const types = {
  PermitTransferFrom: [
    { name: 'permitted', type: 'TokenPermissions' },
    { name: 'spender', type: 'address' },
    { name: 'nonce', type: 'uint256' },
    { name: 'deadline', type: 'uint256' },
  ],
  TokenPermissions: [
    { name: 'token', type: 'address' },
    { name: 'amount', type: 'uint256' },
  ],
};

const wallet = new ethers.Wallet('YOUR_PRIVATE_KEY');
const deadline = String(Math.floor(Date.now() / 1000) + 3600);
const nonce = String(Math.floor(Math.random() * 2 ** 48));

const message = {
  permitted: { token: usdtAddress, amount: '1000000' },
  spender: ethers.ZeroAddress,    // CoinCircuit fills this at settlement
  nonce: BigInt(nonce),
  deadline: BigInt(deadline),
};

const signature = await wallet.signTypedData(domain, types, message);
```

The Permit2 contract deploys at the same address on every EVM blockchain: `0x000000000022D473030F116dDEE9F6B43aC78BA3`.

The `spender` in the signed message must match whoever calls `permitTransferFrom` on-chain. CoinCircuit resolves this at settlement time. Set it to `ethers.ZeroAddress` when building payloads.

## Solana: Native Fee Payer Abstraction

Solana transactions support separate fee payers at the protocol level. You build a transaction, set CoinCircuit's wallet as the fee payer, sign your part, and send the partially signed transaction. CoinCircuit adds its signature and submits.

```
import {
  Connection, Transaction, SystemProgram,
  PublicKey, Keypair,
} from '@solana/web3.js';

const connection = new Connection(
  'https://api.mainnet-beta.solana.com', 'confirmed',
);
const sender = Keypair.fromSecretKey(yourSecretKeyBytes);
const recipient = new PublicKey('RecipientAddress...');
const feePayer = new PublicKey('CoinCircuitFeePayerKey...');

const { blockhash } = await connection.getLatestBlockhash('confirmed');
const tx = new Transaction({ recentBlockhash: blockhash, feePayer });

tx.add(SystemProgram.transfer({
  fromPubkey: sender.publicKey,
  toPubkey: recipient,
  lamports: 11771200, // 0.0117712 SOL
}));

// Partially sign with your key only
tx.partialSign(sender);

const transaction = tx
  .serialize({ requireAllSignatures: false })
  .toString('base64');
```

For SPL token transfers (USDC, USDT on Solana), replace `SystemProgram.transfer` with the SPL Token `createTransferInstruction` from `@solana/spl-token`.

Solana transactions expire after roughly 60 seconds based on the blockhash. Generate the transaction close to when you plan to submit it.

## Building the Settle Payload

Each scheme produces a flat payload with `from`, `to`, `value`, and scheme-specific fields. Wrap it in the settle request body.

**EIP-3009 (USDC on Base):**

```
{
  "scheme": "eip3009",
  "chain": "base",
  "asset": "USDC",
  "payload": {
    "from": "0xYourWallet...",
    "to": "0xDepositAddress...",
    "value": "1000000",
    "validAfter": "0",
    "validBefore": "1774055094",
    "nonce": "0xrandom32bytes...",
    "signature": "0xYourSignature..."
  }
}
```

**Permit2 (USDT on Arbitrum):**

```
{
  "scheme": "permit2",
  "chain": "arbitrum",
  "asset": "USDT",
  "payload": {
    "from": "0xYourWallet...",
    "to": "0xDepositAddress...",
    "value": "1000000",
    "deadline": "1774055094",
    "nonce": "42",
    "signature": "0xYourSignature..."
  }
}
```

**Solana (SOL):**

```
{
  "scheme": "solana",
  "chain": "solana",
  "asset": "SOL",
  "payload": {
    "from": "YourSolanaPublicKey...",
    "to": "RecipientPublicKey...",
    "value": "11771200",
    "transaction": "base64EncodedPartiallySignedTx..."
  }
}
```

POST this to `https://api.coincircuit.io/api/v1/payments/agent/settle` with your API key. CoinCircuit validates the payload, submits it on-chain, covers gas, and returns the transaction hash.

## Quick Reference

Scheme

Assets

Chains

Signing Standard

Key Fields

eip3009

USDC

Base, Arbitrum

EIP-712

validAfter, validBefore, nonce (32-byte hex), signature

permit2

USDC, USDT

Base, Arbitrum, BSC

deadline, nonce (numeric), signature

solana

SOL, USDC, USDT

Solana

Partial tx signing

transaction (base64)

For the full resource server integration flow, see [x402: How to Accept Payments from AI Agents on Your API](https://coincircuit.io/blog/x402-machine-to-machine-payments/).

### Start accepting crypto today

Developer friendly API. Instant settlements. No hidden fees.

[Get Started Now](https://coincircuit.io/docs/)
