Skip to main content
This tutorial builds one runnable flow that pays for an x402 resource without linking the payment back to the user’s funding wallet. It combines four tools:
  • Dynamic signs the user in and secures their wallet.
  • Unlink holds the user’s private balance and breaks the on-chain link.
  • Circle Gateway sends gasless x402 nanopayments from an EOA.
  • Arc Testnet settles fast and uses USDC as its gas token.
By the end you will have signed a user in with Dynamic, created and registered a Dynamic-bound Unlink account on arc-testnet, funded it, withdrawn a smaller amount to a payer EOA, and paid an x402 resource through Circle Gateway, with the payer EOA unlinkable from the original funding wallet.

Prerequisites

  • A Dynamic account or sandbox from the Dynamic dashboard, with an environment ID. Enable Arc in Dynamic’s chains list if your app switches wallets to Arc.
  • An Unlink API key for arc-testnet, created in the Quickstart and stored as UNLINK_API_KEY on your backend only.
  • Arc Testnet USDC for the payer EOA from the Circle faucet. Arc uses USDC as its gas token, so the payer needs USDC for both the Gateway deposit and the payment. See Supported chains.
  • Packages: @unlink-xyz/sdk@canary and @circle-fin/x402-batching.
  • The payer EOA’s keypair (payerAddress, payerPrivateKey) and an Arc RPC URL (rpcUrl), which you supply in the final Gateway step.
Throughout, usdc is the Arc USDC token address configured for your environment.
1

Sign the user in with Dynamic

Dynamic signs users in and secures their wallet. Create a Dynamic app or sandbox in the Dynamic dashboard, sign the user in, and read the session token. The Dynamic user id (the JWT sub) becomes the Unlink user id.
const dynamicToken = dynamicClient.token; // Dynamic session JWT
const userId = dynamicUserIdFromToken(dynamicToken);
2

Create and register the Unlink account

Use the Dynamic user id as the Unlink userId. Recover or create the encrypted recovery envelope, then create the client and register the private account on arc-testnet.
import { account, createUnlinkClient } from "@unlink-xyz/sdk/browser";

const mnemonic = await recoverOrCreateUnlinkMnemonic({ userId, dynamicToken });

const client = createUnlinkClient({
  environment: "arc-testnet",
  account: account.fromMnemonic({ mnemonic }),
  userId,
});

await client.ensureRegistered();
Implement recoverOrCreateUnlinkMnemonic in your app: create a temporary Unlink client with the Dynamic sub as userId, use client.userStorage to store only an encrypted recovery envelope, decrypt locally, and return the mnemonic. Treat the envelope as wallet material. Encrypt it with a key derived locally, never from the Dynamic JWT or user id, and follow Dynamic’s storage best practices. See Accounts and keys for the envelope shape and key-safety rules.
3

Authorize the account on your backend

Mount the Unlink auth routes behind Dynamic JWT verification. This is the Dynamic-flavored version of the routes in Custody models: authenticate with the verified Dynamic sub, and authorize user storage only for the matching id.
import {
  createUnlinkAdmin,
  createUnlinkAuthRoutes,
} from "@unlink-xyz/sdk/admin";

const admin = createUnlinkAdmin({
  environment: "arc-testnet",
  apiKey: process.env.UNLINK_API_KEY!,
});

const routes = createUnlinkAuthRoutes({
  admin,
  authenticate: async (request) => {
    const userId = await requireDynamicUserId(request);
    return { userId };
  },
  onRegister: async ({ session, registration }) => {
    await db.linkUnlinkAddress(session.userId, registration.address);
  },
  authorizeUnlinkAddress: async ({ session, unlinkAddress }) =>
    db.userOwnsUnlinkAddress(session.userId, unlinkAddress),
  authorizeUserStorage: async ({ session, userId }) =>
    session.userId === userId,
});
Storage tokens must authorize only the matching Dynamic user id.
4

Fund the private account

Seed the private account with Arc USDC using the faucet helper, then confirm with getBalances.
await client.faucet.requestPrivateTokens({ token: usdc });

const { balances } = await client.getBalances();
The faucet tx_id is not pollable, so balances are the confirmation signal. See Faucet.
5

Transfer privately (optional)

Optionally move funds privately between Unlink accounts before withdrawing. A private hop strengthens unlinkability. See Transfer for multiple recipients and parameters.
const tx = await client.transfer({
  recipientAddress: "unlink1recipient...",
  token: usdc,
  amount: "1000000", // 1 USDC, base units
});

await tx.wait();
6

Withdraw to the payer EOA

Withdraw a smaller amount privately to the payer EOA. Unlink amounts are in base units. See Withdraw for the parameter reference.
const withdrawal = await client.withdraw({
  recipientEvmAddress: payerAddress,
  token: usdc,
  amount: "2000000", // 2 USDC, base units
});

await withdrawal.wait();
For privacy hygiene, avoid a same-size deposit and withdrawal in the same payment flow. Amount and timing correlation can weaken unlinkability. Keep a larger balance in the private pool, optionally transfer privately, then withdraw smaller payer amounts later.
7

Pay an x402 resource through Circle Gateway

After the payer EOA receives the withdrawal, deposit into Circle Gateway and pay. Gateway deposit amounts are decimal USDC, while Unlink withdrawals are base units.
import { GatewayClient } from "@circle-fin/x402-batching/client";

const gateway = new GatewayClient({
  chain: "arcTestnet",
  privateKey: payerPrivateKey,
  rpcUrl,
});

await gateway.deposit("1.99");
await gateway.pay("https://seller.example/premium-data");
The Gateway payer must be a plain EOA. Do not use an Unlink execution account or smart account as the payer. Keep some withdrawn USDC on the payer EOA because USDC is also the gas token for the Gateway deposit transaction. In Unlink, Arc Testnet is the SDK environment arc-testnet; in Circle Gateway it is the chain name arcTestnet. See Circle’s nanopayments buyer guide and supported chains.

What you built

You signed a user in with Dynamic, created and registered a Dynamic-bound Unlink account on Arc Testnet, funded it, withdrew a smaller amount to a payer EOA, and paid an x402 resource through Circle Gateway. The payer EOA cannot be linked back to the original funding wallet.

Get funds on Arc Testnet

Fund testnet wallets with Arc USDC.

Enable Arc in Dynamic

Optional if your app switches Dynamic wallets to Arc.
For the privacy model behind this flow, see How Unlink works.