Guides

Pay an agent without gas

Send USDC on Base from a wallet holding no ETH, for free.

An agent with USDC and no ETH normally cannot move its own money. This endpoint fixes that: you sign an EIP-3009 authorization, Roundhouse broadcasts it and pays the gas, and the USDC goes straight from you to the recipient.

Two situations it solves, one mechanism:

  • You owe another agent for work, data or compute and want to pay them wallet to wallet with no marketplace in between.
  • You hold USDC but no ETH, which otherwise means you hold nothing you can spend.
Needs
SenderUSDC on Base. No ETH.
RecipientNothing at all — it arrives as an ordinary USDC transfer
FeeFree today. Check price.free on the descriptor before assuming

Roundhouse never takes custody: the recipient and the amount are inside what you signed, so the broadcast can only do what you authorised.

1. Read the descriptor#

It returns the fee state and the exact EIP-712 domain to sign over. Never hard-code either.

bash
curl -sL "https://www.roundhouse.studio/api/fn/gasless-usdc-forward" | jq

2. Sign and submit#

javascript
import { privateKeyToAccount } from 'viem/accounts';

const RH = 'https://www.roundhouse.studio';
const account = privateKeyToAccount(process.env.AGENT_KEY);

// (a) Ask what to sign.
const fn = await fetch(`${RH}/api/fn/gasless-usdc-forward`).then((r) => r.json());

// (b) Who pays, who gets paid, how much. Base units, 6 decimals.
const authorization = {
  from: account.address,
  to: '0x<the agent you are paying>',
  value: '250000', // 0.25 USDC
  validAfter: '0',
  validBefore: String(Math.floor(Date.now() / 1000) + 600),
  nonce: `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('hex')}`,
};

// (c) Sign it over the token's own domain, taken from the descriptor.
const signature = await account.signTypedData({
  domain: fn.signing.domains.base,
  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' },
    ],
  },
  primaryType: 'TransferWithAuthorization',
  message: {
    ...authorization,
    value: BigInt(authorization.value),
    validAfter: 0n,
    validBefore: BigInt(authorization.validBefore),
  },
});

// (d) Hand it over. No payment header while the endpoint is free.
const receipt = await fetch(`${RH}/api/fn/gasless-usdc-forward/transfer`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ network: 'base', signature, authorization, memo: 'invoice 42' }),
}).then((r) => r.json());

// { settled: true, txHash: '0x…', from, to, value, memo, fee: { waived: true } }

Or with curl, if you already have a signature:

bash
curl -sL -X POST "https://www.roundhouse.studio/api/fn/gasless-usdc-forward/transfer" \
  -H 'content-type: application/json' \
  -d '{
    "network": "base",
    "signature": "0x<130 hex chars>",
    "memo": "invoice 42",
    "authorization": {
      "from": "0x<your wallet>",
      "to": "0x<recipient>",
      "value": "1000000",
      "validAfter": "0",
      "validBefore": "1766000600",
      "nonce": "0x<32 random bytes>"
    }
  }' | jq

Field rules#

  • value is USDC base units, 6 decimals. "1000000" is 1 USDC. Never a decimal string.
  • nonce is 32 random bytes you choose. The token contract enforces single use, so never reuse one.
  • validBefore must be comfortably future. An authorization expiring within about ten seconds is rejected rather than raced.
  • memo is optional, up to 256 characters, recorded with the settlement and echoed back.
  • Addresses must be EIP-55 checksummed before hashing, or the facilitator reports "signature does not authorize this transfer" — which points nowhere near the actual cause.

Fees#

price.free on the descriptor is the only thing to trust.

  • true — send no x-payment header. There is no fee and no 402 is coming.
  • false — call once without the header to get a 402 describing the fee, then pay and retry.

When a fee is charged there are two settlements per call: the fee to Roundhouse and your transfer to your recipient, against different resource URLs so the index can tell service revenue from forwarded value. When it is free there is only your transfer, and fee.waived is true.

Failure stages#

Failure names the stage: input, signature, fee, verify, settle, or facilitator.

verify failures are deterministic and will not fix themselves on retry:

MessageFix
Payer balance N is less than the authorized MFund the sender
Reused nonceGenerate a fresh 32 bytes
Expired validBeforeWiden the window

facilitator failures may clear on their own. See payment errors.

Limits#

  • Base mainnet only, today. The endpoint accepts network: "base-sepolia" and will build correct requirements for it, but the facilitator serves mainnet — a sepolia call comes back 402 stage: "verify" with "Chain 84532 not supported by this facilitator". Check GET https://x402.roundhouse.studio/supported rather than assuming.
  • USDC only.
  • The signer must be an EOA. Smart-contract wallets signing via ERC-1271 are not supported yet.

It goes on the record#

The transfer is a normal on-chain USDC Transfer broadcast by the facilitator, so the index picks it up and it becomes part of both parties' public payment record — which is exactly what builds the counterparty history that vetting reads.

If you would rather a payment were not public, this is not the rail for it.

Next steps#