Skip to main content
This guide shows how to build an automated stablecoin collection system where your platform pulls USDC from client wallets on a schedule and settles to a downstream destination, without requiring manual approval from clients on every transfer. A common real-world pattern: a payment processor needs to collect USDC from their fintech clients each day and forward it to a card network for settlement. The flow is:
Client wallet (one-time Smart Session grant)
    → Platform checks balance via Indexer
        → Platform pulls USDC on schedule
            → Treasury wallet aggregates funds
                → Treasury pushes to settlement destination
No per-transfer approval is needed from clients after the initial session grant.

How it works

There are two roles:
  • Client wallet: A Sequence Embedded Wallet held by each client. During onboarding, the client grants a one-time Explicit Smart Session that authorizes your platform to pull USDC up to a defined daily cap, locked to your treasury address only.
  • Treasury wallet: An operator-controlled Sequence wallet. It receives pulled USDC from client wallets and forwards it to the settlement destination (e.g., a card network settlement account or stablecoin bridge).

Prerequisites

  • A project access key from the Polygon project dashboard
  • Polygon mainnet configured (chain ID: 137)
  • USDC on Polygon: 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
  • An operator-controlled treasury wallet

Install

# Frontend (client session grant)
pnpm install @0xsequence/connect wagmi viem

# Backend (monitoring)
pnpm install @0xsequence/indexer

Step 1: Client grants a Smart Session

Each client connects their Sequence Embedded Wallet during onboarding and grants one Explicit Smart Session. This session authorizes your backend to pull USDC up to a daily cap, locked to your treasury address. It cannot be used to send funds anywhere else. Configure this in your frontend provider:
import {
  SequenceConnect,
  createConfig,
  createContractPermission,
  createExplicitSession,
} from "@0xsequence/connect";
import { parseUnits } from "viem";

const USDC_POLYGON = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
const TREASURY_ADDRESS = "0xYourTreasuryAddress";
const DAILY_CAP_USDC = parseUnits("100000", 6); // 100,000 USDC

const config = createConfig({
  projectAccessKey: process.env.NEXT_PUBLIC_PROJECT_ACCESS_KEY!,
  signIn: { projectName: "Your Platform" },
  walletUrl: "https://wallet.polygon.technology",
  dappOrigin: window.location.origin,
  appName: "Your Platform",
  chainIds: [137],
  defaultChainId: 137,
  explicitSession: createExplicitSession({
    chainId: 137,
    nativeTokenSpending: { valueLimit: 0n },
    expiresIn: { days: 30 },
    permissions: [
      createContractPermission({
        address: USDC_POLYGON,
        functionSignature: "function transfer(address to, uint256 amount)",
        rules: [
          {
            param: "to",
            type: "address",
            condition: "EQUAL",
            value: TREASURY_ADDRESS, // locked — cannot send to any other address
          },
          {
            param: "amount",
            type: "uint256",
            condition: "LESS_THAN_OR_EQUAL",
            value: DAILY_CAP_USDC,
            cumulative: true, // enforced across all pulls in the session period
          },
        ],
      }),
    ],
  }),
});

export const App = () => (
  <SequenceConnect config={config}>
    <YourApp />
  </SequenceConnect>
);
The client reviews the permission scope and signs once. Your backend can pull up to the cap at any time within the 30-day session without prompting them again.
cumulative: true enforces the cap across all transfers during the session period, not per-transfer. A client with a 100,000 USDC cap cannot be exceeded in total until the session expires and they re-authorize.

Step 2: Check client USDC balance

Before each pull, verify the client has sufficient USDC using the Sequence Indexer API. This avoids failed transactions and lets you pull only what’s available.
import { SequenceIndexer } from "@0xsequence/indexer";

const indexer = new SequenceIndexer(
  "https://polygon-indexer.sequence.app",
  process.env.PROJECT_ACCESS_KEY!
);

const USDC_POLYGON = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";

async function getClientUsdcBalance(clientAddress: string): Promise<bigint> {
  const result = await indexer.getTokenBalances({
    accountAddress: clientAddress,
    contractAddress: USDC_POLYGON,
    includeMetadata: false,
  });

  const usdcBalance = result.balances.find(
    (b) => b.contractAddress.toLowerCase() === USDC_POLYGON.toLowerCase()
  );

  return BigInt(usdcBalance?.balance ?? "0");
}

Step 3: Pull USDC from client wallets

Your backend runs a scheduled job that calls transfer on each client’s USDC balance via their active Smart Session. The session rules (address lock and cumulative cap) are enforced onchain by the client’s smart account.
import { createWalletClient, http, parseAbi } from "viem";
import { polygon } from "viem/chains";

