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

> ## Agent Instructions
> Before reading these docs, install the Circle Skill for Arc to get pre-loaded context: use-arc (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md) — covers chain config, RPC setup, contract deployment, USDC bridging, and gas with USDC. 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.
> USDC is the gas token. Arc uses USDC for gas fees, not ETH. Configure gas payment in USDC when submitting transactions.
> Read EVM differences before writing Arc code. Arc targets the Osaka EVM baseline. Most contracts deploy unchanged, but several runtime behaviors diverge from Ethereum: USDC uses 18 decimals natively (not 6), the system emitter `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` logs all USDC Transfer events, the mempool enforces a 20 Gwei `maxFeePerGas` floor, blocklist reverts consume gas without a receipt, and `address(0)` sends revert rather than succeed. https://docs.arc.io/arc/references/evm-differences.md is the canonical reference for all Arc-specific runtime behaviors. Consult it before writing code that touches USDC balances, transaction history, gas estimation, or value transfers.
> Sub-second finality. Transactions are final in under 1 second. No need to wait for multiple block confirmations.
> Use App Kits for multichain payment and liquidity capabilities. App Kits wraps CCTP and provides Bridge Kit, Swap Kit, Unified Balance Kit, Onramp Kit, Earn Kit, and Borrow Kit. Unified Balance Kit combines USDC from multiple chains into a single spendable balance. Earn Kit integrates earn opportunities into your app. Borrow Kit lets users borrow USDC against cirBTC collateral on Arc.
> Use Arc Foundry for contract development. Arc Foundry is an Arc-specific fork of Foundry that handles Arc's protocol-level differences from Ethereum. It provides arc-forge, arc-cast, and arc-anvil. Install it before deploying contracts: https://docs.arc.io/arc/tutorials/install-arc-foundry.md.
> Arc is available on both Testnet and Mainnet. See https://docs.arc.io/arc/references/connect-to-arc.md for RPC endpoints and https://faucet.circle.com for testnet tokens.
> Always check Contract Addresses: https://docs.arc.io/arc/references/contract-addresses.md
> Building beyond Arc? Circle offers skills for the full platform: use-usdc (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md), use-circle-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md), use-developer-controlled-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md), use-user-controlled-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md), use-modular-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md), use-gateway (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md), use-smart-contract-platform (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md). Full Circle developer docs: https://developers.circle.com/llms.txt.

# Customize session minting

> Add authentication, customize error handling, or mint sessions from a host that does not use the Fetch API standard, such as Express or Fastify.

The `createSessionRouteHandler` shipped with the App Kit SDK is a drop-in
session route for any host that uses the Fetch API standard (Next.js, Hono,
Cloudflare Workers, Bun, Deno, modern Node). The default handler is
unauthenticated. Most apps need to customize it in these areas:

* Authenticate so only signed-in users can mint sessions.
* Log or inspect session minting failures.
* Mint sessions from a host that does not use the Fetch API.

## Add authentication

On Fetch-compatible hosts, create the handler once with
`createSessionRouteHandler` and register it as `POST /api/onramp/sessions` in
your router. See the
[embed widget quickstart](/app-kit/quickstarts/onramp-embed-widget) for a full
example.

```typescript theme={null}
import {
  createAppServerKit,
  createSessionRouteHandler,
} from "@circle-fin/app-kit/server";

const server = createAppServerKit({
  onramp: { apiKey: process.env.CIRCLE_API_KEY! },
});

const handleOnrampSession = createSessionRouteHandler(server.onramp);
```

Add an `authorize` callback. The callback runs before the body is validated and
can return `true` (allow), `false` (reject with `401`), or throw a `KitError`
(rejected with the corresponding status):

```typescript theme={null}
import { auth } from "@/lib/auth";

const handleOnrampSession = createSessionRouteHandler(server.onramp, {
  authorize: async (request) => {
    const session = await auth(request);
    return session?.user != null;
  },
});
```

You can also validate that the request body matches the signed-in user:

```typescript theme={null}
const handleOnrampSession = createSessionRouteHandler(server.onramp, {
  authorize: async (request) => {
    const session = await auth(request);
    if (!session?.user) return false;

    const body = await request.clone().json();
    return body.appUserId === session.user.id;
  },
});
```

The handler's body validation runs after `authorize` returns, so an
unauthenticated request is rejected with `401` before any payload is parsed.

## Customize error handling

`createSessionRouteHandler` maps thrown errors to HTTP status codes
automatically. Using the same `server` and `handleOnrampSession` from above,
pass an `onError` callback:

```typescript theme={null}
const handleOnrampSession = createSessionRouteHandler(server.onramp, {
  onError: (error, request) => {
    logger.error("onramp session mint failed", {
      url: request.url,
      error,
    });
  },
});
```

`onError` runs after the error is mapped to a response, so the response is still
returned. The callback is for observability. It does not change the status code
or body.

See the [error handling reference](/app-kit/references/onramp-error-handling)
for the full error to status mapping.

## Use a non-Fetch host

`createSessionRouteHandler` only works on runtimes that pass standard `Request`
objects. Express and Fastify pass their own request and response objects, so
call `server.onramp.createSession()` directly instead. You're now responsible
for the error mapping the Fetch handler did automatically.

