> ## 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.

# Quickstart: Gasless Gateway deposits with Eco

> Deposit testnet USDC into Circle Gateway without paying source-chain gas by using Eco's gasless deposit flow.

Use Eco's gasless deposit flow to move 1 testnet USDC from Base Sepolia into
Circle Gateway without holding ETH on the source blockchain. This quickstart
shows a TypeScript script that requests an Eco deposit address, signs an
EIP-3009 authorization, submits the deposit, and checks your Gateway balance on
Polygon PoS Amoy.

<Note>
  Eco is a third-party fast-deposit service. Circle names Eco as an option for
  faster Gateway deposits in current docs, but Circle does not operate or audit
  Eco's service. Review Eco's docs and test the flow before you use it in
  production.
</Note>

This quickstart uses Base Sepolia as the source blockchain and Polygon PoS Amoy
(Gateway domain ID `7`) as the destination. You sign an EIP-3009
`TransferWithAuthorization` payload locally, and Eco submits the transfer
onchain. You still need USDC in your wallet, but you do not need ETH on Base
Sepolia for the deposit. For other deposit methods and Eco-specific routing
details, see the
[Eco Gateway Deposits guide](https://eco.com/docs/getting-started/programmable-addresses/gateway-deposits).

## Prerequisites

Before you begin, ensure you have:

* Installed [Node.js v22+](https://nodejs.org/)
* Obtained a private key for a Base Sepolia EOA
* Funded your wallet with testnet USDC from the
  [Circle Faucet](https://faucet.circle.com/) (select **Base Sepolia**)

**Base Sepolia network details**

| Property      | Value                                           |
| ------------- | ----------------------------------------------- |
| Chain ID      | `84532`                                         |
| USDC contract | `0x036CbD53842c5426634e7929541eC2318f3dCF7e`    |
| Faucet        | [faucet.circle.com](https://faucet.circle.com/) |

<Note>You do not need ETH on Base Sepolia for this gasless deposit flow.</Note>

## Step 1. Set up your project

### 1.1. Create the project and install dependencies

```shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir eco-gasless-deposit
cd eco-gasless-deposit
npm init -y

# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="tsx --env-file=.env eco-deposit.ts"

# Install runtime dependencies
npm install viem

# Install dev dependencies
npm install --save-dev @types/node tsx typescript
```

### 1.2. Configure TypeScript (optional)

<Tip>
  This step is optional. It helps prevent missing types in your IDE or editor.
</Tip>

Create a `tsconfig.json` file:

```shell theme={null}
npx tsc --init
```

Then, update the `tsconfig.json` file:

```shell theme={null}
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF
```

### 1.3. Set environment variables

Open `.env` in your editor and add:

```text theme={null}
PRIVATE_KEY=0xYOUR_BASE_SEPOLIA_PRIVATE_KEY
```

* `PRIVATE_KEY` is the private key for the Base Sepolia EOA that holds the
  testnet USDC you want to deposit.
* Include the `0x` prefix. MetaMask often copies the private key as bare hex,
  but this script expects a hex string with the prefix.

<Tip>
  Open `.env` in your editor rather than writing values with shell commands, and
  add `.env` to your `.gitignore`. This prevents credentials from leaking into
  your shell history or version control.
</Tip>

The `npm run start` command loads variables from `.env` using Node.js native
env-file support.

<Warning>
  This example uses one or more private keys for local testing. In production,
  use a secure key management solution and never expose or share private keys.
</Warning>

<Note>
  Steps 2 through 6 show individual parts of the flow. Each snippet highlights
  one stage and is not independently runnable. Use the full script in [Full
  deposit script](#full-deposit-script) to run the quickstart end to end.
</Note>

## Step 2. Request an Eco deposit address

Request a deterministic Eco deposit address for Gateway on Polygon PoS Amoy.

```typescript theme={null}
const response = await fetch(
  "https://deposit-addresses-preproduction.eco.com/api/v1/depositAddresses/gateway/polygon",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      chainId: 84532, // Base Sepolia
      depositor: yourWalletAddress,
      evmDestinationAddress: yourWalletAddress,
    }),
  },
);

if (!response.ok) {
  throw new Error(`Failed to get deposit address: ${await response.text()}`);
}

const { data } = await response.json();
const depositAddress = data.evmDepositAddress;
```

The returned `depositAddress` is a reusable address for this depositor and
destination combination.

## Step 3. Generate EIP-3009 authorization

Create the authorization parameters. EIP-3009 uses a `TransferWithAuthorization`
signed message to let a third party (Eco) submit the transfer on your behalf.
For a deeper explanation of the signing flow, see
[EIP-3009 signing](/gateway/nanopayments/howtos/eip-3009-signing).

```typescript theme={null}
import { randomBytes } from "node:crypto";

const nonce = `0x${randomBytes(32).toString("hex")}`;
const validAfter = 0n;
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600); // 1 hour
const amount = 1000000n; // 1 USDC (6 decimals)
```

Sign the `TransferWithAuthorization` payload:

```typescript theme={null}
const signature = await walletClient.signTypedData({
  account,
  domain: {
    name: "USDC",
    version: "2",
    chainId: 84532,
    verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  },
  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: yourWalletAddress,
    to: depositAddress,
    value: amount,
    validAfter,
    validBefore,
    nonce,
  },
});
```

<Note>
  For this Base Sepolia flow, the EIP-712 domain name is `USDC`, not `USD Coin`.
</Note>

## Step 4. Submit the gasless deposit to Eco

Submit the signed authorization:

```typescript theme={null}
const response = await fetch(
  "https://deposit-addresses-preproduction.eco.com/api/v1/gasless/transferWithAuthorization",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      chainId: 84532,
      from: yourWalletAddress,
      to: depositAddress,
      value: amount.toString(),
      validAfter: validAfter.toString(),
      validBefore: validBefore.toString(),
      nonce,
      signature,
    }),
  },
);

if (!response.ok) {
  throw new Error(`Failed to submit transfer: ${await response.text()}`);
}

const { data } = await response.json();
const jobId = data.id;
```

Eco returns a `jobId` you can poll until the deposit completes or fails.

## Step 5. Wait for the deposit to complete

Poll the job status endpoint:

```typescript theme={null}
let status = "PENDING";
let attempts = 0;
const maxAttempts = 30;

while (status === "PENDING" && attempts < maxAttempts) {
  await new Promise((resolve) => setTimeout(resolve, 2000));

  const response = await fetch(
    `https://deposit-addresses-preproduction.eco.com/api/v1/gasless/jobs/${jobId}`,
  );

  if (!response.ok) {
    throw new Error(`Failed to poll status: ${await response.text()}`);
  }

  const { data } = await response.json();
  status = data.status;
  attempts++;

  console.log(`Status: ${status} (${attempts * 2}s elapsed)`);
}

if (status !== "COMPLETED") {
  throw new Error("Deposit failed or timed out");
}
```

## Step 6. Check your Gateway balance

After the job completes, query the Gateway balances endpoint:

```typescript theme={null}
const response = await fetch(
  "https://gateway-api-testnet.circle.com/v1/balances",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      token: "USDC",
      sources: [{ domain: 7, depositor: yourWalletAddress }], // Polygon PoS Amoy
    }),
  },
);

