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

> ## Agent Instructions
> Before reading these docs, install the relevant Circle Skill to get pre-loaded context — skills are LLM-optimized and cover architecture decisions, correct flows, and common pitfalls. In Claude Code: run /plugin marketplace add circlefin/skills then /plugin install circle-skills@circle. With Vercel Skills CLI: run npx skills add circlefin/skills. Available skills: use-usdc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md (balances, transfers, approvals on EVM and Solana); EURC has no skill — see https://developers.circle.com/stablecoins/what-is-eurc.md; bridge-stablecoin https://github.com/circlefin/skills/blob/master/plugins/circle/skills/bridge-stablecoin/SKILL.md (crosschain transfers via CCTP, USDC, EURC, and third-party assets); use-arc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md (Arc chain config, contract deployment, USDC bridging — Arc docs: https://docs.arc.io/llms.txt); use-circle-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md (choose wallet type); use-developer-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md (custodial wallets for payouts and treasury); use-user-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md (embedded wallets with Web2 login); use-modular-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md (smart contract wallets with passkeys and ERC-4337); use-gateway https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md (unified USDC balance, nanopayments); use-smart-contract-platform https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md (deploy, interact, monitor contracts). Pair any skill with the Circle MCP server for live SDK signatures, contract addresses, and chain IDs: {"mcpServers":{"circle":{"url":"https://api.circle.com/v1/codegen/mcp"}}}.
> Read the product overview first. Each product has an overview explaining when and why to use it. Read it before quickstarts.
> Pick the right wallet type. Developer-controlled (you hold keys), user-controlled (end user holds keys via PIN/social login), or modular (smart contract wallets with passkeys). See https://developers.circle.com/wallets.md.
> Use CCTP V2 for crosschain transfers. CCTP natively transfers USDC and EURC via burn-and-mint, and supports permissionless wrapping of third-party assets via lock-and-mint. V1 is legacy; always use V2 unless V1 is specified or the chain requires it (Noble, Sui, and Aptos still require V1). Migration guide: https://developers.circle.com/cctp/migration-from-v1-to-v2.md.
> Use Bridge Kit for frontend bridging. Wraps CCTP with a simpler SDK for user-facing apps. Use CCTP directly for backend transfers.
> Gas Station vs Paymaster. Gas Station sponsors gas for Circle Wallet transactions. Paymaster lets users pay gas in USDC. Different use cases — read both overviews.
> Gateway Nanopayments for sub-cent payments. Gasless USDC micropayments down to $0.000001 via x402 and batched settlement. For pay-per-request APIs, AI agent payments, streaming.
> Look up USDC addresses per chain. Never hardcode — use https://developers.circle.com/stablecoins/usdc-contract-addresses.md.
> Prefer SDKs over raw API calls. Node.js and Python SDKs handle auth, retries, and errors.
> API key required. Bearer token in Authorization header. Testnet and mainnet use separate keys and may use different base URLs depending on the product.
> Set up webhooks when available. Most operations are async. Webhooks deliver transaction confirmations and state changes.
> When calling list endpoints, paginate using pageSize and pageAfter until no nextPageAfter cursor is returned—stopping at the first page silently misses records.
> Building an AI agent? Start with the Agent Stack—Circle CLI, agent wallets, and nanopayments built for autonomous use cases: https://developers.circle.com/agent-stack.md.

# Create a non-custodial account

> Sign in an onboarded end customer, register their authorization key, and create a non-custodial account on Arc in sandbox

> [!NOTE]
>
> Non-custodial accounts are available in sandbox only. Production non-custodial
> accounts are on the roadmap.

