> ## 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: Pay for resources with nanopayments

> Deposit USDC into Gateway and make gasless payments to x402-protected APIs

In this quickstart, you will deposit USDC into a Gateway Wallet, pay for an
x402-protected resource without gas fees, and check your balance. By the end,
you'll have a working client that can make gasless payments to any
x402-compatible API that supports Circle Gateway.

<Tabs>
  <Tab title="Circle Wallets">
    Use `@circle-fin/developer-controlled-wallets` to fund an Arc Testnet EOA and
    `@circle-fin/x402-batching` to pay for an x402-protected resource. Smart
    contract account (SCA) wallets are not supported for nanopayments because
    Gateway verifies the EIP-3009 authorization with `ecrecover`. For more
    information, see [Account types](/wallets/account-types).

    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22.6+](https://nodejs.org/).
    * Created a [Circle Console](https://console.circle.com/) account.
    * Created an API key in the [Circle Console](https://console.circle.com/).
    * [Generated and registered an Entity Secret](/wallets/dev-controlled/register-entity-secret).
    * [Created an EOA developer-controlled wallet](/wallets/dev-controlled/create-your-first-wallet)
      on Arc Testnet.

    Complete the linked quickstarts before continuing. This path starts once your
    credentials and Arc Testnet wallet address are available. The wallet address is
    public configuration, so you will add it directly to `pay.ts`.

    ## Step 1. Set up your project

    ### 1.1. Create the project and install dependencies

    ```shell theme={null}
    mkdir nanopayments-circle-wallets
    cd nanopayments-circle-wallets
    npm init -y
    npm pkg set type=module
    npm pkg set scripts.pay="node --env-file=.env pay.ts"
    npm install @circle-fin/developer-controlled-wallets @circle-fin/x402-batching
    npm install --save-dev typescript @types/node
    ```

    ### 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 .env theme={null}
    CIRCLE_API_KEY=YOUR_API_KEY
    CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
    ```

    * `CIRCLE_API_KEY` is your Circle API key.
    * `CIRCLE_ENTITY_SECRET` is your Circle entity secret.

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

    ## Step 2. Initialize the Circle Wallets client

    Create `pay.ts` and replace the wallet address placeholder with the address from
    the developer-controlled wallet quickstart. Copy this block into `pay.ts` first.

    ```ts pay.ts expandable theme={null}
    import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
    import {
      BatchEvmScheme,
      CHAIN_CONFIGS,
    } from "@circle-fin/x402-batching/client";

    function stringifyTypedData(value: unknown): string {
      return JSON.stringify(value, (_, currentValue) =>
        typeof currentValue === "bigint" ? currentValue.toString() : currentValue,
      );
    }

    const apiKey = process.env.CIRCLE_API_KEY;
    const entitySecret = process.env.CIRCLE_ENTITY_SECRET;
    const walletAddress = "0xYOUR_ARC_TESTNET_WALLET_ADDRESS" as `0x${string}`;

    if (!apiKey || !entitySecret) {
      throw new Error(
        "CIRCLE_API_KEY and CIRCLE_ENTITY_SECRET are required in .env.",
      );
    }

    if (walletAddress === "0xYOUR_ARC_TESTNET_WALLET_ADDRESS") {
      throw new Error(
        "Replace walletAddress with your Arc Testnet wallet address.",
      );
    }

    const circleClient = initiateDeveloperControlledWalletsClient({
      apiKey,
      entitySecret,
    });

    const chain = CHAIN_CONFIGS.arcTestnet;
    const depositAmount = "1";
    // 1 USDC = 1_000_000 base units (6 decimals)
    const depositAmountBaseUnits = "1000000";
    const gatewayApi = "https://gateway-api-testnet.circle.com";
    const protectedUrl = "http://localhost:3000/premium-data";

    const batchScheme = new BatchEvmScheme({
      address: walletAddress,
      signTypedData: async (params) => {
        const typedData = {
          domain: {
            ...params.domain,
            chainId: params.domain.chainId.toString(),
          },
          primaryType: params.primaryType,
          types: {
            EIP712Domain: [
              { name: "name", type: "string" },
              { name: "version", type: "string" },
              { name: "chainId", type: "uint256" },
              { name: "verifyingContract", type: "address" },
            ],
            ...params.types,
          },
          message: params.message,
        };

        const response = await circleClient.signTypedData({
          walletAddress,
          blockchain: "ARC-TESTNET",
          data: stringifyTypedData(typedData),
        });

        const signature = response.data?.signature;
        if (!signature) {
          throw new Error("Circle Wallets returned no signature.");
        }
        return (
          signature.startsWith("0x") ? signature : `0x${signature}`
        ) as `0x${string}`;
      },
    });
    ```

    `BatchEvmScheme` builds the EIP-3009 payment authorization. The callback
    normalizes its viem-style typed data for Circle Wallets by adding the explicit
    `EIP712Domain` type, serializing `chainId`, and converting the response to a
    `0x`-prefixed signature. The developer-controlled wallet signs the authorization
    without exposing a private key to this script.

    ## Step 3. Fund the wallet

    Use the [Circle Faucet](https://faucet.circle.com/) to send testnet USDC to the
    Arc Testnet wallet address from the wallet quickstart. Arc Testnet uses
    [USDC as its native gas token](https://docs.arc.io/arc/concepts/stablecoin-native-model#usdc-as-the-native-gas-token),
    so the faucet funds both the USDC deposit and the transaction fees.

    ## Step 4. Deposit USDC into Gateway

    <Tip>
      **Prefer a shorter deposit flow?** Use
      [Unified Balance Kit](https://docs.arc.io/app-kit/unified-balance) with the
      [Circle Wallets adapter](https://docs.arc.io/app-kit/tutorials/adapter-setups#circle-wallets).
      See
      [Deposit and spend a Unified Balance (Circle Wallets)](https://docs.arc.io/app-kit/quickstarts/unified-balance-deposit-and-spend)
      for Arc Testnet.
    </Tip>

    Before making a nanopayment, the wallet needs USDC in its Gateway balance. The
    following function:

    1. Checks the current Gateway balance.
    2. Approves the Gateway Wallet contract to spend USDC.
    3. Deposits USDC into Gateway.
    4. Waits until the deposited balance is available.

    Add these functions to `pay.ts`. They are standalone declarations, so copy them
    as-is below the initialization code.

    ```ts pay.ts theme={null}
    async function ensureGatewayBalance(): Promise<void> {
      let gatewayBalance = await getGatewayBalance();

      if (gatewayBalance >= Number(depositAmount)) {
        console.log(`Gateway balance: ${gatewayBalance} USDC`);
        return;
      }

      console.log(`Approving ${depositAmount} USDC for Gateway...`);
      const approval = await circleClient.createContractExecutionTransaction({
        walletAddress,
        blockchain: "ARC-TESTNET",
        contractAddress: chain.usdc,
        abiFunctionSignature: "approve(address,uint256)",
        abiParameters: [chain.gatewayWallet, depositAmountBaseUnits],
        fee: { type: "level", config: { feeLevel: "MEDIUM" } },
      });

      const approvalId = approval.data?.id;
      if (!approvalId) {
        throw new Error("USDC approval was not created.");
      }
      await waitForCircleTransaction(approvalId);

      console.log(`Depositing ${depositAmount} USDC into Gateway...`);
      const deposit = await circleClient.createContractExecutionTransaction({
        walletAddress,
        blockchain: "ARC-TESTNET",
        contractAddress: chain.gatewayWallet,
        abiFunctionSignature: "deposit(address,uint256)",
        abiParameters: [chain.usdc, depositAmountBaseUnits],
        fee: { type: "level", config: { feeLevel: "MEDIUM" } },
      });

      const depositId = deposit.data?.id;
      if (!depositId) {
        throw new Error("Gateway deposit was not created.");
      }
      await waitForCircleTransaction(depositId);

      console.log("Waiting for the Gateway balance to become available...");
      do {
        await new Promise((resolve) => setTimeout(resolve, 3000));
        gatewayBalance = await getGatewayBalance();
      } while (gatewayBalance < Number(depositAmount));

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

    async function getGatewayBalance(): Promise<number> {
      const response = await fetch(`${gatewayApi}/v1/balances`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          token: "USDC",
          sources: [{ domain: 26, depositor: walletAddress }],
        }),
      });

      if (!response.ok) {
        throw new Error(
          `Gateway balance request failed: ${response.status} ${await response.text()}`,
        );
      }

      const result = (await response.json()) as {
        balances: Array<{ domain: number; balance: string }>;
      };

      return Number(
        result.balances.find(({ domain }) => domain === 26)?.balance ?? "0",
      );
    }

    async function waitForCircleTransaction(transactionId: string): Promise<void> {
      const terminalStates = new Set([
        "COMPLETE",
        "CONFIRMED",
        "FAILED",
        "DENIED",
        "CANCELLED",
      ]);

      while (true) {
        const response = await circleClient.getTransaction({ id: transactionId });
        const state = response.data?.transaction?.state;

        if (state && terminalStates.has(state)) {
          if (state !== "COMPLETE" && state !== "CONFIRMED") {
            throw new Error(`Circle Wallet transaction ended in state: ${state}`);
          }
          return;
        }

        await new Promise((resolve) => setTimeout(resolve, 3000));
      }
    }
    ```

    ## Step 5. Pay for a resource

    The first request to an x402-protected endpoint returns `402 Payment Required`
    and a `PAYMENT-REQUIRED` header. Sellers often advertise Gateway options for
    many blockchains. Select the Arc Testnet `GatewayWalletBatched` option so the
    EIP-712 `chainId` matches the Circle Wallets `ARC-TESTNET` signer, then create
    the authorization and retry with a `PAYMENT-SIGNATURE` that includes the chosen
    option and the `resource` metadata from the `402` response.

    Add `payForResource` to `pay.ts`:

    ```ts pay.ts theme={null}
    async function payForResource(url: string): Promise<unknown> {
      const unpaidResponse = await fetch(url);
      if (unpaidResponse.status !== 402) {
        throw new Error(
          `Expected 402 Payment Required, received ${unpaidResponse.status}.`,
        );
      }

      const paymentRequiredHeader = unpaidResponse.headers.get("PAYMENT-REQUIRED");
      if (!paymentRequiredHeader) {
        throw new Error("The endpoint did not return a PAYMENT-REQUIRED header.");
      }

      const paymentRequired = JSON.parse(
        Buffer.from(paymentRequiredHeader, "base64").toString("utf8"),
      ) as {
        x402Version: number;
        accepts: Array<{
          scheme: string;
          network: string;
          asset: string;
          amount: string;
          payTo: string;
          maxTimeoutSeconds: number;
          extra?: Record<string, unknown>;
        }>;
        resource?: unknown;
      };

      const arcNetwork = `eip155:${chain.chain.id}`;
      const gatewayOption = paymentRequired.accepts.find(
        (option) =>
          option.extra?.name === "GatewayWalletBatched" &&
          option.network === arcNetwork,
      );

      if (!gatewayOption) {
        throw new Error(
          `The endpoint does not support Gateway nanopayments on ${arcNetwork}.`,
        );
      }

      if (!paymentRequired.resource) {
        throw new Error("The endpoint did not return resource metadata.");
      }

      const paymentPayload = await batchScheme.createPaymentPayload(
        paymentRequired.x402Version,
        gatewayOption,
      );
      const paidResponse = await fetch(url, {
        headers: {
          "PAYMENT-SIGNATURE": Buffer.from(
            JSON.stringify({
              ...paymentPayload,
              accepted: gatewayOption,
              resource: paymentRequired.resource,
            }),
          ).toString("base64"),
        },
      });

      if (!paidResponse.ok) {
        throw new Error(
          `Payment failed: ${paidResponse.status} ${await paidResponse.text()}`,
        );
      }

      return paidResponse.json();
    }
    ```

    ## Step 6. Run the script

    At the end of `pay.ts`, add a small entry point that calls the functions in
    order:

    ```ts pay.ts theme={null}
    async function main(): Promise<void> {
      await ensureGatewayBalance();

      const data = await payForResource(protectedUrl);
      console.log("Paid response:");
      console.dir(data, { depth: null });
    }

    main().catch((error) => {
      console.error("Error:");
      console.dir(error, { depth: null });
      process.exit(1);
    });
    ```

    <Tip>
      Start the protected endpoint from the [seller
      quickstart](/gateway-nanopayments/quickstarts/seller) before running this
      script. It should be available at `http://localhost:3000/premium-data`.
    </Tip>

    Run the script:

    ```shell theme={null}
    npm run pay
    ```

    <Note>
      The seller returns the resource as soon as Gateway accepts the payment.
      Onchain settlement happens later when Gateway includes the [payment in a
      batch](/gateway-nanopayments/concepts/batched-settlement) (often several
      minutes). You do not need to wait for that to complete a successful pay. To
      inspect status later, query [Search x402
      Transfers](/api-reference/gateway-nanopayments/search-x402transfers) with the
      EIP-3009 authorization nonce.
    </Note>
  </Tab>

  <Tab title="Self-managed">
    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22.6+](https://nodejs.org/).
    * Obtained an
      [EOA (externally owned account)](/wallets/account-types#externally-owned-accounts-eoa)
      wallet private key for signing transactions and payment authorizations.
    * Obtained testnet USDC from the [Circle Faucet](https://faucet.circle.com).
    * Funded your wallet with testnet ETH (or native gas token) for the one-time
      deposit transaction.

    <Warning>
      Gateway Nanopayments requires an EOA wallet. Smart contract account (SCA)
      wallets are not supported because the batched settlement path verifies EIP-3009
      payment authorizations offchain using `ecrecover`, which is incompatible with
      ERC-1271 contract signatures. This limitation is specific to Gateway
      Nanopayments. Standard Gateway transfers support
      [ERC-1271 signatures](/gateway/references/erc-1271).
    </Warning>

    ## Step 1. Set up your project

    ### 1.1. Create the project and install dependencies

    ```shell theme={null}
    mkdir nanopayments-buyer
    cd nanopayments-buyer
    npm init -y
    npm pkg set type=module
    npm pkg set scripts.pay="node --env-file=.env pay.ts"
    npm install @circle-fin/x402-batching viem typescript
    npm install --save-dev @types/node
    ```

    ### 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=YOUR_PRIVATE_KEY
    ```

    * `PRIVATE_KEY` is the private key for the EOA you use to deposit USDC and sign
      nanopayment authorizations.

    <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 pay` 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>

    ## Step 2. Initialize the client

    Create a new file `pay.ts` and initialize the `GatewayClient` with your chain
    and private key:

    ```ts pay.ts theme={null}
    import { GatewayClient } from "@circle-fin/x402-batching/client";

    const client = new GatewayClient({
      chain: "arcTestnet",
      privateKey: process.env.PRIVATE_KEY as `0x${string}`,
    });
    ```

    The `chain` parameter determines which blockchain the client connects to for
    deposits and withdrawals. See the
    [SDK reference](/sdks/gateway-nanopayments-sdk) for all supported blockchains.

    ## Step 3. Deposit USDC into Gateway

    Before you can make gasless payments, deposit USDC from your wallet into the
    Gateway Wallet contract. This is a one-time onchain transaction:

    ```ts pay.ts theme={null}
    const balances = await client.getBalances();
    console.log(`Gateway balance: ${balances.gateway.formattedAvailable} USDC`);

    // 1 USDC = 1_000_000 base units (6 decimals)
    if (balances.gateway.available < 1_000_000n) {
      console.log("Depositing 1 USDC...");
      const deposit = await client.deposit("1");
      console.log(`Deposit tx: ${deposit.depositTxHash}`);
    }
    ```

    `getBalances()` calls the
    [Get Token Balances](/api-reference/gateway/all/get-token-balances) API
    endpoint. The deposit itself is an onchain transaction and does not use the
    Gateway API.

    After the deposit confirms, your Gateway balance can be used for gasless
    payments to any supported seller. See the discussion at
    [Fast deposits](/gateway/references/supported-blockchains#fast-deposits) about
    increasing deposit speeds.

    ## Step 4. Pay for a resource

    Add the payment logic to `pay.ts`. Call `client.pay()` with the URL of an
    x402-protected resource. The client handles the full payment flow automatically:

    1. Sends the initial request to the URL.
    2. Receives the `402 Payment Required` response with payment details.
    3. Signs an EIP-3009 authorization offchain (zero gas).
    4. Retries the request with the `PAYMENT-SIGNATURE` header.

    ```ts pay.ts theme={null}
    const url = "http://localhost:3000/premium-data";

    const { data, status } = await client.pay(url);

    console.log(`Status: ${status}`);
    console.log("Response:", data);
    ```

    Under the hood, `pay()` negotiates the `402` flow and submits the payment
    through the
    [Settle x402 Payment](/api-reference/gateway-nanopayments/settle-x402payment)
    API endpoint.

    <Tip>
      Don't have a seller URL to test with? Set up a local test API in two minutes
      using the [seller quickstart](/gateway-nanopayments/quickstarts/seller).
    </Tip>

    ## Step 5. Check your balance

    Add balance checking after the payment using the
    [Get Token Balances](/api-reference/gateway/all/get-token-balances) API
    endpoint:

    ```ts pay.ts theme={null}
    const updated = await client.getBalances();
    console.log(`Wallet USDC: ${updated.wallet.formatted}`);
    console.log(`Gateway available: ${updated.gateway.formattedAvailable}`);
    ```

    ## Step 6. Run the script

    Run the complete script:

    ```shell theme={null}
    npm run pay
    ```

    You should see the deposit transaction (if needed), the response from the paid
    resource, and your updated balance.

    ## Step 7. Withdraw funds (optional)

    You can withdraw USDC from Gateway back to your wallet at any time. Same-chain
    withdrawals are instant:

    ```ts pay.ts theme={null}
    const result = await client.withdraw("5");
    console.log(`Withdrew ${result.formattedAmount} USDC`);
    console.log(`Tx: ${result.mintTxHash}`);
    ```

    To withdraw to a different blockchain:

    ```ts pay.ts theme={null}
    const crossChain = await client.withdraw("5", { chain: "baseSepolia" });
    console.log(`Withdrew to ${crossChain.destinationChain}`);
    ```

    <Note>
      Crosschain withdrawals require native gas tokens on the destination blockchain
      to cover the minting transaction.
    </Note>

    ## Check support before paying

    Before attempting a payment, you can verify that the target URL supports Gateway
    batching. The `supports()` method requests the target URL, checks for a `402`
    response, and inspects the `PAYMENT-REQUIRED` header for a compatible Gateway
    batching option:

    ```ts theme={null}
    const support = await client.supports(url);

    if (!support.supported) {
      console.error("This URL does not support Gateway payments");
    } else {
      const { data } = await client.pay(url);
    }
    ```

    This is useful when building clients that interact with APIs where Gateway
    support is not guaranteed.
  </Tab>
</Tabs>