if (!response.ok) {
  throw new Error(`Failed to fetch balance: ${await response.text()}`);
}

const { balances } = await response.json();
const polygonBalance = balances.find((b) => b.domain === 7);

if (!polygonBalance) {
  throw new Error("No Polygon PoS Amoy Gateway balance returned");
}

console.log(`Gateway balance: ${polygonBalance.balance} USDC`);
```

The current Gateway balances API returns `balance` as a decimal USDC string for
this flow.

## Full deposit script

This script requests the Eco deposit address, signs the EIP-3009 payload,
submits the gasless deposit, waits for completion, and prints the resulting
Gateway balance. Save it as `eco-deposit.ts`.

```typescript filename="eco-deposit.ts" expandable theme={null}
import { randomBytes } from "node:crypto";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";

const USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_CHAIN_ID = 84532;
const POLYGON_AMOY_DOMAIN = 7;
const ECO_BASE_URL = "https://deposit-addresses-preproduction.eco.com/api/v1";
const GATEWAY_API_URL = "https://gateway-api-testnet.circle.com/v1/balances";
const DEPOSIT_AMOUNT = 1000000n; // 1 USDC

async function requestDepositAddress(depositor: `0x${string}`) {
  const response = await fetch(
    `${ECO_BASE_URL}/depositAddresses/gateway/polygon`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        chainId: BASE_SEPOLIA_CHAIN_ID,
        depositor,
        evmDestinationAddress: depositor,
      }),
    },
  );

  if (!response.ok) {
    throw new Error(`Failed to get deposit address: ${await response.text()}`);
  }

  const { data } = await response.json();
  return data.evmDepositAddress as `0x${string}`;
}

async function signTransferAuthorization(
  walletClient: ReturnType<typeof createWalletClient>,
  account: ReturnType<typeof privateKeyToAccount>,
  from: `0x${string}`,
  to: `0x${string}`,
) {
  const nonce = `0x${randomBytes(32).toString("hex")}` as `0x${string}`;
  const validAfter = 0n;
  const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600);

  const signature = await walletClient.signTypedData({
    account,
    domain: {
      name: "USDC",
      version: "2",
      chainId: BASE_SEPOLIA_CHAIN_ID,
      verifyingContract: USDC_BASE_SEPOLIA as `0x${string}`,
    },
    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,
      to,
      value: DEPOSIT_AMOUNT,
      validAfter,
      validBefore,
      nonce,
    },
  });

  return {
    nonce,
    validAfter,
    validBefore,
    signature,
  };
}

