Modern multi-chain treasury management system. This sample application uses Next.js, Supabase, and Circle Developer Controlled Wallets, Circle Gateway, Circle App Kit, and Circle EarnKit to demonstrate a multi-chain treasury management system with bridge, swap, yield, and compliance capabilities.
- Features
- Prerequisites
- Getting Started
- How It Works
- Webhooks & Real-Time Updates
- Environment Variables
- User Accounts
The dashboard is organized into a set of pages, each backed by Circle APIs:
- Treasury overview (
/dashboard) — Aggregated multi-chain USDC and EURC balances, including a unified cross-chain balance via Circle Gateway. - Wallets (
/dashboard/wallets) — Create and manage Developer-Controlled Wallets across supported chains and view per-network USDC/EURC balances. - Add Funds (available from the dashboard and wallet views) — Buy USDC/EURC with fiat directly into a Developer-Controlled Wallet via Circle's hosted Onramp Kit widget. Sandbox only — see Environment Variables.
- Swap (
/dashboard/swap) — Swap USDC ⇄ EURC on Arc Testnet via App Kit (kit.swap/kit.estimateSwap). Requires aKIT_KEY. - Earn (
/dashboard/earn) — Discover USDC vaults on Arc Testnet with EarnKit, deposit, withdraw, and track positions. (Reward accrual is mocked — see How It Works.) - Activity (
/dashboard/activity) — A unified transaction history spanning transfers, bridges, swaps, and vault operations. - Compliance (
/dashboard/compliance) — Screen addresses with Circle's Compliance Engine before initiating transfers.
- Node.js v22+ — Install via nvm
- Supabase CLI — Install via
npm install -g supabaseor see Supabase CLI docs - Docker Desktop (only if using the local Supabase path) — Install Docker Desktop
- Circle Developer Controlled Wallets API key and Entity Secret
- A Circle Kit Key — optional for Earn, but required for Swap and Add Funds (see Environment Variables)
-
Clone the repository and install dependencies:
git clone git@github.com:akelani-circle/fintech-starter.git cd fintech-starter npm install -
Set up environment variables:
cp .env.example .env.local
Then edit
.env.localand fill in all required values (see Environment Variables section below). For webhook delivery in local development, setWEBHOOK_ENDPOINT_URLto your tunnel URL — see Webhooks & Real-Time Updates. -
Set up the database — Choose one of the two paths below:
Path 1: Local Supabase (Docker)
Requires Docker Desktop installed and running.
npx supabase start npx supabase migration up
The output of
npx supabase startwill display the Supabase URL and API keys needed for your.env.local.Path 2: Remote Supabase (Cloud)
Requires a Supabase account and project.
npx supabase link --project-ref <your-project-ref> npx supabase db push
Retrieve your project URL and API keys from the Supabase dashboard under Settings → API.
-
Start the development server:
npm run dev
The app will be available at
http://localhost:3000. -
(Optional) Enable webhooks so balances update automatically when funds arrive. See Webhooks & Real-Time Updates.
- Built with Next.js App Router and Supabase
- Uses Circle Developer Controlled Wallets for managing multi-chain transactions
- Uses Circle Gateway for a unified, cross-chain USDC balance
- Utilizes
@circle-fin/app-kitfor bridging assets across supported chains (kit.bridge/kit.estimateBridge) and for swapping USDC ⇄ EURC on Arc Testnet (kit.swap/kit.estimateSwap) - Uses
@circle-fin/earn-kitto discover USDC vaults on Arc Testnet, deposit, withdraw, and track positions - Uses
@crcl-main/onramp-kitfor Add Funds: the server mints a single-use session (/api/onramp/session) for a wallet the caller owns, and the browser opens Circle's hosted onramp widget in a popup with that session. Sandbox-only — see Environment Variables - Uses Circle's Compliance Engine to screen addresses before transfers
- Circle webhooks keep transaction and Gateway state in sync (see Webhooks & Real-Time Updates)
- Real-time UI updates powered by Supabase Realtime subscriptions
- Styled with Tailwind CSS and components from shadcn/ui
Earn rewards are mocked. Vault deposits, withdrawals, and positions are real EarnKit operations, but reward accrual and claiming are faked client-side in
lib/earn/mock-rewards.ts— Arc Testnet has no live Merkl reward distribution. The module is self-contained and meant to be deleted once real rewards ship.
The dashboard refreshes balances automatically when funds move: a Circle webhook updates a row in Supabase, and a Supabase Realtime subscription pushes that change to the UI.
Circle must reach your endpoint over the public internet, so local development needs a tunnel (e.g. ngrok). Point a tunnel at your dev server and set WEBHOOK_ENDPOINT_URL accordingly:
ngrok http 3000The app uses two subscriptions (both routed to the same handler): a standard Developer-Controlled Wallets subscription at /api/circle/webhook for transactions.* events, and a permissionless Gateway subscription at /api/circle/gateway-webhook for gateway.deposit.finalized. Circle requires a unique endpoint URL per subscription, which is why the Gateway subscription uses a distinct path. New wallet addresses are registered on the Gateway subscription automatically when wallets are created; npm run webhooks:register backfills addresses for wallets that already exist.
A settled Add Funds purchase is meant to arrive as onramp.deposit.settled on the standard DCW subscription above. That notification type is enabled per-subscription in the Circle Console, not by npm run webhooks:register — turn it on there or no onramp deposit is ever recorded.
Copy .env.example to .env.local and fill in the required values:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
SUPABASE_SECRET_KEY=your-secret-key
# Circle
CIRCLE_API_KEY=your-circle-api-key
CIRCLE_ENTITY_SECRET=your-circle-entity-secret
# Circle Kit Key (shared by Earn, Swap, and Add Funds; required for Swap and Add Funds)
KIT_KEY=KIT_KEY:<keyId>:<keySecret>
# Add Funds (Onramp Kit) sandbox endpoints — keep these set, see table below
ONRAMP_API_BASE_URL=https://api-test.circle.com
NEXT_PUBLIC_ONRAMP_WIDGET_BASE_URL=https://onramp-sandbox.arc.io
# Webhooks (see "Webhooks & Real-Time Updates" below)
WEBHOOK_ENDPOINT_URL=https://your-ngrok-url/api/circle/webhook
# GATEWAY_WEBHOOK_ENDPOINT_URL= # optional override; derived from the above if unset
# Arc Testnet RPC (optional)
ARC_TESTNET_RPC_KEY=| Variable | Scope | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Public | Supabase project URL. |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
Public | Supabase publishable key. |
SUPABASE_SECRET_KEY |
Server-side | Supabase secret key for admin operations. |
CIRCLE_API_KEY |
Server-side | Circle API key for wallet operations, compliance screening, and webhook subscription management. |
CIRCLE_ENTITY_SECRET |
Server-side | Circle entity secret for signing transactions. |
KIT_KEY |
Server-side | Circle Kit Key, shared by EarnKit, App Kit Swap, and Onramp Kit (Add Funds). Optional for Earn — it runs permissionlessly without one. Required for Swap and Add Funds — the Stablecoin Service rejects an empty kit key for Swap, and the onramp session route throws without one. Setting it also enables integrator attribution and higher rate limits. Must be a sandbox key: a key is bound to one environment and the other rejects it. Get a free key from the Circle Console. Format: KIT_KEY:<keyId>:<keySecret>. |
ONRAMP_API_BASE_URL |
Server-side | Add Funds sandbox API endpoint (https://api-test.circle.com). Unset, empty, or whitespace all mean the kit's production default — real money, on mainnet — so this app refuses to mint an onramp session unless it's set. Must always match NEXT_PUBLIC_ONRAMP_WIDGET_BASE_URL's environment. |
NEXT_PUBLIC_ONRAMP_WIDGET_BASE_URL |
Public | Add Funds sandbox widget origin (https://onramp-sandbox.arc.io). Same unset-means-production rule as ONRAMP_API_BASE_URL, which it must match. Inlined at build time — restart npm run dev after changing it. |
WEBHOOK_ENDPOINT_URL |
Server-side | Public HTTPS URL Circle posts notifications to (e.g. your ngrok tunnel + /api/circle/webhook). Used to create/sync the standard and Gateway webhook subscriptions. If unset, falls back to ${NEXT_PUBLIC_APP_URL}/api/circle/webhook and registration is skipped when neither is set. |
GATEWAY_WEBHOOK_ENDPOINT_URL |
Server-side | Optional. Dedicated endpoint for the Gateway permissionless subscription. Circle requires a unique URL per subscription, so this must differ from WEBHOOK_ENDPOINT_URL. If unset, it is derived by swapping the path to /api/circle/gateway-webhook. |
NEXT_PUBLIC_APP_URL |
Public | Optional. Base URL of the deployed app, used as a fallback to derive the webhook endpoints when WEBHOOK_ENDPOINT_URL / GATEWAY_WEBHOOK_ENDPOINT_URL are unset. Not required for local development when WEBHOOK_ENDPOINT_URL is set. |
ARC_TESTNET_RPC_KEY |
Server-side | Optional. API key for Arc Testnet RPC reads; without it, a rate-limited public RPC is used. |
SUPABASE_SERVICE_ROLE_KEY |
Server-side | Only needed by the npm run webhooks:register backfill script, which reads wallet addresses directly. The app itself uses SUPABASE_SECRET_KEY. |
On first visit, sign up with any email and password.
This sample application:
- Assumes testnet usage only. Add Funds refuses to mint an onramp session unless it's configured against Circle's sandbox — see
ONRAMP_API_BASE_URL/NEXT_PUBLIC_ONRAMP_WIDGET_BASE_URLabove - Handles secrets via environment variables
- Is not intended for production use without modification
