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

# Adapter setups

> Configure Viem, Ethers, Solana, or Circle Wallets adapters for the App Kit SDK

The App Kit SDK uses adapters to connect SDK calls to a client and signer. The
client reads blockchain data and submits signed transactions while the signer
authorizes transactions from a private key, browser wallet, or wallet provider.
The following adapters are supported:

* [`viem`](https://viem.sh/) v2 for EVM blockchains
* [`ethers`](https://ethers.org/) v6 for EVM blockchains
* [`solana`](https://solana.com/) for the Solana blockchain
* [`circle-wallets`](https://developers.circle.com/wallets) for Circle-managed
  developer-controlled and user-controlled wallets

<Note>
  Before setting up an adapter, you should already have created the wallet,
  account, API keys, and policies in your wallet provider.
</Note>

Pick your adapter to see its setup options.

<Tabs>
  <Tab title="Viem">
    Use the Viem adapter for EVM apps that use `viem` accounts or wallet clients.

    ## Server-side wallet

    Use a server-side wallet when your backend signs transactions. The signer can be
    a private key or a wallet provider.

    <Tabs>
      <Tab title="Private key">
        Create one adapter from your wallet private key. The adapter works across EVM
        blockchains and uses built-in public RPC endpoints.

        <Note>
          Default RPC URLs are shared and may be rate-limited. For a more stable
          connection, configure a [custom RPC](#custom-rpc).
        </Note>

        ```typescript TypeScript theme={null}
        import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2";

        const adapter = createViemAdapterFromPrivateKey({
          privateKey: process.env.PRIVATE_KEY as string,
        });
        ```
      </Tab>

      <Tab title="Turnkey">
        Use Turnkey Company Wallets with the Viem adapter for backend signing. The setup
        uses `@turnkey/viem` to create a Turnkey-backed `viem` account, then passes that
        account to the Viem adapter.

        Set these environment variables from your Turnkey account:

        ```text .env theme={null}
        TURNKEY_API_PUBLIC_KEY=YOUR_TURNKEY_API_PUBLIC_KEY
        TURNKEY_API_PRIVATE_KEY=YOUR_TURNKEY_API_PRIVATE_KEY
        TURNKEY_ORGANIZATION_ID=YOUR_TURNKEY_ORGANIZATION_ID
        TURNKEY_WALLET_ADDRESS=YOUR_TURNKEY_ETHEREUM_WALLET_ADDRESS
        ```

        Adapter setup:

        ```typescript TypeScript theme={null}
        import { AppKit } from "@circle-fin/app-kit";
        import { ViemAdapter } from "@circle-fin/adapter-viem-v2";
        import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";
        import { createAccount } from "@turnkey/viem";
        import { createPublicClient, createWalletClient, http } from "viem";

        const turnkey = new TurnkeyServerSDK({
          apiBaseUrl: "https://api.turnkey.com",
          apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!,
          apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!,
          defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
        });

        const account = await createAccount({
          client: turnkey.apiClient(),
          organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
          signWith: process.env.TURNKEY_WALLET_ADDRESS!,
        });

        const appKit = new AppKit();
        const supportedChains = (await appKit.getSupportedChains()).filter(
          (chain) => chain.type === "evm",
        );

        const adapter = new ViemAdapter(
          {
            getPublicClient: ({ chain }) =>
              createPublicClient({
                chain,
                transport: http(),
              }),
            getWalletClient: ({ chain }) =>
              createWalletClient({
                account,
                chain,
                transport: http(),
              }),
          },
          {
            addressContext: "user-controlled",
            supportedChains,
          },
        );
        ```

        **What to know about this setup**:

        * Turnkey supports both `viem` and `ethers`. This setup uses `viem` because
          `@turnkey/viem` creates a Turnkey-backed account directly, which keeps the
          adapter setup shorter.
        * Use this setup only on your backend. `TURNKEY_API_PRIVATE_KEY` authenticates
          requests to Turnkey and must not be exposed in browser code.
        * For crosschain flows, create one adapter per blockchain if your wallet client
          or RPC setup is blockchain-specific.
      </Tab>
    </Tabs>

    ## Browser wallet

    This setup expects a wallet extension in the browser (for example
    `window.ethereum` or `window.solana`), not a Node.js server. You can use wallets
    like [MetaMask](https://metamask.io/) or [Phantom](https://phantom.com/):

    <Tabs>
      <Tab title="MetaMask (EIP-6963)">
        Use EIP-6963 to discover injected browser wallets, select a provider, request
        access to the user's accounts, then pass the provider to the Viem adapter.

        The example selects MetaMask by reverse-DNS identifier. To use any injected
        wallet, omit `requiredRdns`.

        ```typescript TypeScript theme={null}
        import {
          createViemAdapterFromProvider,
          type CreateViemAdapterFromProviderParams,
        } from "@circle-fin/adapter-viem-v2";

        type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"];

        type EIP6963ProviderDetail = {
          info: {
            uuid: string;
            name: string;
            icon: string;
            rdns: string;
          };
          provider: BrowserWalletProvider;
        };

        declare global {
          interface WindowEventMap {
            "eip6963:announceProvider": CustomEvent<EIP6963ProviderDetail>;
          }
        }

        async function getInjectedWalletProvider(
          requiredRdns?: string,
        ): Promise<BrowserWalletProvider> {
          const providers = new Map<string, EIP6963ProviderDetail>();

          const onAnnounce = ((event: CustomEvent<EIP6963ProviderDetail>) => {
            providers.set(event.detail.info.uuid, event.detail);
          }) as EventListener;

          window.addEventListener("eip6963:announceProvider", onAnnounce);
          window.dispatchEvent(new Event("eip6963:requestProvider"));
          await new Promise((resolve) => window.setTimeout(resolve, 250));
          window.removeEventListener("eip6963:announceProvider", onAnnounce);

          const provider = requiredRdns
            ? [...providers.values()].find(({ info }) => info.rdns === requiredRdns)
                ?.provider
            : [...providers.values()][0]?.provider;

          if (!provider) {
            throw new Error(
              requiredRdns
                ? `No EIP-6963 wallet found for ${requiredRdns}`
                : "No EIP-6963 browser wallet found",
            );
          }

          return provider;
        }

        const provider = await getInjectedWalletProvider("io.metamask");
        // For any injected wallet, use:
        // const provider = await getInjectedWalletProvider();

        await provider.request({
          method: "eth_requestAccounts",
          params: undefined,
        });

        const adapter = await createViemAdapterFromProvider({
          provider,
        });
        ```
      </Tab>

      <Tab title="window.ethereum">
        Use `window.ethereum` only when the wallet does not support EIP-6963 or your app
        intentionally accepts the default injected provider.

        ```typescript TypeScript theme={null}
        import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";
        import type { EIP1193Provider } from "viem";

        declare global {
          interface Window {
            ethereum?: EIP1193Provider;
          }
        }

        if (!window.ethereum) {
          throw new Error("No wallet provider found");
        }

        const adapter = await createViemAdapterFromProvider({
          provider: window.ethereum,
        });
        ```
      </Tab>
    </Tabs>

    ## Custom RPC

    By default, adapters use built-in RPC endpoints, which may be rate-limited or
    unreliable. Override them with your own provider.
    [Alchemy](https://www.alchemy.com/), [QuickNode](https://www.quicknode.com/),
    and [chainlist.org](https://chainlist.org/) are common places to source
    endpoints. This example uses Alchemy.

    The `getPublicClient`/`getWalletClient` override pairs with any signer setup
    that uses `viem`, such as a private key or browser wallet.

    ```typescript TypeScript theme={null}
    import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2";
    import { EthereumSepolia, ArcTestnet } from "@circle-fin/app-kit/chains";
    import { createPublicClient, createWalletClient, http } from "viem";

    const RPC_BY_CHAIN_NAME: Record<string, string> = {
      [EthereumSepolia.name]: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
      [ArcTestnet.name]: `https://arc-testnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
    };

    const adapter = createViemAdapterFromPrivateKey({
      privateKey: process.env.PRIVATE_KEY as string,
      getPublicClient: ({ chain }) => {
        const rpcUrl = RPC_BY_CHAIN_NAME[chain.name];
        if (!rpcUrl) {
          throw new Error(`No RPC configured for chain: ${chain.name}`);
        }
        return createPublicClient({
          chain,
          transport: http(rpcUrl, {
            retryCount: 3,
            timeout: 10000,
          }),
        });
      },
      getWalletClient: ({ chain, account }) => {
        const rpcUrl = RPC_BY_CHAIN_NAME[chain.name];
        if (!rpcUrl) {
          throw new Error(`No RPC configured for chain: ${chain.name}`);
        }
        return createWalletClient({
          account,
          chain,
          transport: http(rpcUrl, {
            retryCount: 3,
            timeout: 10000,
          }),
        });
      },
    });
    ```
  </Tab>

  <Tab title="Ethers">
    Use the Ethers adapter for EVM apps that use `ethers` providers or signers.

    <Info>
      Ethers only supports read operations on Borrow. Use Viem or Circle Wallets to
      open or modify loans.
    </Info>

    ## Server-side wallet

    Use a server-side wallet when your backend signs transactions. The signer can be
    a private key or a wallet provider.

    <Tabs>
      <Tab title="Private key">
        Create one adapter from your wallet private key. The adapter works across EVM
        blockchains and uses built-in public RPC endpoints.

        <Note>
          Default RPC URLs are shared and may be rate-limited. For a more stable
          connection, configure a custom RPC.
        </Note>

        ```typescript TypeScript theme={null}
        import { createEthersAdapterFromPrivateKey } from "@circle-fin/adapter-ethers-v6";

        const adapter = createEthersAdapterFromPrivateKey({
          privateKey: process.env.PRIVATE_KEY as string,
        });
        ```
      </Tab>

      <Tab title="Privy">
        Use Privy server wallets with the Ethers adapter when Privy signs for your
        backend. Privy server wallets sign through the Privy wallet API by `walletId`,
        so the setup needs a small custom `ethers` signer.

        Set these environment variables from your Privy app and server wallet:

        ```text .env theme={null}
        PRIVY_APP_ID=YOUR_PRIVY_APP_ID
        PRIVY_APP_SECRET=YOUR_PRIVY_APP_SECRET
        PRIVY_AUTHORIZATION_PRIVATE_KEY=YOUR_PRIVY_AUTHORIZATION_PRIVATE_KEY
        PRIVY_WALLET_ID=YOUR_PRIVY_SERVER_WALLET_ID
        PRIVY_WALLET_ADDRESS=YOUR_PRIVY_SERVER_WALLET_ADDRESS
        ```

        Use a server-owned Privy wallet. `PRIVY_AUTHORIZATION_PRIVATE_KEY` must belong
        to the wallet owner or an authorized signer for `PRIVY_WALLET_ID`.

        Create the `PrivyServerSigner.ts` helper first. The adapter setup imports this
        helper in the next step.

        ```typescript TypeScript expandable theme={null}
        import { PrivyClient, type Hex, type Quantity } from "@privy-io/server-auth";
        import { ethers } from "ethers";

        interface PrivyServerSignerOptions {
          walletId: string;
          walletAddress: string;
          privy: PrivyClient;
          provider: ethers.Provider;
        }

        type JsonSafe =
          string | number | boolean | null | JsonSafe[] | { [key: string]: JsonSafe };

        type PrivySignTransactionRequest = Parameters<
          PrivyClient["walletApi"]["ethereum"]["signTransaction"]
        >[0];

        type PrivySignMessageRequest = Parameters<
          PrivyClient["walletApi"]["ethereum"]["signMessage"]
        >[0];

        type PrivySignTypedDataRequest = Parameters<
          PrivyClient["walletApi"]["ethereum"]["signTypedData"]
        >[0];

        export class PrivyServerSigner extends ethers.AbstractSigner {
          private walletId: string;
          private walletAddress: string;
          private privy: PrivyClient;

          constructor(options: PrivyServerSignerOptions) {
            super(options.provider);
            this.walletId = options.walletId;
            this.walletAddress = options.walletAddress;
            this.privy = options.privy;
          }

          connect(provider: ethers.Provider): PrivyServerSigner {
            return new PrivyServerSigner({
              walletId: this.walletId,
              walletAddress: this.walletAddress,
              privy: this.privy,
              provider,
            });
          }

          async getAddress(): Promise<string> {
            return this.walletAddress;
          }

          async signTransaction(tx: ethers.TransactionRequest): Promise<string> {
            const toQuantity = (
              value: ethers.BigNumberish | null | undefined,
            ): Quantity | undefined =>
              value != null ? (ethers.toBeHex(value) as Hex) : undefined;

            const toHex = (value: unknown): Hex | undefined =>
              value != null ? (value.toString() as Hex) : undefined;

            const request: PrivySignTransactionRequest = {
              walletId: this.walletId,
              chainType: "ethereum",
              transaction: {
                to: toHex(tx.to),
                nonce: tx.nonce != null ? Number(tx.nonce) : undefined,
                chainId: tx.chainId != null ? Number(tx.chainId) : undefined,
                data: toHex(tx.data),
                value: toQuantity(tx.value),
                type: (tx.type ?? 2) as 0 | 1 | 2,
                gasLimit: toQuantity(tx.gasLimit),
                maxFeePerGas: toQuantity(tx.maxFeePerGas),
                maxPriorityFeePerGas: toQuantity(tx.maxPriorityFeePerGas),
              },
            };

            const { signedTransaction } =
              await this.privy.walletApi.ethereum.signTransaction(request);

            return signedTransaction;
          }

          async signMessage(message: string | Uint8Array): Promise<string> {
            const request: PrivySignMessageRequest = {
              walletId: this.walletId,
              chainType: "ethereum",
              message: typeof message === "string" ? message : ethers.hexlify(message),
            };

            const { signature } =
              await this.privy.walletApi.ethereum.signMessage(request);

            return signature;
          }

          async signTypedData(
            domain: ethers.TypedDataDomain,
            types: Record<string, ethers.TypedDataField[]>,
            value: Record<string, unknown>,
          ): Promise<string> {
            const toJsonSafe = (input: unknown): JsonSafe => {
              if (typeof input === "bigint") {
                return input.toString();
              }
              if (Array.isArray(input)) {
                return input.map(toJsonSafe);
              }
              if (input && typeof input === "object") {
                return Object.fromEntries(
                  Object.entries(input).map(([key, nestedValue]) => [
                    key,
                    toJsonSafe(nestedValue),
                  ]),
                );
              }
              return input as JsonSafe;
            };

            const primaryType =
              Object.keys(types).find((key) => key !== "EIP712Domain") ?? "";

            const jsonSafeDomain = toJsonSafe(domain) as Record<string, unknown>;
            const jsonSafeMessage = toJsonSafe(value) as Record<string, unknown>;

            const request: PrivySignTypedDataRequest = {
              walletId: this.walletId,
              typedData: {
                domain: jsonSafeDomain,
                types,
                message: jsonSafeMessage,
                primaryType,
              },
            };

            const { signature } =
              await this.privy.walletApi.ethereum.signTypedData(request);

            return signature;
          }
        }
        ```

        After adding the helper, create the adapter:

        ```typescript TypeScript theme={null}
        import { AppKit } from "@circle-fin/app-kit";
        import { EthersAdapter } from "@circle-fin/adapter-ethers-v6";
        import { PrivyClient } from "@privy-io/server-auth";
        import { ethers } from "ethers";
        import { PrivyServerSigner } from "./PrivyServerSigner";

        const privy = new PrivyClient(
          process.env.PRIVY_APP_ID!,
          process.env.PRIVY_APP_SECRET!,
          {
            walletApi: {
              authorizationPrivateKey: process.env.PRIVY_AUTHORIZATION_PRIVATE_KEY!,
            },
          },
        );

        const appKit = new AppKit();
        const supportedChains = (await appKit.getSupportedChains()).filter(
          (chain) => chain.type === "evm",
        );
        const arcTestnet = supportedChains.find(
          (chain) => chain.chain === "Arc_Testnet",
        );

        if (!arcTestnet) {
          throw new Error("Arc Testnet is not supported");
        }

        const provider = new ethers.JsonRpcProvider(arcTestnet.rpcEndpoints[0]);

        const signer = new PrivyServerSigner({
          walletId: process.env.PRIVY_WALLET_ID!,
          walletAddress: process.env.PRIVY_WALLET_ADDRESS!,
          privy,
          provider,
        });

        const adapter = new EthersAdapter(
          {
            signer,
            getProvider: ({ chain }) =>
              new ethers.JsonRpcProvider(chain.rpcEndpoints[0]),
          },
          {
            addressContext: "user-controlled",
            supportedChains,
          },
        );
        ```

        **What to know about this setup**:

        * Privy's stock `createEthersSigner` is built for user-linked wallets. Server
          wallets sign by `walletId`, which is why this setup uses `PrivyServerSigner`.
        * User-owned embedded wallets are not server wallets. For this setup, create a
          server-owned wallet or add your backend key as an authorized signer.
        * Automatic chain switching does not support this custom signer. For crosschain
          flows, create one adapter per blockchain.
      </Tab>
    </Tabs>

    ## Browser wallet

    This setup expects a wallet extension in the browser (for example
    `window.ethereum` or `window.solana`), not a Node.js server. You can use wallets
    like [MetaMask](https://metamask.io/) or [Phantom](https://phantom.com/):

    <Note>
      In crosschain flows, use the Viem adapter for injected browser wallets. The
      Ethers adapter can work for same-chain browser wallet operations, but the
      `BrowserProvider` from `ethers` can throw a network-change error when the wallet
      switches blockchains during a bridge or other crosschain flow.
    </Note>

    <Tabs>
      <Tab title="MetaMask (EIP-6963)">
        Use EIP-6963 to discover injected browser wallets, select a provider, request
        access to the user's accounts, then pass the provider to the Ethers adapter.

        The example selects MetaMask by reverse-DNS identifier. To use any injected
        wallet, omit `requiredRdns`.

        ```typescript TypeScript theme={null}
        import {
          createEthersAdapterFromProvider,
          type CreateEthersAdapterFromProviderParams,
        } from "@circle-fin/adapter-ethers-v6";

        type BrowserWalletProvider = CreateEthersAdapterFromProviderParams["provider"];

        type EIP6963ProviderDetail = {
          info: {
            uuid: string;
            name: string;
            icon: string;
            rdns: string;
          };
          provider: BrowserWalletProvider;
        };

        declare global {
          interface WindowEventMap {
            "eip6963:announceProvider": CustomEvent<EIP6963ProviderDetail>;
          }
        }

        async function getInjectedWalletProvider(
          requiredRdns?: string,
        ): Promise<BrowserWalletProvider> {
          const providers = new Map<string, EIP6963ProviderDetail>();

          const onAnnounce = ((event: CustomEvent<EIP6963ProviderDetail>) => {
            providers.set(event.detail.info.uuid, event.detail);
          }) as EventListener;

          window.addEventListener("eip6963:announceProvider", onAnnounce);
          window.dispatchEvent(new Event("eip6963:requestProvider"));
          await new Promise((resolve) => window.setTimeout(resolve, 250));
          window.removeEventListener("eip6963:announceProvider", onAnnounce);

          const provider = requiredRdns
            ? [...providers.values()].find(({ info }) => info.rdns === requiredRdns)
                ?.provider
            : [...providers.values()][0]?.provider;

          if (!provider) {
            throw new Error(
              requiredRdns
                ? `No EIP-6963 wallet found for ${requiredRdns}`
                : "No EIP-6963 browser wallet found",
            );
          }

          return provider;
        }

        const provider = await getInjectedWalletProvider("io.metamask");
        // For any injected wallet, use:
        // const provider = await getInjectedWalletProvider();

        await provider.request({
          method: "eth_requestAccounts",
          params: undefined,
        });

        const adapter = await createEthersAdapterFromProvider({
          provider,
        });
        ```
      </Tab>

      <Tab title="window.ethereum">
        Use `window.ethereum` only when the wallet does not support EIP-6963 or your app
        intentionally accepts the default injected provider.

        ```typescript TypeScript theme={null}
        import { createEthersAdapterFromProvider } from "@circle-fin/adapter-ethers-v6";
        import type { Eip1193Provider } from "ethers";

        declare global {
          interface Window {
            ethereum?: Eip1193Provider;
          }
        }

        if (!window.ethereum) {
          throw new Error("No wallet provider found");
        }

        const adapter = await createEthersAdapterFromProvider({
          provider: window.ethereum,
        });
        ```
      </Tab>
    </Tabs>

    ## Custom RPC

    By default, adapters use built-in RPC endpoints, which may be rate-limited or
    unreliable. Override them with your own provider.
    [Alchemy](https://www.alchemy.com/), [QuickNode](https://www.quicknode.com/),
    and [chainlist.org](https://chainlist.org/) are common places to source
    endpoints. This example uses Alchemy.

    The `getProvider` override pairs with any signer setup that uses `ethers`, such
    as a private key or browser wallet.

    ```typescript TypeScript theme={null}
    import { createEthersAdapterFromPrivateKey } from "@circle-fin/adapter-ethers-v6";
    import { EthereumSepolia, ArcTestnet } from "@circle-fin/app-kit/chains";
    import { JsonRpcProvider } from "ethers";

    const RPC_BY_CHAIN_NAME: Record<string, string> = {
      [EthereumSepolia.name]: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
      [ArcTestnet.name]: `https://arc-testnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
    };

    const adapter = createEthersAdapterFromPrivateKey({
      privateKey: process.env.PRIVATE_KEY as string,
      getProvider: ({ chain }) => {
        const rpcUrl = RPC_BY_CHAIN_NAME[chain.name];
        if (!rpcUrl) {
          throw new Error(`No RPC configured for chain: ${chain.name}`);
        }
        return new JsonRpcProvider(rpcUrl);
      },
    });
    ```
  </Tab>

  <Tab title="Solana">
    Use the Solana adapter for Solana wallets and RPC clients.

    ## Server-side wallet

    Use a server-side wallet when your backend signs transactions.

    ### Private key

    Create one adapter from your Solana private key. Solana accepts Base58, Base64,
    or JSON array format private keys.

    <Note>
      Default RPC URLs are shared and may be rate-limited. For a more stable
      connection, configure a custom RPC.
    </Note>

    ```typescript TypeScript theme={null}
    import { createSolanaKitAdapterFromPrivateKey } from "@circle-fin/adapter-solana-kit";

    const adapter = createSolanaKitAdapterFromPrivateKey({
      privateKey: process.env.PRIVATE_KEY as string,
    });
    ```

    ## Browser wallet

    This setup expects a wallet extension in the browser (for example
    `window.ethereum` or `window.solana`), not a Node.js server. You can use wallets
    like [MetaMask](https://metamask.io/) or [Phantom](https://phantom.com/):

    ```typescript TypeScript theme={null}
    import { createSolanaKitAdapterFromProvider } from "@circle-fin/adapter-solana-kit";

    type SolanaKitWalletProvider = Parameters<
      typeof createSolanaKitAdapterFromProvider
    >[0]["provider"];

    declare global {
      interface Window {
        solana?: SolanaKitWalletProvider;
      }
    }

    if (!window.solana) {
      throw new Error("No Solana wallet provider found");
    }

    const adapter = await createSolanaKitAdapterFromProvider({
      provider: window.solana,
    });
    ```

    ## Custom RPC

    By default, adapters use built-in RPC endpoints, which may be rate-limited or
    unreliable. Override them with your own provider.
    [Alchemy](https://www.alchemy.com/), [QuickNode](https://www.quicknode.com/),
    and [chainlist.org](https://chainlist.org/) are common places to source
    endpoints. This example uses Alchemy.

    The `getRpc` override pairs with any Solana signer setup, such as a private key
    or browser wallet.

    ```typescript TypeScript theme={null}
    import { createSolanaKitAdapterFromPrivateKey } from "@circle-fin/adapter-solana-kit";
    import { createSolanaRpc } from "@solana/kit";

    const adapter = createSolanaKitAdapterFromPrivateKey({
      privateKey: process.env.SOLANA_PRIVATE_KEY as string,
      getRpc: () =>
        createSolanaRpc(
          `https://solana-devnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
        ),
    });
    ```
  </Tab>

  <Tab title="Circle Wallets">
    <Badge color="blue">server-side only</Badge>

    Use the Circle Wallets adapter if you already
    [manage wallets through Circle](https://developers.circle.com/wallets). The
    adapter is suited for backend apps and works with
    [developer-controlled](https://developers.circle.com/wallets/dev-controlled) or
    [user-controlled](https://developers.circle.com/wallets/user-controlled) wallets
    so you can use the App Kit SDK on
    [supported blockchains](/app-kit/references/supported-blockchains#adapters)
    without managing private keys yourself.

    Pick the custody model that matches your app.

    <Tabs>
      <Tab title="Developer-controlled">
        Use the developer-controlled Circle Wallets adapter when your backend initiates
        wallet actions.

        ## Setup

        The developer-controlled wallets adapter requires these credentials from the
        [Circle Console](https://console.circle.com):

        * **[API Key](https://developers.circle.com/w3s/circle-developer-account#creating-an-api-key-for-developer-services):**
          Environment-prefixed (examples: `TEST_API_KEY:abc123:def456`,
          `LIVE_API_KEY:xyz:uvw`) or Base64-encoded
        * **[Entity Secret](https://developers.circle.com/wallets/dev-controlled/register-entity-secret):**
          64 lowercase alphanumeric characters

        This code block initializes the Circle Wallets adapter:

        ```typescript TypeScript theme={null}
        import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

        const adapter = createCircleWalletsAdapter({
          apiKey: process.env.CIRCLE_API_KEY!,
          entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
        });
        ```

        ## Usage

        When you use the developer-controlled adapter in an App Kit operation, pass the
        wallet address in the operation context. The address tells the App Kit SDK which
        Circle Wallet acts on that blockchain:

        ```typescript TypeScript theme={null}
        from: {
          adapter,
          chain: "Base_Sepolia",
          address: walletAddress,
        }
        ```

        Use the wallet address for the same blockchain as `chain`.

        * The wallet address you pass to App Kit operations determines whether Circle
          handles the transaction as a dev-controlled externally owned account (EOA) or
          smart contract account (SCA) wallet. See
          [Account type comparison](https://developers.circle.com/wallets/account-types#account-type-comparison)
          to understand the difference.
        * **Dev-controlled EOA**: Each Circle Wallet exists on one blockchain. For
          crosschain flows, provision one wallet per source blockchain. The onchain
          addresses differ.
        * **Dev-controlled SCA**: Each Circle Wallet exists on one blockchain. For
          crosschain flows, provision one SCA per source blockchain.

        <Warning>
          The Circle Wallets adapter does not support
          [gas sponsorship](https://developers.circle.com/wallets/gas-station) for
          crosschain transfers that originate on Solana. For those transactions, the
          user's Solana wallet must hold sufficient SOL to pay network fees.
        </Warning>
      </Tab>

      <Tab title="User-controlled">
        Use the user-controlled adapter when end users approve wallet actions in your
        app. The adapter runs on your server. Challenge completion runs in the client
        with the
        [User-Controlled Wallets Web SDK](https://developers.circle.com/sdks/user-controlled/web-sdk).

        ## Setup

        Before creating a user-controlled wallet adapter, the user must have a
        configured signing method and credentials from your existing user-controlled
        wallet login flow. If not, adapter creation fails with Circle error code
        `155110`.

        The user-controlled adapter requires these server-side credentials:

        * **[API Key](https://developers.circle.com/w3s/circle-developer-account#creating-an-api-key-for-developer-services):**
          Environment-prefixed (examples: `TEST_API_KEY:abc123:def456`,
          `LIVE_API_KEY:xyz:uvw`) or Base64-encoded
        * **`userToken`:** From your existing UCW authentication flow. See
          [Authentication methods](https://developers.circle.com/wallets/user-controlled/authentication-methods).

        Your client also needs the `encryptionKey` from that same login (and your Circle
        App ID) to complete challenges with the Web SDK. The adapter does not take
        `encryptionKey`.

        This code block initializes the user-controlled Circle Wallets adapter:

        ```typescript TypeScript theme={null}
        import { createCircleUserWalletAdapter } from "@circle-fin/adapter-circle-wallets/ucw/server";

        const adapter = await createCircleUserWalletAdapter({
          apiKey: process.env.CIRCLE_API_KEY!,
          userToken, // from your existing UCW login
          chain: "Arc_Testnet",
          onChallenge: ({ challengeId }) => {
            // notify your client to complete the challenge
          },
        });
        ```

        ## Usage

        When you use the user-controlled adapter in an App Kit operation, pass only
        `adapter` and `chain` in the operation context. Omit `address`; the adapter
        resolves the wallet address:

        ```typescript TypeScript theme={null}
        from: {
          adapter,
          chain: "Arc_Testnet",
        }
        ```

        * **Account type:** The user-controlled adapter uses smart contract account
          (SCA) wallets. See
          [Account type comparison](https://developers.circle.com/wallets/account-types#account-type-comparison).

        * **Approval flow:** An App Kit method such as `kit.send` waits for user
          approval. The adapter calls `onChallenge` with a `challengeId`; use it to
          notify the client only. Do not return a signature. The client completes the
          challenge with the
          [Web SDK](https://developers.circle.com/sdks/user-controlled/web-sdk), while
          the adapter polls Circle until approval completes. Keep the client-side
          challenge UI and listener available for the entire duration of the server-side
          call.

        * **When challenges fire:** Passing `chain` or `chains` can find or create the
          user's SCA wallet, so a challenge may fire during adapter creation or a later
          App Kit operation. Crosschain operations may emit multiple challenges; keep
          listening until the operation finishes.

        * **Chain selection:** Pass `chain` for a single-chain operation. For crosschain
          operations, pass `chains` (an array of every blockchain involved) when
          creating the adapter. Use the same adapter instance for both the source
          (`from`) and destination (`to`):

          ```typescript TypeScript theme={null}
          from: { adapter, chain: "Arc_Testnet" },
          to:   { adapter, chain: "Base_Sepolia" },
          ```

        * **Challenge delivery:** Deliver each `challengeId` to the client through your
          own channel, such as SSE, WebSocket, or polling.
      </Tab>
    </Tabs>
  </Tab>
</Tabs>