By the end of this quickstart, an end customer has signed in from your React
Native application, registered an authorization key on their device, and
approved the creation of a non-custodial account on Arc. You also get the
account's deposit address. For background, see
[Non-custodial accounts](https://developers.circle.com/digital-asset-accounts/concepts/non-custodial-accounts.md).

You build the flow in five steps. Step 1 runs on your backend, and the rest run
in your application.

## Prerequisites

Before you begin, ensure that you've:

* Obtained a Digital Asset Accounts sandbox API key with non-custodial accounts
  enabled. Contact your Circle representative for access. Keep the key on your
  backend. Never put it in your mobile application.
* Onboarded the end customer, and waited until their first account is active.
  See
  [Onboard customers](https://developers.circle.com/digital-asset-accounts/quickstarts/onboard-customers.md).
  Store their `clientEntityId` on your backend.
* Set up Google sign-in in your application. Google is the only supported
  provider. Your sign-in must let you pass a custom nonce and return an ID
  token. On Android, pass your Google Web application client ID as
  `serverClientId`. Before production, you register your client IDs in Circle
  Hub. See
  [Register your Google client IDs](https://developers.circle.com/digital-asset-accounts/references/going-to-production.md#register-your-google-client-ids).
* Installed the Digital Asset Accounts React Native SDK, its native SDKs, and
  `expo-crypto`. See [Install](https://developers.circle.com/sdks/daa/react-native-sdk.md#install).
* Set up a physical iPhone or Android-powered device with a screen lock. On an
  Android-powered device, also enroll a strong biometric, such as a fingerprint
  or face unlock. The authorization key is hardware-backed, so simulators and
  emulators without secure key hardware can't create one.

## Step 1: Set up a backend relay

The SDK never holds your API key. Your application gives the SDK a transport
that sends each request to your backend. Your backend adds your API key and
forwards the request to the Circle API.

> [!WARNING]
>
> Your API key can act for all of your end customers, and your relay adds it to
> every request that it forwards. Your relay must:
>
> * **Authenticate the end customer**: accept only requests from a signed-in user
>   of your application.
> * **Allow only the SDK's routes**: forward only the operations in the following
>   tables, and reject everything else.
> * **Forward writes only for the end customer's own non-custodial account**:
>   Circle checks the end customer's signature only when the source is a
>   non-custodial account. Your API key alone can create accounts and debit
>   custodial accounts. Forward account, transfer, and withdrawal requests only if
>   they're signed. They must be for a non-custodial account of the signed-in end
>   customer.
> * **Check ownership of unsigned requests**: reads, social sign-in, and deposit
>   address requests don't carry the end customer's signature. Before you forward
>   one, check that the account or resource in the request belongs to the
>   `clientEntityId` of the signed-in end customer.
>
> Otherwise, anyone who can reach your relay could use your API key, and one end
> customer could read another end customer's data or move their funds.

### Allow only the SDK's routes

#### Sign-in and writes

These operations sign the end customer in and change data. The SDK adds the end
customer's signature in the `X-Device-Authorization` header where it's required.

| Operation | Method | Path | End customer signature |
| - | - | - | - |
| [Prepare a social login](https://developers.circle.com/api-reference/digital-asset-accounts/all/prepare-social-login.md) | `POST` | `/v1/accounts/auth/loginRequests` | No |
| [Complete a social session](https://developers.circle.com/api-reference/digital-asset-accounts/all/complete-social-session.md) | `POST` | `/v1/accounts/auth/sessions` | No |
| [Complete device registration](https://developers.circle.com/api-reference/digital-asset-accounts/all/complete-device-registration.md) | `POST` | `/v1/accounts/auth/deviceKeyRegistration` | Yes, plus `X-Registration-Flow-Authorization` |
| [Get the device context](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-device-context.md) | `GET` | `/v1/accounts/auth/context` | Yes |
| [Create an account](https://developers.circle.com/api-reference/digital-asset-accounts/all/create-account.md) | `POST` | `/v1/accounts` | Yes |
| [Create an account transfer](https://developers.circle.com/api-reference/digital-asset-accounts/all/create-account-transfer.md) | `POST` | `/v1/accounts/transfers` | Yes |
| [Create an account bank withdrawal](https://developers.circle.com/api-reference/digital-asset-accounts/all/create-account-withdrawal.md) | `POST` | `/v1/accounts/withdrawals` | Yes |
| [Create an account deposit address](https://developers.circle.com/api-reference/digital-asset-accounts/all/create-account-deposit-address.md) | `POST` | `/v1/accounts/addresses/deposit` | No |

#### Reads

Reads don't carry the end customer's signature, and your API key can read data
for all of your end customers.

> [!WARNING]
>
> Before you forward a read, check that the account or resource in the request
> belongs to the `clientEntityId` of the signed-in end customer. Otherwise, one
> end customer could read another end customer's data through your relay.

> [accordion] Read routes
>
> | Operation | Method | Path |
> | - | - | - |
> | [List all accounts](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-accounts.md) | `GET` | `/v1/accounts` |
> | [Get an account](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account.md) | `GET` | `/v1/accounts/{accountId}` |
> | [List account limits and usage](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-limits-usage.md) | `GET` | `/v1/accounts/{accountId}/limits/usage` |
> | [List all account transfers](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-transfers.md) | `GET` | `/v1/accounts/transfers` |
> | [Get an account transfer](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-transfer.md) | `GET` | `/v1/accounts/transfers/{id}` |
> | [List all account bank withdrawals](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-withdrawals.md) | `GET` | `/v1/accounts/withdrawals` |
> | [Get an account bank withdrawal](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-withdrawal.md) | `GET` | `/v1/accounts/withdrawals/{id}` |
> | [List all account bank deposits](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-deposits.md) | `GET` | `/v1/accounts/deposits` |
> | [Get an account bank deposit](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-deposit.md) | `GET` | `/v1/accounts/deposits/{id}` |
> | [List all account transactions](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-transactions.md) | `GET` | `/v1/accounts/transactions` |
> | [List all account deposit addresses](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-account-deposit-addresses.md) | `GET` | `/v1/accounts/addresses/deposit` |
> | [Get an account deposit address](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-account-deposit-address.md) | `GET` | `/v1/accounts/addresses/deposit/{id}` |
> | [List all wire bank accounts](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-wire-accounts.md) | `GET` | `/v1/banks/wires` |
> | [Get a wire bank account](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-wire-account.md) | `GET` | `/v1/banks/wires/{id}` |
> | [Get wire instructions](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-wire-account-instructions.md) | `GET` | `/v1/banks/wires/{id}/instructions` |
> | [List all ACH bank accounts](https://developers.circle.com/api-reference/digital-asset-accounts/all/list-achaccounts.md) | `GET` | `/v1/banks/ach` |
> | [Get an ACH bank account](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-achaccount.md) | `GET` | `/v1/banks/ach/{id}` |
> | [Get ACH instructions](https://developers.circle.com/api-reference/digital-asset-accounts/all/get-achaccount-instructions.md) | `GET` | `/v1/banks/ach/{id}/instructions` |

### Forward the exact bytes

The end customer signature covers the exact request body. If anything parses the
body, changes its format, or serializes it again, Circle rejects the request.
Most web frameworks parse JSON bodies by default, so read the body as raw bytes.

This Express relay authenticates your application's session, checks the route,
adds your API key, and forwards everything else unchanged:

```typescript relay.ts expandable theme={null}
import express from "express";
import type { Express, NextFunction, Request, Response } from "express";

const app: Express = express();

// Forward each allowed SDK request to Circle with your API key.
app.use(
  "/daa",
  authenticateAppSession, // Your own session check.
  express.raw({ type: () => true }), // Keep the body as raw bytes.
  async (req: Request, res: Response): Promise<void> => {
    if (DOT_SEGMENT.test(req.path) || !isAllowedRoute(req.method, req.path)) {
      res.sendStatus(404);
      return;
    }

    // Check that the request belongs to the signed-in end customer.
    const clientEntityId: string = res.locals.clientEntityId;
    const url: URL | null = await scopeToSignedInUser(req, clientEntityId);
    if (url === null) {
      res.sendStatus(404);
      return;
    }

    const headers: Record<string, string> = {
      Authorization: `Bearer ${API_KEY}`,
    };
    for (const name of FORWARDED_HEADERS) {
      const value: string | undefined = req.header(name);
      if (value !== undefined) headers[name] = value;
    }

    let upstream: globalThis.Response;
    try {
      upstream = await fetch(url, {
        method: req.method,
        headers,
        body: req.method === "GET" ? undefined : req.body,
      });
    } catch {
      res.sendStatus(502);
      return;
    }

    const body: Buffer = Buffer.from(await upstream.arrayBuffer());
    if (
      upstream.ok &&
      !(await responseBelongsTo(url.pathname, body, clientEntityId))
    ) {
      res.sendStatus(404);
      return;
    }

    res.status(upstream.status);
    upstream.headers.forEach((value: string, name: string) => {
      if (RETURNED_RESPONSE_HEADERS.includes(name)) {
        res.setHeader(name, value);
      }
    });
    res.send(body);
  },
);

app.listen(3000);

// Ownership checks. Reads and some sign-in requests don't carry the end
// customer's signature, so the relay checks them against the signed-in end
// customer. These rules cover the requests in this quickstart, and transfers and
// withdrawals. Add a rule for any other read that your application uses. The
// relay rejects every read that no rule covers.

// Returns the URL to forward, or null to reject the request.
async function scopeToSignedInUser(
  req: Request,
  clientEntityId: string,
): Promise<URL | null> {
  const url: URL = new URL(`${CIRCLE_API}${req.url}`);
  // Check the URL that the relay forwards, after parsing.
  if (url.origin !== CIRCLE_API || !isAllowedRoute(req.method, url.pathname)) {
    return null;
  }
  const path: string = url.pathname;

  if (req.method === "POST") {
    let body: {
      clientEntityId?: string;
      accountId?: string;
      custodyType?: string;
      source?: { type?: string; id?: string };
    };
    try {
      body = JSON.parse(
        Buffer.isBuffer(req.body) && req.body.length > 0
          ? req.body.toString("utf8")
          : "{}",
      );
    } catch {
      return null; // Not JSON. Circle would reject it anyway.
    }
    // Sign-in steps that aren't tied to an account.
    if (
      path === "/v1/accounts/auth/loginRequests" ||
      path === "/v1/accounts/auth/deviceKeyRegistration"
    ) {
      return url;
    }
    // Social sign-in links the user to clientEntityId permanently.
    if (path === "/v1/accounts/auth/sessions") {
      return body.clientEntityId === clientEntityId ? url : null;
    }
    // Creating a deposit address isn't signed by the end customer.
    if (path === "/v1/accounts/addresses/deposit") {
      return typeof body.accountId === "string" &&
        (await ownsAccount(body.accountId, clientEntityId))
        ? url
        : null;
    }
    // Account, transfer, and withdrawal requests. Circle checks the signature
    // only for a non-custodial account. Your API key alone can create accounts
    // and debit custodial accounts, so forward only signed requests for the
    // signed-in user's own non-custodial account.
    if (req.header("x-device-authorization") === undefined) {
      return null;
    }
    if (path === "/v1/accounts") {
      return body.custodyType === "self" &&
        body.clientEntityId === clientEntityId
        ? url
        : null;
    }
    return body.source?.type === "account" &&
      typeof body.source.id === "string" &&
      (await ownsAccount(body.source.id, clientEntityId, "self"))
      ? url
      : null;
  }

  // Lists that filter by end customer: always list the signed-in user's data.
  if (path === "/v1/accounts" || /^\/v1\/banks\/(wires|ach)$/.test(path)) {
    url.searchParams.set("clientEntityId", clientEntityId);
    return url;
  }
  // Context of the signed-in device. Signed by the end customer.
  if (path === "/v1/accounts/auth/context") {
    return url;
  }
  // A single wire or ACH bank account, or its instructions: it must be in the
  // signed-in user's list, because the response doesn't name the end customer.
  // Instructions credit the account in the accountId parameter, and Circle
  // doesn't check that it belongs to the bank account's owner, so it must be
  // one of the signed-in user's accounts too.
  const bank: RegExpMatchArray | null = path.match(
    /^\/v1\/banks\/(wires|ach)\/([^/]+)(\/instructions)?$/,
  );
  if (bank !== null) {
    const accountIds: string[] = url.searchParams.getAll("accountId");
    return (await ownsBankAccount(bank[1], bank[2], clientEntityId)) &&
      accountIds.length <= 1 &&
      (accountIds.length === 0 ||
        (await ownsAccount(accountIds[0], clientEntityId)))
      ? url
      : null;
  }
  // A single account, or its limits.
  const account: RegExpMatchArray | null = path.match(
    /^\/v1\/accounts\/([^/]+)(\/limits\/usage)?$/,
  );
  if (account !== null && !ACCOUNT_COLLECTIONS.includes(account[1])) {
    return (await ownsAccount(account[1], clientEntityId)) ? url : null;
  }
  // Lists that filter by account: require one of the user's accounts.
  if (
    /^\/v1\/accounts\/(transfers|withdrawals|deposits|transactions|addresses\/deposit)$/.test(
      path,
    )
  ) {
    // Remove filters that can widen the list beyond the account, and limit
    // the lists that accept it to the signed-in user.
    for (const name of LIST_FILTERS_TO_REMOVE) {
      url.searchParams.delete(name);
    }
    if (/^\/v1\/accounts\/(transfers|transactions)$/.test(path)) {
      url.searchParams.set("clientEntityId", clientEntityId);
    }
    const accountIds: string[] = url.searchParams.getAll("accountId");
    return accountIds.length === 1 &&
      (await ownsAccount(accountIds[0], clientEntityId))
      ? url
      : null;
  }
  // A single transfer, withdrawal, deposit, or deposit address: checked in the
  // response.
  if (SINGLE_READS.test(path)) {
    return url;
  }
  return null;
}

// Checks single reads against the account that the response names.
async function responseBelongsTo(
  path: string,
  body: Buffer,
  clientEntityId: string,
): Promise<boolean> {
  if (!SINGLE_READS.test(path)) {
    return true; // Already scoped before forwarding.
  }
  type Location = { type?: string; id?: string };
  let data: {
    source?: Location;
    destination?: Location;
    sourceAccountId?: string;
    accountId?: string;
  };
  try {
    data = JSON.parse(body.toString("utf8")).data ?? {};
  } catch {
    return false;
  }
  // The accounts that the response names. A transfer can be outgoing or
  // incoming, so either side can be the user's account.
  const accountIds: (string | undefined)[] = path.startsWith(
    "/v1/accounts/transfers/",
  )
    ? [data.source, data.destination]
        .filter((location?: Location): boolean => location?.type === "account")
        .map((location?: Location): string | undefined => location?.id)
    : path.startsWith("/v1/accounts/withdrawals/")
      ? [data.sourceAccountId]
      : path.startsWith("/v1/accounts/deposits/")
        ? [data.destination?.id]
        : [data.accountId];
  for (const accountId of accountIds) {
    if (
      accountId !== undefined &&
      (await ownsAccount(accountId, clientEntityId))
    ) {
      return true;
    }
  }
  return false;
}

async function ownsBankAccount(
  type: string,
  bankAccountId: string,
  clientEntityId: string,
): Promise<boolean> {
  const response: globalThis.Response = await fetch(
    `${CIRCLE_API}/v1/banks/${type}?clientEntityId=${encodeURIComponent(clientEntityId)}`,
    { headers: { Authorization: `Bearer ${API_KEY}` } },
  );
  if (!response.ok) {
    return false;
  }
  const list: { data?: { id?: string }[] } = await response.json();
  return (list.data ?? []).some(
    (bankAccount: { id?: string }): boolean => bankAccount.id === bankAccountId,
  );
}

async function ownsAccount(
  accountId: string,
  clientEntityId: string,
  custodyType?: "self",
): Promise<boolean> {
  const response: globalThis.Response = await fetch(
    `${CIRCLE_API}/v1/accounts/${encodeURIComponent(accountId)}`,
    { headers: { Authorization: `Bearer ${API_KEY}` } },
  );
  if (!response.ok) {
    return false;
  }
  const account: { data?: { clientEntityId?: string; custodyType?: string } } =
    await response.json();
  return (
    account.data?.clientEntityId === clientEntityId &&
    (custodyType === undefined || account.data.custodyType === custodyType)
  );
}

// Configuration and helpers.

const CIRCLE_API: string = "https://api-sandbox.circle.com";
const API_KEY: string = requireEnv("CIRCLE_API_KEY");
const FORWARDED_HEADERS: readonly string[] = [
  "content-type",
  "x-device-authorization",
  "x-registration-flow-authorization",
];
// Response headers to return. The SDK reads Link and X-Request-Id.
const RETURNED_RESPONSE_HEADERS: readonly string[] = [
  "content-type",
  "link",
  "location",
  "x-request-id",
];
// Matches "." and ".." path segments, including percent-encoded ones.
const DOT_SEGMENT: RegExp = /(^|\/)(\.|%2e){1,2}(\/|$)/i;
// Path segments under /v1/accounts that aren't account IDs.
const ACCOUNT_COLLECTIONS: readonly string[] = [
  "transfers",
  "withdrawals",
  "deposits",
  "transactions",
  "addresses",
  "auth",
];
// List filters that can widen a list beyond the account in accountId.
const LIST_FILTERS_TO_REMOVE: readonly string[] = [
  "custodyType",
  "sourceAccountId",
  "destinationAccountId",
];
const SINGLE_READS: RegExp =
  /^\/v1\/accounts\/(transfers|withdrawals|deposits|addresses\/deposit)\/[^/]+$/;

// The routes from the preceding tables. Forward nothing else.
const ALLOWED_ROUTES: ReadonlyArray<readonly [string, RegExp]> = [
  [
    "POST",
    /^\/v1\/accounts\/auth\/(loginRequests|sessions|deviceKeyRegistration)$/,
  ],
  ["GET", /^\/v1\/accounts\/auth\/context$/],
  ["POST", /^\/v1\/accounts(\/transfers|\/withdrawals|\/addresses\/deposit)?$/],
  ["GET", /^\/v1\/accounts(\/[^/]+(\/limits\/usage)?)?$/],
  ["GET", /^\/v1\/accounts\/(transfers|withdrawals|deposits)\/[^/]+$/],
  ["GET", /^\/v1\/accounts\/addresses\/deposit(\/[^/]+)?$/],
  ["GET", /^\/v1\/banks\/(wires|ach)(\/[^/]+(\/instructions)?)?$/],
];

function isAllowedRoute(method: string, path: string): boolean {
  return ALLOWED_ROUTES.some(
    ([allowedMethod, pattern]: readonly [string, RegExp]): boolean =>
      allowedMethod === method && pattern.test(path),
  );
}

// Replace with your own check that the request comes from a signed-in user of
// your application, and set res.locals.clientEntityId to that user's
// clientEntityId from your session. Until you do, the relay rejects every
// request.
function authenticateAppSession(
  req: Request,
  res: Response,
  next: NextFunction,
): void {
  const clientEntityId: string | undefined = undefined; // From your session.
  if (clientEntityId === undefined) {
    res.sendStatus(401);
    return;
  }
  res.locals.clientEntityId = clientEntityId;
  next();
}

function requireEnv(name: string): string {
  const value: string | undefined = process.env[name];
  if (value === undefined || value === "") {
    throw new Error(`Missing environment variable ${name}`);
  }
  return value;
}
```

`authenticateAppSession` is a stub that rejects every request. Replace it with
your own session check, and set `res.locals.clientEntityId` from your session.
The ownership rules cover the requests in this quickstart, in
[Send crypto transfers](https://developers.circle.com/digital-asset-accounts/howtos/crypto-transfers.md), and in
[Wire deposits and withdrawals](https://developers.circle.com/digital-asset-accounts/howtos/wire-deposits-and-withdrawals.md).
Add a rule for any other read that your application uses. The relay rejects
reads that no rule covers.

Keep ID tokens, signatures, and request and response bodies out of your logs.
Bank account responses include sensitive fields such as `billingDetails`,
`bankAddress`, and `routingNumber`.

## Step 2: Connect your application

Point the SDK at your relay, then create the SDK client.

### Send the SDK's requests to your relay

In your application, implement the SDK's `DaaTransport` interface. Pass the
bytes in `bodyBytes` through without changing them, and send the same session
credential that `authenticateAppSession` checks on your relay:

```typescript transport.ts expandable theme={null}
import type * as Daa from "@circle-fin/daa-react-native-sdk";

const RELAY_URL: string = "https://your-backend.example.com/daa";

const backendTransport: Daa.DaaTransport = {
  async send(request: Daa.DaaHttpRequest): Promise<Daa.DaaHttpResponse> {
    const query: string = request.query === null ? "" : `?${request.query}`;
    const response: Response = await fetch(
      `${RELAY_URL}${request.path}${query}`,
      {
        method: request.method,
        // yourSessionHeaders is your own function. It returns the session
        // credential that authenticateAppSession checks on your relay.
        headers: { ...request.headers, ...yourSessionHeaders() },
        body:
          request.bodyBytes === null
            ? undefined
            : new Uint8Array(request.bodyBytes),
      },
    );

    const headers: Record<string, string> = {};
    response.headers.forEach((value: string, name: string) => {
      headers[name] = value;
    });
    return {
      status: response.status,
      headers,
      bodyBytes: new Uint8Array(await response.arrayBuffer()),
    };
  },
};
```

If your transport throws during sign-in (`prepareSocialLogin`,
`completeSocialSession`, or `complete`), the SDK passes your error through
unchanged. For every other call, the SDK throws a `DaaError` whose `detail.kind`
is `transport`, and keeps your error as `cause`. Its `detail.idempotencyKey` is
the key to reuse when you retry.

### Create the SDK client

Create one `DaaClient` when your application starts:

```typescript theme={null}
import { DaaClient, DEVICE_KEY_POLICY } from "@circle-fin/daa-react-native-sdk";

const daa: DaaClient = new DaaClient({
  appId: "YOUR_APP_ID",
  environment: "sandbox",
  transport: backendTransport,
  keyPolicy: DEVICE_KEY_POLICY,
});
```

* `appId`: a stable identifier that you choose for your application, such as its
  bundle ID. Use 1 to 41 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, and `-`.
  The SDK uses it only on the device, to keep your application's authorization
  keys separate from other apps. It isn't sent to Circle. If you change it, the
  SDK can't find existing authorization keys, and end customers must sign in
  again.
* `keyPolicy`: must be `DEVICE_KEY_POLICY`. The SDK rejects any other value.

The constructor throws if a field is missing, or if `appId`, `environment`, or
`keyPolicy` is invalid. An `appId` in the wrong format throws a
`contract_violation` error.

## Step 3: Sign in the end customer

Signing in links the end customer to their onboarded `clientEntityId` and
registers an authorization key on their device. It takes two parts: social
sign-in, then key registration.

### Complete social sign-in

The SDK prepares a login and returns a nonce and a state value. Your application
runs its own Google sign-in with those values and passes the ID token back to
the SDK. The SDK doesn't show any sign-in screens.

```typescript theme={null}
import type {
  PendingDeviceRegistration,
  PreparedSocialLogin,
} from "@circle-fin/daa-react-native-sdk";

const prepared: PreparedSocialLogin = await daa.auth.prepareSocialLogin({
  provider: "GOOGLE",
});

// signInWithGoogle is your own Google sign-in call. Pass the nonce to Google,
// and send the state value as the OAuth state parameter.
const idToken: string = await signInWithGoogle({
  nonce: prepared.oidcNonce,
  state: prepared.oidcState,
});

// getClientEntityIdFromBackend is your own call. It reads clientEntityId from
// your backend session for the signed-in end customer.
const clientEntityId: string = await getClientEntityIdFromBackend();

const pending: PendingDeviceRegistration = await prepared.completeSocialSession(
  {
    idToken,
    clientEntityId,
  },
);
```

When the provider returns, check that the state value matches
`prepared.oidcState` before you continue. Keep your existing cross-site request
forgery (CSRF) protection.

Expected result: `completeSocialSession` returns a pending registration. A
prepared login expires after 5 minutes. If it does, the SDK throws a
`login_request_expired` error, and you start again with a new
`prepareSocialLogin` call. The device registration that follows expires after 5
minutes too. If the end customer backs out of the provider's sign-in, call
`await prepared.abandon()`. Discarding `prepared` doesn't clear the sign-in
state that the SDK keeps on the device.

> [!WARNING]
>
> Circle permanently links the signed-in end customer to the `clientEntityId` that
> you send. You can't change this link later. Always read `clientEntityId` from
> your own backend session for the signed-in user. Never accept it from user
> input.

### Register the authorization key

Complete the registration. The SDK registers the authorization key that it
prepared on the device during sign-in:

```typescript theme={null}
import type {
  DaaUserClient,
  DeviceRegistrationResult,
} from "@circle-fin/daa-react-native-sdk";

const registration: DeviceRegistrationResult = await pending.complete();
if (registration.status !== "ACTIVE") {
  throw new Error("Device registration did not activate");
}

const user: DaaUserClient = daa.forUser(registration.context);
```

Expected result: `registration.status` is `ACTIVE`, and
`registration.context.clientEntityId` matches the value you sent.

A new device for an existing end customer follows the same steps: social login,
then registration. So does a reinstall of your application. The new key is
added. Earlier keys stay registered.

Registration doesn't create an account. You create it in the next step.

## Step 4: Create the account

Create a non-custodial account on Arc. The device asks the end customer to
confirm with biometrics, or the screen lock on an iPhone, before the SDK signs
the request:

```typescript theme={null}
import { randomUUID } from "expo-crypto";
import type { Account } from "@circle-fin/daa-react-native-sdk";

// Store the key before you send the request, so a retry can reuse it.
const idempotencyKey: string = randomUUID();
const account: Account = await user.accounts.create({
  idempotencyKey,
  clientEntityId: registration.context.clientEntityId,
  custodyType: "self",
  type: "third_party",
  purpose: "custody",
  description: "USDC account",
  blockchain: "ARC",
});
```

* `custodyType`, `type`, and `purpose`: must be `self`, `third_party`, and
  `custody`.
* `clientEntityId`: must match the end customer linked at sign-in.
* `description`: required. A label for the account, up to 255 characters.
* `blockchain`: the account's home blockchain. Optional, and defaults to `ARC`.
  You can't change it later.
* `idempotencyKey`: a UUID v4. Store it before you send the request, and reuse
  it if you retry.

Account creation is asynchronous. The account starts with a `status.kind` of
`pending`. Read the account until it's `active`:

```typescript theme={null}
async function waitUntilActive(accountId: string): Promise<Account> {
  for (let attempt: number = 0; attempt < 30; attempt++) {
    const latest: Account = await user.accounts.get(accountId);
    if (latest.status.kind !== "pending") return latest;
    await new Promise<void>((resolve) => setTimeout(() => resolve(), 2000));
  }
  throw new Error("Account is still pending");
}

const active: Account = await waitUntilActive(account.accountId);
```

Expected result: `active.status.kind` is `active`. If it's `rejected`, the
account can't be used.

If the end customer cancels the prompt, `user.accounts.create` throws a
`DaaError` whose `detail.kind` is `local_user_authentication_required`, and
Circle doesn't receive the request. Ask the end customer to try again, and reuse
the same `idempotencyKey`.

## Step 5: Get the deposit address

After the account is active, get the address where it receives funds on its home
blockchain:

```typescript theme={null}
import { randomUUID } from "expo-crypto";
import type { AddressObject } from "@circle-fin/daa-react-native-sdk";

const depositAddress: AddressObject = await user.depositAddresses.create({
  idempotencyKey: randomUUID(),
  accountId: account.accountId,
  currency: "USD",
});
```

* `currency`: `USD` requests the USDC address. The API names stablecoins by the
  currency they track, so `USD` means USDC.

Expected result: `depositAddress.address` holds the account's address, and
`depositAddress.chain` names its home blockchain.

This call doesn't need an end customer signature, because it doesn't move funds.
It returns the address of the account's home wallet. It doesn't create a new
address. Send only USDC on the home blockchain to this address.

## Test your integration

Before you build on the account, check that the relay and the account behave as
expected in sandbox:

* **The relay rejects other routes**: send a request for a route that isn't in
  the table, such as `POST /daa/v1/addresses/recipient`, through your relay.
  Expect HTTP `404` from your relay, with no request sent to Circle.
* **The relay forwards writes only for the user's non-custodial account**: sign
  in as one end customer. Through your relay, send a transfer whose `source` is
  another end customer's account. Then send one whose `source` is a custodial
  account. Expect HTTP `404` from your relay both times, with no request sent to
  Circle.
* **The relay keeps end customers apart**: sign in as one end customer, and
  request another end customer's account through your relay, such as
  `GET /daa/v1/accounts/{otherAccountId}`. Expect HTTP `404` from your relay. Do
  the same for wire instructions on one of your own bank accounts with another
  end customer's account, such as
  `GET /daa/v1/banks/wires/{bankAccountId}/instructions?accountId={otherAccountId}`.
* **The relay forwards bytes unchanged**: create the account in Step 4. If any
  part of your relay changes the body, Circle rejects the request and the SDK
  throws a `contract_violation` error.
* **The account receives funds**: get testnet USDC on Arc Testnet from the
  [Circle Faucet](https://faucet.circle.com), and send it to the deposit address
  from Step 5. Then check the address on an Arc Testnet block explorer.

## Next steps

The account is ready to receive funds. From here:

* To send funds from the account, see
  [Send crypto transfers](https://developers.circle.com/digital-asset-accounts/howtos/crypto-transfers.md).
* To withdraw to a bank account, see
  [Wire deposits and withdrawals](https://developers.circle.com/digital-asset-accounts/howtos/wire-deposits-and-withdrawals.md#step-5-withdraw-to-a-bank-account).
* To handle SDK errors, see [Errors](https://developers.circle.com/sdks/daa/react-native-sdk.md#errors).
* Before you go live, see
  [Going to production](https://developers.circle.com/digital-asset-accounts/references/going-to-production.md).
