> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unlink.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a private nanopayment app

> End-to-end tutorial. Sign in with Dynamic, fund a private Unlink account, and pay an x402 resource on Arc Testnet through Circle Gateway.

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.

<Info>
  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.
</Info>

## Prerequisites

* A Dynamic account or sandbox from the [Dynamic dashboard](https://app.dynamic.xyz/), 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](/quickstart#create-an-api-key) and stored as `UNLINK_API_KEY` on your backend only.
* Arc Testnet USDC for the payer EOA from the [Circle faucet](https://faucet.circle.com/). Arc uses USDC as its gas token, so the payer needs USDC for both the Gateway deposit and the payment. See [Supported chains](/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.

<Steps>
  <Step title="Sign the user in with Dynamic">
    [Dynamic](https://docs.dynamic.xyz) signs users in and secures their wallet.
    Create a Dynamic app or sandbox in the
    [Dynamic dashboard](https://app.dynamic.xyz/), sign the user in, and read the
    session token. The Dynamic user id (the JWT `sub`) becomes the Unlink user id.

    ```ts theme={null}
    const dynamicToken = dynamicClient.token; // Dynamic session JWT
    const userId = dynamicUserIdFromToken(dynamicToken);
    ```
  </Step>

  <Step title="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`.

    ```ts theme={null}
    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](https://www.dynamic.xyz/docs/react/wallets/embedded-wallets/mpc/delegated-access/storage-best-practices#security-requirements-checklist).
    See [Accounts and keys](/accounts-and-keys#user-storage) for the envelope
    shape and key-safety rules.
  </Step>

  <Step title="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](/custody-models#app-backend): authenticate with the verified
    Dynamic `sub`, and authorize user storage only for the matching id.

    ```ts theme={null}
    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.
  </Step>

  <Step title="Fund the private account">
    Seed the private account with Arc USDC using the faucet helper, then confirm
    with `getBalances`.

    ```ts theme={null}
    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](/faucet).
  </Step>

  <Step title="Transfer privately (optional)">
    Optionally move funds privately between Unlink accounts before withdrawing. A
    private hop strengthens unlinkability. See [Transfer](/transfer) for multiple
    recipients and parameters.

    ```ts theme={null}
    const tx = await client.transfer({
      recipientAddress: "unlink1recipient...",
      token: usdc,
      amount: "1000000", // 1 USDC, base units
    });

    await tx.wait();
    ```
  </Step>

  <Step title="Withdraw to the payer EOA">
    Withdraw a smaller amount privately to the payer EOA. Unlink amounts are in
    base units. See [Withdraw](/withdraw) for the parameter reference.

    ```ts theme={null}
    const withdrawal = await client.withdraw({
      recipientEvmAddress: payerAddress,
      token: usdc,
      amount: "2000000", // 2 USDC, base units
    });

    await withdrawal.wait();
    ```

    <Note>
      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.
    </Note>
  </Step>

  <Step title="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.

    ```ts theme={null}
    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](https://developers.circle.com/gateway/nanopayments/howtos/x402-buyer)
    and
    [supported chains](https://developers.circle.com/gateway/nanopayments/supported-networks).
  </Step>
</Steps>

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

<CardGroup cols={2}>
  <Card title="Get funds on Arc Testnet" icon="droplet" href="https://faucet.circle.com/">
    Fund testnet wallets with Arc USDC.
  </Card>

  <Card title="Enable Arc in Dynamic" icon="wallet" href="https://app.dynamic.xyz/dashboard/chains-and-networks#evm">
    Optional if your app switches Dynamic wallets to Arc.
  </Card>
</CardGroup>

For the privacy model behind this flow, see
[How Unlink works](/how-unlink-works).