const USDC_POLYGON = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
const TREASURY_ADDRESS = "0xYourTreasuryAddress";

const USDC_ABI = parseAbi([
  "function transfer(address to, uint256 amount) returns (bool)",
]);

async function pullFromClient(
  sessionWalletClient: ReturnType<typeof createWalletClient>,
  clientAddress: `0x${string}`,
  amountUsdc: bigint
): Promise<`0x${string}`> {
  const hash = await sessionWalletClient.writeContract({
    address: USDC_POLYGON,
    abi: USDC_ABI,
    functionName: "transfer",
    args: [TREASURY_ADDRESS, amountUsdc],
    account: clientAddress,
    chain: polygon,
  });

  console.log("Pull submitted. tx:", hash);
  return hash;
}

Step 4: Push from treasury to settlement

Once USDC has accumulated in the treasury, forward it to the settlement destination using your operator-controlled treasury wallet. If your treasury is itself a Sequence wallet, you can use the same SDK to manage outbound settlement.
async function pushToSettlement(
  treasuryWalletClient: ReturnType<typeof createWalletClient>,
  treasuryAddress: `0x${string}`,
  settlementAddress: `0x${string}`,
  amountUsdc: bigint
): Promise<`0x${string}`> {
  const hash = await treasuryWalletClient.writeContract({
    address: USDC_POLYGON,
    abi: USDC_ABI,
    functionName: "transfer",
    args: [settlementAddress, amountUsdc],
    account: treasuryAddress,
    chain: polygon,
  });

  console.log("Settlement push submitted. tx:", hash);
  return hash;
}

Step 5: Monitor with Indexer

After each run, check the live treasury balance and query transaction history to verify pulls settled correctly.
async function getTreasuryBalance(): Promise<bigint> {
  const result = await indexer.getTokenBalances({
    accountAddress: TREASURY_ADDRESS,
    contractAddress: USDC_POLYGON,
    includeMetadata: false,
  });

  const usdcBalance = result.balances.find(
    (b) => b.contractAddress.toLowerCase() === USDC_POLYGON.toLowerCase()
  );

  return BigInt(usdcBalance?.balance ?? "0");
}

async function getTreasuryHistory() {
  const result = await indexer.getTransactionHistory({
    filter: {
      accountAddress: TREASURY_ADDRESS,
      contractAddresses: [USDC_POLYGON],
    },
  });

  return result.transactions;
}

Putting it together

A complete daily settlement run: check balances, pull from each client, push to settlement, and log the result:
async function dailySettlementRun(
  clients: Array<{
    sessionWalletClient: ReturnType<typeof createWalletClient>;
    address: `0x${string}`;
    requestedAmount: bigint;
  }>,
  treasuryWalletClient: ReturnType<typeof createWalletClient>,
  treasuryAddress: `0x${string}`,
  settlementAddress: `0x${string}`
) {
  let totalCollected = 0n;

  // 1. Check balance and pull from each client
  for (const client of clients) {
    const available = await getClientUsdcBalance(client.address);
    const pullAmount =
      available < client.requestedAmount ? available : client.requestedAmount;

    if (pullAmount === 0n) {
      console.log(`Skipping ${client.address} — zero balance`);
      continue;
    }

    await pullFromClient(client.sessionWalletClient, client.address, pullAmount);
    totalCollected += pullAmount;
  }

  // 2. Push accumulated USDC to settlement
  if (totalCollected > 0n) {
    await pushToSettlement(
      treasuryWalletClient,
      treasuryAddress,
      settlementAddress,
      totalCollected
    );
  }

  // 3. Log final treasury balance
  const finalBalance = await getTreasuryBalance();
  console.log(
    `Run complete. Collected: ${totalCollected} USDC. Treasury balance: ${finalBalance} USDC`
  );
}

Security considerations

  • Session locking: The to address rule locks transfers to your treasury address only. A client’s session cannot be used to send funds anywhere else, even if your backend is compromised.
  • Cumulative cap: cumulative: true prevents the backend from pulling more than the authorized total across the session period.
  • Onchain enforcement: Session rules are enforced at the smart contract level. They cannot be bypassed by your backend or any third party.
  • Key custody: Client wallets are non-custodial. Private keys are secured in AWS Nitro Enclaves via threshold cryptography. Neither your platform nor Sequence holds user keys.

Next steps

Smart Sessions

Understand implicit vs. explicit sessions and when to use each.

Wallet Quickstart

Set up a Sequence Embedded Wallet from scratch.

Allow Your Users to Earn

Route client USDC into DeFi yield vaults using Trails.

Wallet Operations

Full reference for sending transactions and reading balances.