Hands-on workshop for developers new to building with stablecoins.
In about 30 minutes you'll use Circle's TypeScript SDKs to:
- Embed wallets in a backend app — no seed phrases, no browser wallets
- Receive USDC and EURC on Arc Testnet
- Send stablecoins between wallets you control
- Bridge USDC across chains: Arc → Base Sepolia or Arc → Solana Devnet
Each step is one short TypeScript file in src/. Read the file header, run the
script, then move on.
Arc is Circle's EVM-compatible Layer-1 designed for stablecoin finance. The thing that matters for this workshop:
USDC is the native gas token on Arc.
That means your wallet only needs USDC — no separate ETH, MATIC, or SOL — to do everything in this workshop: send transfers and bridge across chains. It also makes the developer experience radically simpler when you start building real apps. One asset, one balance to monitor, one fee to estimate.
┌──────────────────────────────────┐
│ Your wallet set on Circle │
│ (apiKey + entitySecret) │
└──────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
Wallet A (Arc) Wallet B (Arc) Wallet C (Base / Sol)
sender receiver bridge destination
│ ▲ ▲
│ step 4: send │ │
├───── 1 USDC ──────┘ │
│ │
│ step 5: bridge (CCTP + Forwarder) │
└─────── 1 USDC ────────────────────────┘
All wallets live in one wallet set, all signing flows through the same API key + entity secret.
- Node.js 20+ and npm
- A Circle Developer account: https://console.circle.com
- 5–10 minutes to register an entity secret (one-time setup)
That's it. No Hardhat, no Anchor, no private keys, no faucets you have to chase across Discord.
npm install- Sign in at https://console.circle.com
- Switch the environment toggle to Testnet (top-right)
- Go to API Keys → Create a key
- Copy the key (starts with
TEST_API_KEY:…)
cp .env.example .envOpen .env and paste in your CIRCLE_API_KEY. Leave
CIRCLE_ENTITY_SECRET=your_32_byte_hex_entity_secret_here as-is for now —
the next step will fill it in for you.
The entity secret is a 32-byte key Circle uses to encrypt every signing request your app makes. You generate it locally; Circle never sees the raw value, only an encrypted ciphertext that proves you possess it.
Skip this step if you already registered an entity secret for your Circle account (e.g. in the Circle Developer Console quickstart or another project). Just paste your existing
CIRCLE_ENTITY_SECRETinto.envand continue to step 1.
Otherwise, run:
npm run 0-register-entity-secretThis script will:
- Generate a fresh 32-byte hex secret using Node's
crypto - Register its ciphertext with Circle via the SDK
- Save Circle's recovery file (
recovery_file_*.dat) to the project root - Write
CIRCLE_ENTITY_SECRET=…into your.envfile
Important: Move the recovery file to a password manager or offline storage. It's the only way to rotate the entity secret if it's ever lost. Circle does not store your entity secret and cannot recover it for you.
Run each script in order. Each one prints a clear "Next:" line at the end.
| Step | Command | What it does |
|---|---|---|
| 0 | npm run 0-register-entity-secret |
(Prereq) Generate + register an entity secret if you don't have one yet |
| 1 | npm run 1-create-wallets |
Create two SCA wallets on Arc Testnet |
| 2 | npm run 2-fund-wallet |
Receive USDC + EURC from the Circle faucet |
| 3 | npm run 3-setup-monitoring |
Filter wallet balances to USDC + EURC |
| 4 | npm run 4-transfer |
Send 1 USDC between two wallets you own |
| 5 | npm run 5-bridge |
Bridge 1 USDC from Arc → Base Sepolia (CCTP + Forwarder) |
Two ways to run step 5:
# Default: bridge to Base Sepolia
npm run 5-bridge
# Bridge to Solana Devnet instead
BRIDGE_DESTINATION=solana npm run 5-bridgeState is persisted to wallet-state.json between scripts, so destination
wallets are created once and reused on reruns.
src/
├── client.ts ← shared SDK client, token addresses, state file
├── log.ts ← shared console formatting (banners, sections, kv)
├── 0-register-entity-secret.ts ← generateEntitySecret + registerEntitySecretCiphertext
├── 1-create-wallets.ts ← client.createWalletSet + client.createWallets
├── 2-fund-wallet.ts ← reading wallet balances via REST
├── 3-setup-monitoring.ts ← client.updateMonitoredTokensScope + createMonitoredTokens
├── 4-transfer.ts ← client.createTransaction + polling for confirmation
└── 5-bridge.ts ← AppKit + Circle Wallets adapter + CCTP Forwarding Service
Each script is short (under 150 lines) and the SDK call you care about is always near the bottom, after a header that explains the concept.
| Package | What it does |
|---|---|
@circle-fin/developer-controlled-wallets |
Create + manage wallets, sign transactions, read balances |
@circle-fin/app-kit |
High-level bridge operations across chains |
@circle-fin/adapter-circle-wallets |
Lets App Kit sign using your developer-controlled wallets |
You never touch viem, ethers, or web3.js directly. You also never touch a private key — Circle holds them in HSMs and signs on your behalf when you call the SDK.
unable to get local issuer certificate
The npm scripts already prefix NODE_TLS_REJECT_UNAUTHORIZED=0 to handle
corporate proxies / SSL inspection. If you're running tsx directly, set
that env var yourself.
wallet-state.json not found
Run step 1 first: npm run 1-create-wallets.
Token not found in wallet balance after 8 attempts (step 3)
The Circle faucet hasn't landed your EURC yet. Re-run step 2 and wait until
both USDC and EURC show up before running step 3.
Bridge fee estimation fails The script falls back to bridging the net amount as-is — your recipient may receive slightly less than 1 USDC after fees. Re-run a few minutes later if you want the exact-amount behaviour.
Bridge to Solana ends with mint [error]
Solana mints require the recipient to have a USDC Associated Token Account
(ATA) — without one, there is nowhere on-chain for the bridged USDC to
land. A freshly-created Circle Solana wallet does not have an ATA yet. Fund
the address with a small amount of USDC from a faucet first (the faucet will
create the ATA for you) and then re-run the bridge:
https://faucet.circle.com → select Solana Devnet.
Want to start over?
npm run reset # removes wallet-state.jsonYour wallets stay in Circle's database — only the local pointer file is removed. To fully clean up, archive the wallet set in the Circle Console.
After the workshop:
- Try mainnet by swapping
TEST_API_KEY:…for aLIVE_API_KEY:…and adjusting the chain identifiers (ARCinstead ofARC-TESTNET,BASEinstead ofBASE-SEPOLIA, etc.) - Add a third wallet to the set and chain transfers (A → B → C)
- Sponsor gas with Circle's Gas Station so users don't need a USDC balance to send their first transaction
- Build a webhook listener for
transactions.outbound.confirmedevents so your app reacts to on-chain state without polling
Documentation:
- Developer Controlled Wallets: https://developers.circle.com/wallets/dev-controlled
- App Kit & CCTP: https://docs.arc.network/app-kit
- Arc Testnet: https://docs.arc.network