From ff8ddecc184bef66f9e455d9fa64449366b1a3d5 Mon Sep 17 00:00:00 2001
From: HJ Chen <221941357+hjchen-circle@users.noreply.github.com>
Date: Wed, 16 Sep 2026 12:09:59 +0000
Subject: [PATCH] feat: update packages to latest and new kits
---
README.md | 19 ++
app-kit-bridge-evm/README.md | 17 +-
app-kit-bridge-evm/package.json | 8 +-
app-kit-bridge-solana/README.md | 3 +-
app-kit-bridge-solana/package.json | 10 +-
app-kit-bridge-solana/src/main.ts | 6 +-
app-kit-earn/.gitignore | 24 ++
app-kit-earn/README.md | 83 +++++++
app-kit-earn/index.html | 69 ++++++
app-kit-earn/package.json | 19 ++
app-kit-earn/src/main.ts | 356 +++++++++++++++++++++++++++
app-kit-earn/tsconfig.json | 23 ++
app-kit-onramp/.env.example | 16 ++
app-kit-onramp/.gitignore | 25 ++
app-kit-onramp/README.md | 79 ++++++
app-kit-onramp/index.html | 85 +++++++
app-kit-onramp/package.json | 20 ++
app-kit-onramp/server.ts | 102 ++++++++
app-kit-onramp/src/main.ts | 215 ++++++++++++++++
app-kit-onramp/tsconfig.json | 23 ++
app-kit-onramp/vite.config.ts | 27 ++
app-kit-send/package.json | 8 +-
app-kit-swap/package.json | 8 +-
app-kit-unified-balance/package.json | 10 +-
24 files changed, 1228 insertions(+), 27 deletions(-)
create mode 100644 app-kit-earn/.gitignore
create mode 100644 app-kit-earn/README.md
create mode 100644 app-kit-earn/index.html
create mode 100644 app-kit-earn/package.json
create mode 100644 app-kit-earn/src/main.ts
create mode 100644 app-kit-earn/tsconfig.json
create mode 100644 app-kit-onramp/.env.example
create mode 100644 app-kit-onramp/.gitignore
create mode 100644 app-kit-onramp/README.md
create mode 100644 app-kit-onramp/index.html
create mode 100644 app-kit-onramp/package.json
create mode 100644 app-kit-onramp/server.ts
create mode 100644 app-kit-onramp/src/main.ts
create mode 100644 app-kit-onramp/tsconfig.json
create mode 100644 app-kit-onramp/vite.config.ts
diff --git a/README.md b/README.md
index b98678e..31255ec 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,25 @@ destination and a Solana wallet for the source.
Run with `npm install && npm run dev`.
+### [`app-kit-earn`](./app-kit-earn)
+
+Browser app that discovers earn vaults on Arc Testnet, deposits and withdraws
+USDC, and checks a position using
+[App Kit](https://www.npmjs.com/package/@circle-fin/app-kit) with the viem
+adapter. Connects to any EIP-6963 browser wallet (e.g., MetaMask).
+
+Run with `npm install && npm run dev`.
+
+### [`app-kit-onramp`](./app-kit-onramp)
+
+Browser app that embeds the Arc Onramp widget with
+[App Kit](https://www.npmjs.com/package/@circle-fin/app-kit): mint a session on
+a small Node server, then `fetchSession()`, `mountIframe()`, or `openWindow()`.
+Requires a Circle API key; run the session server and Vite client together.
+
+Run with `npm install`, then `npm run server` and `npm run dev` in separate
+terminals.
+
### [`app-kit-send`](./app-kit-send)
Browser app that estimates and sends USDC on Arc Testnet using
diff --git a/app-kit-bridge-evm/README.md b/app-kit-bridge-evm/README.md
index f56cb2a..9979176 100644
--- a/app-kit-bridge-evm/README.md
+++ b/app-kit-bridge-evm/README.md
@@ -41,5 +41,18 @@ npm run build
## Key file
- `src/main.ts` — wallet connect, adapter creation, bridge, and retry.
- Change `from` / `to` `chain` and `amount` in the bridge call for different
- chains or amounts.
+ Change `from` / `to` `chain`, `amount`, and optional `token` in the bridge
+ call for different chains, amounts, or tokens.
+
+## Bridge a non-USDC token
+
+This sample defaults to USDC. To bridge a
+[non-USDC token](https://developers.circle.com/cctp/expanded-assets), pass
+`token` on the same `kit.bridge()` call in `src/main.ts` — for example
+`token: "EURC"` (also `"wETH"`, `"cirBTC"`, or a configured asset's
+`tokenId`). Fund the source wallet with that token. `amount` stays a
+human-readable decimal string (EURC uses 6 decimals, cirBTC 8, wETH 18).
+
+Both chains must support CCTP for non-USDC, and the token must be configured on
+both. Check
+[supported blockchains and tokens](https://developers.circle.com/cctp/expanded-assets/concepts/supported-chains-and-domains).
diff --git a/app-kit-bridge-evm/package.json b/app-kit-bridge-evm/package.json
index b188e09..b8cc943 100644
--- a/app-kit-bridge-evm/package.json
+++ b/app-kit-bridge-evm/package.json
@@ -9,11 +9,11 @@
},
"devDependencies": {
"typescript": "~7.0.2",
- "vite": "^8.2.2"
+ "vite": "^8.3.0"
},
"dependencies": {
- "@circle-fin/app-kit": "^1.14.0",
- "@circle-fin/adapter-viem-v2": "^1.17.1",
- "viem": "^2.56.3"
+ "@circle-fin/app-kit": "^1.15.1",
+ "@circle-fin/adapter-viem-v2": "^1.18.0",
+ "viem": "^2.56.5"
}
}
diff --git a/app-kit-bridge-solana/README.md b/app-kit-bridge-solana/README.md
index 0961d11..9272bf6 100644
--- a/app-kit-bridge-solana/README.md
+++ b/app-kit-bridge-solana/README.md
@@ -12,7 +12,8 @@ calls `bridge()` and `retryBridge()` when needed.
- [Node.js 22 or later](https://nodejs.org/)
- An EIP-6963 EVM browser wallet such as MetaMask
-- A Solana browser wallet on `window.solana` such as Phantom
+- A Solana browser wallet such as Phantom (`window.solana`) or Solflare
+ (`window.solflare`)
- Testnet USDC on Solana Devnet
- Native gas tokens for both wallets
diff --git a/app-kit-bridge-solana/package.json b/app-kit-bridge-solana/package.json
index 3251015..7845d90 100644
--- a/app-kit-bridge-solana/package.json
+++ b/app-kit-bridge-solana/package.json
@@ -9,12 +9,12 @@
},
"devDependencies": {
"typescript": "~7.0.2",
- "vite": "^8.2.2"
+ "vite": "^8.3.0"
},
"dependencies": {
- "@circle-fin/adapter-solana": "^1.7.2",
- "@circle-fin/adapter-viem-v2": "^1.17.1",
- "@circle-fin/app-kit": "^1.14.0",
- "viem": "^2.56.3"
+ "@circle-fin/adapter-solana": "^1.7.3",
+ "@circle-fin/adapter-viem-v2": "^1.18.0",
+ "@circle-fin/app-kit": "^1.15.1",
+ "viem": "^2.56.5"
}
}
diff --git a/app-kit-bridge-solana/src/main.ts b/app-kit-bridge-solana/src/main.ts
index 1883c9a..ffbe2c6 100644
--- a/app-kit-bridge-solana/src/main.ts
+++ b/app-kit-bridge-solana/src/main.ts
@@ -42,6 +42,7 @@ declare global {
}
interface Window {
solana?: SolanaWalletProvider;
+ solflare?: SolanaWalletProvider;
}
}
@@ -107,11 +108,12 @@ async function handleSolanaConnect() {
try {
connectSolButton.disabled = true;
- if (!window.solana) {
+ const provider = window.solana ?? window.solflare;
+ if (!provider) {
throw new Error("No Solana browser wallet found");
}
- solanaProvider = window.solana;
+ solanaProvider = provider;
const connection = await solanaProvider.connect();
solanaWalletInfo.textContent =
connection.publicKey?.toString() ??
diff --git a/app-kit-earn/.gitignore b/app-kit-earn/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/app-kit-earn/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/app-kit-earn/README.md b/app-kit-earn/README.md
new file mode 100644
index 0000000..fbd4e21
--- /dev/null
+++ b/app-kit-earn/README.md
@@ -0,0 +1,83 @@
+# Deposit and withdraw USDC in an earn vault
+
+Use [`@circle-fin/app-kit`](https://www.npmjs.com/package/@circle-fin/app-kit)
+to discover earn vaults on Arc Testnet, deposit USDC, check a position, and
+withdraw with a browser wallet and the viem adapter.
+
+This Vite + TypeScript page connects an EIP-6963 wallet (for example MetaMask),
+creates a Circle adapter from the provider, then calls `earn.exploreVaults()`,
+`earn.getDepositQuote()`, `earn.deposit()`, `earn.getPosition()`,
+`earn.getWithdrawalQuote()`, and `earn.withdraw()`.
+
+## Prerequisites
+
+- [Node.js 22 or later](https://nodejs.org/)
+- An EIP-6963 browser wallet such as MetaMask
+- Testnet USDC on Arc Testnet
+- Native gas token on Arc Testnet for the wallet transaction
+
+This project does not require environment variables or API keys.
+
+## Install and run
+
+```bash
+npm install
+npm run dev
+```
+
+Open the local Vite URL in a browser with your wallet installed.
+
+```bash
+npm run build
+```
+
+## What this example does
+
+1. Discovers an EIP-6963 browser wallet and requests account access.
+2. Lists active vaults on `Arc_Testnet` with `kit.earn.exploreVaults()`.
+3. Creates a viem adapter with `createViemAdapterFromProvider()`.
+4. Previews and deposits 1 USDC with `kit.earn.getDepositQuote()` and
+ `kit.earn.deposit()`.
+5. Reads the position with `kit.earn.getPosition()`.
+6. Previews and withdraws 1 USDC with `kit.earn.getWithdrawalQuote()` and
+ `kit.earn.withdraw()`.
+
+## Key file
+
+- `src/main.ts` — wallet connect, vault discovery, quotes, deposit, position,
+ and withdraw. Change `chain`, `amount`, and the selected vault for a
+ different chain, amount, or opportunity.
+
+## Deposit from another chain
+
+This sample deposits on the same chain as the vault (`Arc_Testnet`). To deposit
+USDC from a wallet on another blockchain into that vault, pass a different
+`from.chain` and a `to` destination on the same `kit.earn.deposit()` call in
+`src/main.ts` — for example Ethereum Sepolia → Arc Testnet:
+
+```typescript
+const result = await kit.earn.deposit({
+ from: { adapter, chain: "Ethereum_Sepolia" },
+ to: {
+ chain: "Arc_Testnet",
+ recipientAddress, // position owner on the vault chain
+ },
+ vaultAddress,
+ amount: "1",
+ transferSpeed: "FAST", // optional: "FAST" | "SLOW"
+});
+```
+
+Fund the source wallet with USDC and gas on the source chain (not Arc). Testnet
+sources are Ethereum Sepolia, Arbitrum Sepolia, and Base Sepolia; the vault
+destination is Arc Testnet.
+
+A crosschain deposit returns when the bridge submit succeeds
+(`result.kind === "cross-chain"`), not when the vault position is funded. Use
+`kit.earn.getCrossChainDepositStatus()` or
+`kit.earn.waitForCrossChainDeposit()` with `result.execId` to track settlement.
+Preview fees with the same `from` / `to` shape on `kit.earn.getDepositQuote()`.
+
+See
+[Deposit crosschain into an Earn vault](https://docs.arc.io/app-kit/tutorials/earn/crosschain-deposit)
+for routes, fees, and status tracking.
diff --git a/app-kit-earn/index.html b/app-kit-earn/index.html
new file mode 100644
index 0000000..3012a67
--- /dev/null
+++ b/app-kit-earn/index.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+ Earn USDC on Arc Testnet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app-kit-earn/package.json b/app-kit-earn/package.json
new file mode 100644
index 0000000..a034879
--- /dev/null
+++ b/app-kit-earn/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "app-kit-earn",
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "~7.0.2",
+ "vite": "^8.3.0"
+ },
+ "dependencies": {
+ "@circle-fin/adapter-viem-v2": "^1.18.0",
+ "@circle-fin/app-kit": "^1.15.1",
+ "viem": "^2.56.5"
+ }
+}
diff --git a/app-kit-earn/src/main.ts b/app-kit-earn/src/main.ts
new file mode 100644
index 0000000..12de9b4
--- /dev/null
+++ b/app-kit-earn/src/main.ts
@@ -0,0 +1,356 @@
+/**
+ * Copyright 2026 Circle Internet Group, Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { AppKit } from "@circle-fin/app-kit";
+import type { EarnVaultInfo } from "@circle-fin/app-kit";
+import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2";
+import 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;
+ }
+}
+
+const kit = new AppKit();
+let walletProvider: BrowserWalletProvider | null = null;
+let vaults: EarnVaultInfo[] = [];
+
+/** Discover an EIP-6963 browser wallet (prefers MetaMask). */
+async function getProvider(): Promise {
+ const providers = new Map();
+
+ const onAnnounce = ((event: CustomEvent) => {
+ 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 selectedProvider =
+ [...providers.values()].find(
+ ({ info }) => info.rdns === "io.metamask" || info.name === "MetaMask",
+ )?.provider ?? [...providers.values()][0]?.provider;
+
+ if (!selectedProvider) {
+ throw new Error("No EIP-6963 browser wallet found");
+ }
+
+ return selectedProvider;
+}
+
+/** Connect the wallet and enable earn actions. */
+async function handleWalletConnect() {
+ try {
+ connectWalletButton.disabled = true;
+
+ walletProvider = await getProvider();
+ await walletProvider.request({
+ method: "eth_requestAccounts",
+ params: undefined,
+ });
+ const accounts = (await walletProvider.request({
+ method: "eth_accounts",
+ params: undefined,
+ })) as string[];
+
+ walletInfo.textContent = accounts[0] ?? "Connected";
+ output.textContent = "";
+ } catch (error) {
+ walletProvider = null;
+ render({
+ error: (error as { message?: string })?.message ?? String(error),
+ });
+ } finally {
+ connectWalletButton.disabled = Boolean(walletProvider);
+ updateActionButtons();
+ }
+}
+
+/** Load active earn vaults on Arc Testnet, sorted by APY. */
+async function handleLoadVaults() {
+ try {
+ vaultSelect.disabled = true;
+
+ const result = await kit.earn.exploreVaults({
+ chain: "Arc_Testnet",
+ sortBy: "apy",
+ });
+
+ vaults = [...result.vaults];
+
+ vaultSelect.replaceChildren();
+ vaultSelect.add(new Option("--Select vault--", "", true, true));
+ vaultSelect.options[0]!.disabled = true;
+
+ for (const vault of vaults) {
+ if (vault.liquidityProfile?.status !== "active" || !vault.address) {
+ continue;
+ }
+
+ const apyPercent = ((vault.apyProfile?.current ?? 0) * 100).toFixed(2);
+ vaultSelect.add(
+ new Option(`${vault.name} (${apyPercent}% APY)`, vault.address),
+ );
+ }
+ } catch (error) {
+ render({
+ error: (error as { message?: string })?.message ?? String(error),
+ });
+ } finally {
+ vaultSelect.disabled = false;
+ updateActionButtons();
+ }
+}
+
+/** Show the selected vault's metadata in the output panel. */
+function handleVaultSelect() {
+ const vault = vaults.find((entry) => entry.address === vaultSelect.value);
+ if (vault) {
+ render(vault);
+ }
+ updateActionButtons();
+}
+
+/** Preview depositing 1 USDC into the selected vault. */
+async function handleDepositQuote() {
+ try {
+ if (!walletProvider) {
+ throw new Error("Connect a wallet first");
+ }
+
+ const vaultAddress = vaultSelect.value;
+ if (!vaultAddress) {
+ throw new Error("Select a vault first");
+ }
+
+ depositQuoteButton.disabled = true;
+
+ const adapter = await createViemAdapterFromProvider({
+ provider: walletProvider,
+ });
+
+ const quote = await kit.earn.getDepositQuote({
+ from: { adapter, chain: "Arc_Testnet" },
+ vaultAddress,
+ amount: "1",
+ });
+
+ render(quote);
+ } catch (error) {
+ render({
+ error: (error as { message?: string })?.message ?? String(error),
+ });
+ } finally {
+ updateActionButtons();
+ }
+}
+
+/** Deposit 1 USDC into the selected vault on Arc Testnet. */
+async function handleDeposit() {
+ try {
+ if (!walletProvider) {
+ throw new Error("Connect a wallet first");
+ }
+
+ const vaultAddress = vaultSelect.value;
+ if (!vaultAddress) {
+ throw new Error("Select a vault first");
+ }
+
+ depositButton.disabled = true;
+
+ const adapter = await createViemAdapterFromProvider({
+ provider: walletProvider,
+ });
+
+ const result = await kit.earn.deposit({
+ from: { adapter, chain: "Arc_Testnet" },
+ vaultAddress,
+ amount: "1",
+ });
+
+ render(result);
+ } catch (error) {
+ render({
+ error: (error as { message?: string })?.message ?? String(error),
+ });
+ } finally {
+ updateActionButtons();
+ }
+}
+
+/** Read the wallet's position in the selected vault. */
+async function handleCheckPosition() {
+ try {
+ if (!walletProvider) {
+ throw new Error("Connect a wallet first");
+ }
+
+ const vaultAddress = vaultSelect.value;
+ if (!vaultAddress) {
+ throw new Error("Select a vault first");
+ }
+
+ checkPositionButton.disabled = true;
+
+ const adapter = await createViemAdapterFromProvider({
+ provider: walletProvider,
+ });
+
+ const position = await kit.earn.getPosition({
+ from: { adapter, chain: "Arc_Testnet" },
+ vaultAddress,
+ });
+
+ render(position);
+ } catch (error) {
+ render({
+ error: (error as { message?: string })?.message ?? String(error),
+ });
+ } finally {
+ updateActionButtons();
+ }
+}
+
+/** Preview withdrawing 1 USDC from the selected vault. */
+async function handleWithdrawQuote() {
+ try {
+ if (!walletProvider) {
+ throw new Error("Connect a wallet first");
+ }
+
+ const vaultAddress = vaultSelect.value;
+ if (!vaultAddress) {
+ throw new Error("Select a vault first");
+ }
+
+ withdrawQuoteButton.disabled = true;
+
+ const adapter = await createViemAdapterFromProvider({
+ provider: walletProvider,
+ });
+
+ const quote = await kit.earn.getWithdrawalQuote({
+ from: { adapter, chain: "Arc_Testnet" },
+ vaultAddress,
+ amount: "1",
+ });
+
+ render(quote);
+ } catch (error) {
+ render({
+ error: (error as { message?: string })?.message ?? String(error),
+ });
+ } finally {
+ updateActionButtons();
+ }
+}
+
+/** Withdraw 1 USDC from the selected vault on Arc Testnet. */
+async function handleWithdraw() {
+ try {
+ if (!walletProvider) {
+ throw new Error("Connect a wallet first");
+ }
+
+ const vaultAddress = vaultSelect.value;
+ if (!vaultAddress) {
+ throw new Error("Select a vault first");
+ }
+
+ withdrawButton.disabled = true;
+
+ const adapter = await createViemAdapterFromProvider({
+ provider: walletProvider,
+ });
+
+ const result = await kit.earn.withdraw({
+ from: { adapter, chain: "Arc_Testnet" },
+ vaultAddress,
+ amount: "1",
+ });
+
+ render(result);
+ } catch (error) {
+ render({
+ error: (error as { message?: string })?.message ?? String(error),
+ });
+ } finally {
+ updateActionButtons();
+ }
+}
+
+/** Enable quote / deposit / position / withdraw when wallet and vault are set. */
+function updateActionButtons() {
+ const enabled = Boolean(walletProvider) && vaultSelect.value !== "";
+ depositQuoteButton.disabled = !enabled;
+ depositButton.disabled = !enabled;
+ checkPositionButton.disabled = !enabled;
+ withdrawQuoteButton.disabled = !enabled;
+ withdrawButton.disabled = !enabled;
+}
+
+/** Pretty-print a value into the output panel. */
+function render(value: unknown) {
+ output.textContent = JSON.stringify(
+ value,
+ (_key, currentValue) =>
+ typeof currentValue === "bigint" ? currentValue.toString() : currentValue,
+ 2,
+ );
+ output.scrollTop = 0;
+}
+
+const connectWalletButton =
+ document.querySelector("#connectWallet")!;
+const vaultSelect = document.querySelector("#vaults")!;
+const depositQuoteButton =
+ document.querySelector("#depositQuote")!;
+const depositButton = document.querySelector("#deposit")!;
+const checkPositionButton =
+ document.querySelector("#checkPosition")!;
+const withdrawQuoteButton =
+ document.querySelector("#withdrawQuote")!;
+const withdrawButton = document.querySelector("#withdraw")!;
+const walletInfo = document.querySelector("#walletInfo")!;
+const output = document.querySelector("#output")!;
+
+connectWalletButton.addEventListener("click", handleWalletConnect);
+vaultSelect.addEventListener("change", handleVaultSelect);
+depositQuoteButton.addEventListener("click", handleDepositQuote);
+depositButton.addEventListener("click", handleDeposit);
+checkPositionButton.addEventListener("click", handleCheckPosition);
+withdrawQuoteButton.addEventListener("click", handleWithdrawQuote);
+withdrawButton.addEventListener("click", handleWithdraw);
+void handleLoadVaults();
+updateActionButtons();
diff --git a/app-kit-earn/tsconfig.json b/app-kit-earn/tsconfig.json
new file mode 100644
index 0000000..1ab38c8
--- /dev/null
+++ b/app-kit-earn/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "es2023",
+ "module": "esnext",
+ "lib": ["ES2023", "DOM"],
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/app-kit-onramp/.env.example b/app-kit-onramp/.env.example
new file mode 100644
index 0000000..036e24f
--- /dev/null
+++ b/app-kit-onramp/.env.example
@@ -0,0 +1,16 @@
+# Circle API key for onramp session minting (server only).
+CIRCLE_API_KEY=YOUR_API_KEY
+
+# Bare hostname of the page that embeds the iframe (no scheme/port/path).
+# Vite defaults to localhost — keep this in sync with where you open the app.
+ONRAMP_REFERRER_DOMAIN=localhost
+
+# --- sandbox (use for local development) ---
+ONRAMP_API_BASE_URL=https://api-test.circle.com
+ONRAMP_WIDGET_BASE_URL=https://onramp-sandbox.arc.io
+VITE_ONRAMP_WIDGET_BASE_URL=https://onramp-sandbox.arc.io
+
+# --- production (defaults — omit sandbox lines to use prod) ---
+# ONRAMP_API_BASE_URL=https://api.circle.com
+# ONRAMP_WIDGET_BASE_URL=https://onramp.arc.io
+# VITE_ONRAMP_WIDGET_BASE_URL=https://onramp.arc.io
diff --git a/app-kit-onramp/.gitignore b/app-kit-onramp/.gitignore
new file mode 100644
index 0000000..438657a
--- /dev/null
+++ b/app-kit-onramp/.gitignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+.env
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/app-kit-onramp/README.md b/app-kit-onramp/README.md
new file mode 100644
index 0000000..c8e23c5
--- /dev/null
+++ b/app-kit-onramp/README.md
@@ -0,0 +1,79 @@
+# Embed Arc Onramp in a browser app
+
+Use [`@circle-fin/app-kit`](https://www.npmjs.com/package/@circle-fin/app-kit)
+to mint an onramp session on your server, then embed Circle's hosted Arc
+Onramp widget with `kit.onramp.fetchSession()`, `mountIframe()`, and
+`openWindow()`.
+
+Unlike the other App Kit samples, onramp is a **widget embed**: the long-lived
+`apiKey` stays on the server (`@circle-fin/app-kit/server`), and the browser
+only receives a short-lived session. Any Fetch-compatible host can expose the
+session route (for example Next.js, Hono, or Workers); this sample uses a
+small Node `http` server only to keep the demo self-contained.
+
+## Prerequisites
+
+- [Node.js 22 or later](https://nodejs.org/)
+- A Circle API key that can mint onramp sessions
+- A destination wallet address that should receive purchased funds
+
+## Configure
+
+```bash
+cp .env.example .env
+```
+
+Set `CIRCLE_API_KEY` in `.env`. Keep `ONRAMP_REFERRER_DOMAIN=localhost` when
+you open the Vite app on `http://localhost:5173` (hostname only — no scheme
+or port). The example defaults to sandbox hosts (`api-test.circle.com` /
+`onramp-sandbox.arc.io`); comment those out for production defaults.
+
+## Install and run
+
+```bash
+npm install
+```
+
+Start the session server (terminal 1):
+
+```bash
+npm run server
+```
+
+Start the Vite client (terminal 2):
+
+```bash
+npm run dev
+```
+
+Open the local Vite URL. Vite proxies `/api/*` to the session server on
+port `3001`.
+
+```bash
+npm run build
+```
+
+## What this example does
+
+1. `server.ts` creates `createAppServerKit` with your `apiKey` and exposes
+ `POST /api/onramp/sessions` via `createSessionRouteHandler`.
+2. The client calls `kit.onramp.fetchSession()` against that route.
+3. **Mount iframe** embeds the hosted widget with `kit.onramp.mountIframe()`.
+4. **Open popup** launches `kit.onramp.openWindow()` synchronously from the
+ click handler (session must already be prepared so the user gesture is
+ preserved).
+5. Widget lifecycle events are printed in the output panel.
+
+## Key files
+
+- `server.ts` — session minting route (`apiKey` never reaches the browser).
+- `src/main.ts` — prepare session, mount iframe, open popup.
+- `vite.config.ts` — proxies `/api` to the local session server.
+
+## Production notes
+
+- Pass an `authorize` hook to `createSessionRouteHandler` so anonymous
+ callers cannot mint sessions with attacker-chosen destination addresses.
+- Set `referrerDomain` to the real hostname that embeds the iframe.
+- Allow `frame-src https://onramp.arc.io` (or your staging widget host) in
+ the embedding page CSP.
diff --git a/app-kit-onramp/index.html b/app-kit-onramp/index.html
new file mode 100644
index 0000000..dc73017
--- /dev/null
+++ b/app-kit-onramp/index.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+ Embed Arc Onramp
+
+
+
+
+