Documentation Index
Fetch the complete documentation index at: https://eco.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
Three ways to fund a Gateway deposit address. All three produce the same downstream result: once USDC lands on the deposit address, the contract publishes the Routes intents and solvers fulfill on Polygon.
| Method | User txs | User needs gas? | When to use |
|---|
ERC-3009 transferWithAuthorization | 0 | No | Recommended — single-tx gasless (USDC-native) |
| EIP-2612 Permit | 0 | No | Fallback for tokens that support permit() but not ERC-3009 |
| Direct transfer | 1 | Yes | Sender already has source-chain gas |
Under the gasless flows the user pays nothing on-chain: the deposit address service pays gas for the source-chain transaction, and the Eco solver service pays gas for final fulfillment on the destination chain.
For both gasless methods, Eco’s operator wallet submits the on-chain transactions. Requests are validated, then queued through RabbitMQ (prefetch=1) to serialize submissions and prevent operator-wallet nonce collisions under concurrent load.
Prerequisite for all three: call POST /api/v1/depositAddresses/gateway/polygon once to register the deposit address. The address is deterministic (CREATE2) and can be shared before it’s deployed on-chain — first deposit deploys it.
Reference UI that implements the signing and polling end-to-end: gateway.eco.com.
ERC-3009 transferWithAuthorization
Recommended. USDC’s native gasless path. User signs a TransferWithAuthorization off-chain; the operator wallet submits a single transferWithAuthorization() call that moves tokens directly from the signer to the deposit address.
USDC’s EIP-712 domain name varies per chain — "USDC" on Base Sepolia, "USD Coin" elsewhere. Use the correct value for the chain you’re signing on.
import { parseUnits, toHex } from 'viem'
const value = parseUnits('100', 6)
const nonce = toHex(crypto.getRandomValues(new Uint8Array(32)))
const validAfter = '0'
const validBefore = String(Math.floor(Date.now() / 1000) + 3600)
const signature = await signTypedDataAsync({
domain: { name: 'USD Coin', version: '2', chainId, verifyingContract: USDC_ADDRESS },
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: { from: owner, to: evmDepositAddress, value, validAfter: BigInt(validAfter), validBefore: BigInt(validBefore), nonce },
})
const { data } = await fetch(`${SERVICE_URL}/api/v1/gasless/transferWithAuthorization`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chainId,
from: owner,
to: evmDepositAddress,
value: value.toString(),
validAfter,
validBefore,
nonce,
signature,
}),
}).then((r) => r.json())
// data.id → poll GET /api/v1/gasless/jobs/{id}
Status transitions PENDING → COMPLETED (or FAILED).
EIP-2612 Permit
Fallback when the token supports permit() but not ERC-3009. User signs a Permit off-chain; the operator wallet submits two txs on their behalf:
permit(owner, spender = depositAddress, value, deadline, v, r, s) on the token
createIntentWithApproval(funder = owner) on the deposit contract, which atomically transferFroms the tokens and publishes the intent(s)
import { parseUnits } from 'viem'
const value = parseUnits('100', 6)
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600)
const signature = await signTypedDataAsync({
domain: { name: 'USD Coin', version: '2', chainId, verifyingContract: USDC_ADDRESS },
types: {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'Permit',
message: { owner, spender: evmDepositAddress, value, nonce, deadline },
})
const { data } = await fetch(`${SERVICE_URL}/api/v1/gasless/permit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chainId,
owner,
depositAddress: evmDepositAddress,
value: value.toString(),
deadline: deadline.toString(),
signature,
}),
}).then((r) => r.json())
// data.id → poll GET /api/v1/gasless/jobs/{id}
Response is 202 Accepted with { id, status: "PENDING" }. Status transitions PENDING → PERMIT_SENT → COMPLETED (or FAILED).
Direct transfer
The sender submits a vanilla ERC-20 transfer() to the deposit address. Balance monitoring detects the deposit and kicks off deployment (first time) and intent creation.
import { createWalletClient, http, parseUnits, erc20Abi } from 'viem'
import { base } from 'viem/chains'
const walletClient = createWalletClient({ chain: base, transport: http() })
await walletClient.writeContract({
address: USDC_ADDRESS,
abi: erc20Abi,
functionName: 'transfer',
args: [evmDepositAddress, parseUnits('100', 6)],
})
No signature, no API call beyond the initial POST to obtain the address. The sender pays gas.
Polling the job
const { data } = await fetch(`${SERVICE_URL}/api/v1/gasless/jobs/${jobId}`).then((r) => r.json())
// data: { id, status, permitTxHash?, transferTxHash?, intentHash?, amount?, error? }
Typical client: poll every few seconds until status is COMPLETED or FAILED, with a 1–2 minute overall timeout.
Validation
The service rejects requests before queueing if any of:
- Deposit address isn’t registered on
chainId
- USDC isn’t configured for
chainId
- Permit
deadline / ERC-3009 validBefore is in the past, or validAfter is in the future
value <= 0, or signer’s USDC balance is less than value
- Signature, nonce, or address fields don’t match the expected shapes (see Validation rules)
On failure after queueing, the job record captures the error and moves to FAILED; the transaction is never retried automatically.