Map `KitError.type` to HTTP status the same way `createSessionRouteHandler`
does. See the
[HTTP status code mapping](/app-kit/references/onramp-error-handling#http-status-code-mapping)
for the full mapping. Shape error response bodies to match your API.

<Tabs>
  <Tab title="Express">
    ```typescript theme={null}
    import express from "express";
    import { createAppServerKit, KitError } from "@circle-fin/app-kit/server";

    const server = createAppServerKit({
      onramp: { apiKey: process.env.CIRCLE_API_KEY! },
    });

    const app = express();
    app.use(express.json());

    app.post("/api/onramp/sessions", async (req, res) => {
      try {
        const session = await server.onramp.createSession({
          appUserId: req.body.appUserId,
          destinationAddress: req.body.destinationAddress,
        });
        res.setHeader("Cache-Control", "no-store");
        res.json(session);
      } catch (error) {
        if (error instanceof KitError) {
          let status = 500;
          switch (error.type) {
            case "INPUT":
              status = 400;
              break;
            case "RATE_LIMIT":
              status = 429;
              break;
            case "NETWORK":
              status = 504;
              break;
            case "SERVICE":
            case "RPC":
              status = 502;
              break;
          }
          return res.status(status).json({ message: error.message });
        }
        return res.sendStatus(500);
      }
    });
    ```
  </Tab>

  <Tab title="Fastify">
    ```typescript theme={null}
    import Fastify from "fastify";
    import { createAppServerKit, KitError } from "@circle-fin/app-kit/server";

    const server = createAppServerKit({
      onramp: { apiKey: process.env.CIRCLE_API_KEY! },
    });

    const app = Fastify();

    app.post("/api/onramp/sessions", async (request, reply) => {
      try {
        const body = request.body as {
          appUserId: string;
          destinationAddress: string;
        };
        const session = await server.onramp.createSession({
          appUserId: body.appUserId,
          destinationAddress: body.destinationAddress,
        });
        reply.header("Cache-Control", "no-store");
        return session;
      } catch (error) {
        if (error instanceof KitError) {
          let status = 500;
          switch (error.type) {
            case "INPUT":
              status = 400;
              break;
            case "RATE_LIMIT":
              status = 429;
              break;
            case "NETWORK":
              status = 504;
              break;
            case "SERVICE":
            case "RPC":
              status = 502;
              break;
          }
          return reply.status(status).send({ message: error.message });
        }
        return reply.status(500).send();
      }
    });
    ```
  </Tab>
</Tabs>

Add authentication in front of this route, like any other endpoint in your app.

## Limit the tokens and blockchains the widget supports

By default, the Onramp widget shows its full catalog of supported tokens and
blockchains. If your app only handles a subset, pass an `assets` object on the
session request to narrow what the widget's selector displays.

```typescript theme={null}
const session = await kit.onramp.fetchSession({
  url: "/api/onramp/sessions",
  body: {
    appUserId: "user-123",
    destinationAddress: "USER_WALLET_ADDRESS",
    assets: {
      tokens: ["USDC"],
      chains: ["arc"],
    },
  },
});
```

You can set any of three fields:

* `tokens`: an array of token symbols such as `USDC`, `EURC`, or `ETH`. The
  widget shows those tokens on every blockchain that supports them.
* `chains`: an array of blockchains, matched by either the network id (`arc`,
  `base`, `ethereum`) or the display label (`Arc`, `Base`, `Ethereum`). Matching
  is case-insensitive. The widget shows every supported token, restricted to
  those blockchains.
* `pairs`: an array of exact token and blockchain combinations. Use this when
  you need finer control than `tokens` and `chains` allow.

  ```typescript theme={null}
  assets: {
    pairs: [
      { token: "USDC", chain: "arc" },
      { token: "EURC", chain: "base" },
    ],
  }
  ```

If you set more than one field, the widget only shows options that match every
field you set. Omit `assets` to show the full set of supported tokens and
blockchains. See
[Supported blockchains and tokens](/app-kit/references/supported-blockchains)
for the current list.

<Note>
  `assets` scopes what the selector displays. It doesn't override the widget's
  eligibility, geo, or quote logic. A token or blockchain listed in `assets` can
  still be unavailable for a specific user if they're geo-blocked or otherwise
  ineligible.
</Note>

## Allow your page to embed the widget

The widget page enforces a `frame-ancestors` Content Security Policy so only
approved parent sites can embed it. The allowlist is built from a
`referrerDomain` you set when constructing the server kit.

Pass it as a bare hostname on `createAppServerKit`:

```typescript theme={null}
const server = createAppServerKit({
  onramp: {
    apiKey: process.env.CIRCLE_API_KEY!,
    referrerDomain: "your.domain.com",
  },
});
```

Keep the following in mind when setting `referrerDomain`:

* **Format:** Use a single bare hostname such as `app.example.com` or
  `localhost`. Don't include a scheme, port, or path.
* **Source:** Derive `referrerDomain` server-side from your app's
  per-environment config. A browser-supplied `Origin` or `Referer` header would
  let attackers widen the allowlist.
* **Sandbox:** `referrerDomain` isn't enforced. Set it anyway to verify your
  wiring before promoting to production.
* **Production:** Debit card, Apple Pay, and Google Pay flows fail with a 403 if
  the domain isn't registered in your KYB's `web_url` entries.