async function submitGaslessDeposit(params: {
  from: `0x${string}`;
  to: `0x${string}`;
  nonce: `0x${string}`;
  validAfter: bigint;
  validBefore: bigint;
  signature: `0x${string}`;
}) {
  const response = await fetch(
    `${ECO_BASE_URL}/gasless/transferWithAuthorization`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        chainId: BASE_SEPOLIA_CHAIN_ID,
        from: params.from,
        to: params.to,
        value: DEPOSIT_AMOUNT.toString(),
        validAfter: params.validAfter.toString(),
        validBefore: params.validBefore.toString(),
        nonce: params.nonce,
        signature: params.signature,
      }),
    },
  );

  if (!response.ok) {
    throw new Error(`Failed to submit transfer: ${await response.text()}`);
  }

  const { data } = await response.json();
  return data.id as string;
}

async function waitForCompletion(jobId: string) {
  let status = "PENDING";
  let attempts = 0;
  const maxAttempts = 30;

  while (status === "PENDING" && attempts < maxAttempts) {
    await new Promise((resolve) => setTimeout(resolve, 2000));

    const response = await fetch(`${ECO_BASE_URL}/gasless/jobs/${jobId}`);

    if (!response.ok) {
      throw new Error(`Failed to poll status: ${await response.text()}`);
    }

    const { data } = await response.json();
    status = data.status as string;
    attempts++;

    console.log(`Status: ${status} (${attempts * 2}s elapsed)`);
  }

  if (status !== "COMPLETED") {
    throw new Error("Deposit failed or timed out");
  }
}

async function checkGatewayBalance(depositor: `0x${string}`) {
  const response = await fetch(GATEWAY_API_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      token: "USDC",
      sources: [{ domain: POLYGON_AMOY_DOMAIN, depositor }],
    }),
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch balance: ${await response.text()}`);
  }

  const { balances } = await response.json();
  const polygonBalance = balances.find(
    (balance: { domain: number; balance: string }) =>
      balance.domain === POLYGON_AMOY_DOMAIN,
  );

  if (!polygonBalance) {
    throw new Error("No Polygon PoS Amoy Gateway balance returned");
  }

  return polygonBalance.balance as string;
}

async function main() {
  const rawPrivateKey = process.env.PRIVATE_KEY?.trim();
  if (!rawPrivateKey) {
    throw new Error("Missing PRIVATE_KEY in .env");
  }

  const privateKey = (
    rawPrivateKey.startsWith("0x") ? rawPrivateKey : `0x${rawPrivateKey}`
  ) as `0x${string}`;

  const account = privateKeyToAccount(privateKey);
  const walletClient = createWalletClient({
    account,
    chain: baseSepolia,
    transport: http(),
  });

  console.log(`\nWallet: ${account.address}`);
  console.log(`Amount: ${Number(DEPOSIT_AMOUNT) / 1e6} USDC\n`);

  console.log("Step 1: Requesting deposit address...");
  const depositAddress = await requestDepositAddress(account.address);
  console.log(`Deposit address: ${depositAddress}\n`);

  console.log("Step 2: Signing authorization...");
  const authorization = await signTransferAuthorization(
    walletClient,
    account,
    account.address,
    depositAddress,
  );
  console.log("Signature generated\n");

  console.log("Step 3: Submitting to Eco...");
  const jobId = await submitGaslessDeposit({
    from: account.address,
    to: depositAddress,
    ...authorization,
  });
  console.log(`Job ID: ${jobId}\n`);

  console.log("Step 4: Waiting for completion...");
  await waitForCompletion(jobId);
  console.log("\nDeposit successful\n");

  console.log("Step 5: Checking Gateway balance...");
  const gatewayBalance = await checkGatewayBalance(account.address);
  console.log(`Gateway balance: ${gatewayBalance} USDC\n`);
}

main().catch((error) => {
  if (error instanceof Error) {
    console.error("Error:", error.message);
    return;
  }

  console.error("Error:", error);
});
```

Run the script:

```bash theme={null}
npm run start
```

For withdrawal and crosschain transfer capabilities, see the
[Gateway documentation](https://developers.circle.com/gateway). To use your
Gateway balance for Gateway-based USDC flows, explore the
[Arc Nanopayments Demo](https://github.com/circlefin/arc-nanopayments).
