Skip to main content
In this guide, you build a script that:
  • Generates and registers an with Circle.
  • Creates a Wallet Set and an Wallet on Arc Testnet.
  • Appends credentials to the project .env for use in later guides.
  • After you fund the wallet (using the Circle Faucet), creates a second wallet in the same wallet set, sends USDC to it, and verifies both wallet balances.

Prerequisites

Before you begin, ensure you have:

Step 1. Set up your project

Create a project directory, install dependencies, and configure the project.

1.1. Create the project and install dependencies

In your terminal, create a directory for your dev-controlled wallet scripts. You’ll add more scripts to this folder in later guides.
mkdir dev-controlled-projects
cd dev-controlled-projects
npm init -y
npm pkg set type=module
Install the dev-controlled wallets SDK and dependencies for a TypeScript project:
npm install @circle-fin/developer-controlled-wallets typescript tsx
npm install --save-dev @types/node

1.2. Configure TypeScript (optional)

This step is optional. It helps prevent missing types in your IDE or editor.
Create a tsconfig.json file:
npx tsc --init
Then, update the tsconfig.json file:
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF

1.3. Set environment variables

Create a .env file in the project directory:
touch .env
Then, add your API key:
CIRCLE_API_KEY=YOUR_API_KEY
Where YOUR_API_KEY is your actual Circle Developer API key.
Prefer editing .env files in your IDE or editor so credentials are not leaked to your shell history.

Step 2. Create the script

Skip ahead to the full script if you prefer a fast-track option.
Create a file named create-wallet.ts. The script appends credentials to your project .env and writes the recovery file and wallet details to output/. Each subsection below adds a new functionality.

2.1. Add imports and constants

Add the required imports and constants:
create-wallet.ts
// create-wallet.ts

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
import {
  registerEntitySecretCiphertext,
  initiateDeveloperControlledWalletsClient,
  type TokenBlockchain,
} from "@circle-fin/developer-controlled-wallets";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT_DIR = path.join(__dirname, "output");
const WALLET_SET_NAME = "Circle Wallet Onboarding";

2.2. Register entity secret

Add the main function, generate a 32-byte Entity Secret, register it with Circle (the SDK saves the recovery file to output/), and append the secret to your project .env:
create-wallet.ts
async function main() {
  const apiKey = process.env.CIRCLE_API_KEY;
  if (!apiKey) {
    throw new Error(
      "CIRCLE_API_KEY is required. Add it to .env or set it as an environment variable."
    );
  }

  // Register Entity Secret
  console.log("Registering Entity Secret...");
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  const entitySecret = crypto.randomBytes(32).toString("hex");
  await registerEntitySecretCiphertext({
    apiKey,
    entitySecret,
    recoveryFileDownloadPath: OUTPUT_DIR,
  });
  const envPath = path.join(__dirname, ".env");
  fs.appendFileSync(envPath, `\nCIRCLE_ENTITY_SECRET=${entitySecret}\n`, "utf-8");
  console.log("Entity Secret registered.");

2.3. Create wallet set

Add the SDK client and create a Wallet Set.
create-wallet.ts
// Create Wallet Set
console.log("\nCreating Wallet Set...");
const client = initiateDeveloperControlledWalletsClient({
  apiKey,
  entitySecret,
});
const walletSet = (await client.createWalletSet({ name: WALLET_SET_NAME })).data
  ?.walletSet;
if (!walletSet?.id) {
  throw new Error("Wallet Set creation failed: no ID returned");
}
console.log("Wallet Set ID:", walletSet.id);

2.4. Create the wallet

Create a wallet on Arc Testnet, append its address and blockchain to your project .env, write wallet details to output/wallet-info.json, and prompt for the faucet:
create-wallet.ts
// Create Wallet
console.log("\nCreating Wallet on ARC-TESTNET...");
const wallet = (
  await client.createWallets({
    walletSetId: walletSet.id,
    blockchains: ["ARC-TESTNET"],
    count: 1,
    accountType: "EOA",
  })
).data?.wallets?.[0];
if (!wallet) {
  throw new Error("Wallet creation failed: no wallet returned");
}
console.log("Wallet ID:", wallet.id);
console.log("Address:", wallet.address);

fs.appendFileSync(
  envPath,
  `CIRCLE_WALLET_ADDRESS=${wallet.address}\n`,
  "utf-8",
);
fs.appendFileSync(
  envPath,
  `CIRCLE_WALLET_BLOCKCHAIN=${wallet.blockchain}\n`,
  "utf-8",
);
fs.writeFileSync(
  path.join(OUTPUT_DIR, "wallet-info.json"),
  JSON.stringify(wallet, null, 2),
  "utf-8",
);
console.log("\nBefore continuing, request test USDC from the faucet:");
console.log("  1. Go to https://faucet.circle.com");
console.log('  2. Select "Arc Testnet" network');
console.log(`  3. Paste your wallet address: ${wallet.address}`);
console.log('  4. Click "Send USDC"');

2.5. Pause for faucet

Add a readline prompt so the script waits until you have funded the wallet:
create-wallet.ts
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});
await new Promise<void>((resolve) =>
  rl.question("\nPress Enter once faucet tokens have been sent... ", () => {
    rl.close();
    resolve();
  }),
);

2.6. Create second wallet, send USDC, and verify balances

After you have funded the first wallet and pressed Enter, the script creates a second wallet in the same wallet set, sends USDC to it, waits for the transaction to complete, then prints both wallets’ token balances:
create-wallet.ts
  // Create second wallet
  console.log("\nCreating second wallet...");
  const secondWallet = (
    await client.createWallets({
      walletSetId: walletSet.id,
      blockchains: ["ARC-TESTNET"],
      count: 1,
      accountType: "EOA",
    })
  ).data?.wallets?.[0];
  if (!secondWallet) {
    throw new Error("Second wallet creation failed: no wallet returned");
  }
  console.log("Second wallet address:", secondWallet.address);

  // Send USDC to second wallet (Arc Testnet USDC token address)
  const ARC_TESTNET_USDC = "0x3600000000000000000000000000000000000000";
  console.log("\nSending 5 USDC to second wallet...");
  const txResponse = await client.createTransaction({
    blockchain: wallet.blockchain as TokenBlockchain,
    walletAddress: wallet.address,
    destinationAddress: secondWallet.address,
    amount: ["5"],
    tokenAddress: ARC_TESTNET_USDC,
    fee: { type: "level", config: { feeLevel: "MEDIUM" } },
  });
  const txId = txResponse.data?.id;
  if (!txId) throw new Error("Transaction creation failed: no ID returned");
  console.log("Transaction ID:", txId);

  // Poll until transaction reaches a terminal state
  const terminalStates = new Set([
    "COMPLETE",
    "FAILED",
    "CANCELLED",
    "DENIED",
  ]);
  let currentState: string | undefined = txResponse.data?.state;
  while (!currentState || !terminalStates.has(currentState)) {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    const poll = await client.getTransaction({ id: txId });
    const tx = poll.data?.transaction;
    currentState = tx?.state;
    console.log("Transaction state:", currentState);
    if (currentState === "COMPLETE" && tx?.txHash) {
      console.log(`Explorer: https://testnet.arcscan.app/tx/${tx.txHash}`);
    }
  }
  if (currentState !== "COMPLETE") {
    throw new Error(`Transaction ended in state: ${currentState}`);
  }

  // Verify both wallets' token balances
  console.log("\nSource wallet balances:");
  const srcBalances = (
    await client.getWalletTokenBalance({ id: wallet.id })
  ).data?.tokenBalances;
  for (const b of srcBalances ?? []) {
    console.log(`  ${b.token?.symbol ?? "Unknown"}: ${b.amount}`);
  }
  console.log("\nSecond wallet balances:");
  const secondBalances = (
    await client.getWalletTokenBalance({ id: secondWallet.id })
  ).data?.tokenBalances;
  for (const b of secondBalances ?? []) {
    console.log(`  ${b.token?.symbol ?? "Unknown"}: ${b.amount}`);
  }
  console.log("\nDone!");
}

main().catch((err) => {
  console.error("Error:", err.message || err);
  process.exit(1);
});

2.7. Copy the full script

The complete create-wallet.ts is below. Replace the contents with the full script below if you prefer:
create-wallet.ts
/**
 * create-wallet.ts: Create a Dev-Controlled Wallet
 *
 * 1. Register Entity Secret (appends CIRCLE_ENTITY_SECRET to .env)
 * 2. Create Wallet Set
 * 3. Create Wallet (appends CIRCLE_WALLET_ADDRESS and CIRCLE_WALLET_BLOCKCHAIN to .env)
 * 4. Pause for you to fund the wallet (faucet); then create a second wallet,
 *    send USDC to it, and verify both wallets' balances.
 *
 * Required .env (or environment):
 *   CIRCLE_API_KEY: Your Circle API key (https://console.circle.com)
 *
 * Usage:
 *   node --env-file=.env --import=tsx create-wallet.ts
 */

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
import {
  registerEntitySecretCiphertext,
  initiateDeveloperControlledWalletsClient,
  type TokenBlockchain,
} from "@circle-fin/developer-controlled-wallets";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT_DIR = path.join(__dirname, "output");
const WALLET_SET_NAME = "Circle Wallet Onboarding";

async function main() {
  const apiKey = process.env.CIRCLE_API_KEY;
  if (!apiKey) {
    throw new Error(
      "CIRCLE_API_KEY is required. Add it to .env or set it as an environment variable.",
    );
  }

  // Register Entity Secret
  console.log("Registering Entity Secret...");
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  const entitySecret = crypto.randomBytes(32).toString("hex");
  await registerEntitySecretCiphertext({
    apiKey,
    entitySecret,
    recoveryFileDownloadPath: OUTPUT_DIR,
  });
  const envPath = path.join(__dirname, ".env");
  fs.appendFileSync(
    envPath,
    `\nCIRCLE_ENTITY_SECRET=${entitySecret}\n`,
    "utf-8",
  );
  console.log("Entity Secret registered.");

  // Create Wallet Set
  console.log("\nCreating Wallet Set...");
  const client = initiateDeveloperControlledWalletsClient({
    apiKey,
    entitySecret,
  });
  const walletSet = (await client.createWalletSet({ name: WALLET_SET_NAME }))
    .data?.walletSet;
  if (!walletSet?.id) {
    throw new Error("Wallet Set creation failed: no ID returned");
  }
  console.log("Wallet Set ID:", walletSet.id);

  // Create Wallet
  console.log("\nCreating Wallet on ARC-TESTNET...");
  const wallet = (
    await client.createWallets({
      walletSetId: walletSet.id,
      blockchains: ["ARC-TESTNET"],
      count: 1,
      accountType: "EOA",
    })
  ).data?.wallets?.[0];
  if (!wallet) {
    throw new Error("Wallet creation failed: no wallet returned");
  }
  console.log("Wallet ID:", wallet.id);
  console.log("Address:", wallet.address);

  fs.appendFileSync(
    envPath,
    `CIRCLE_WALLET_ADDRESS=${wallet.address}\n`,
    "utf-8",
  );
  fs.appendFileSync(
    envPath,
    `CIRCLE_WALLET_BLOCKCHAIN=${wallet.blockchain}\n`,
    "utf-8",
  );
  fs.writeFileSync(
    path.join(OUTPUT_DIR, "wallet-info.json"),
    JSON.stringify(wallet, null, 2),
    "utf-8",
  );
  console.log("\nBefore continuing, request test USDC from the faucet:");
  console.log("  1. Go to https://faucet.circle.com");
  console.log('  2. Select "Arc Testnet" network');
  console.log(`  3. Paste your wallet address: ${wallet.address}`);
  console.log('  4. Click "Send USDC"');
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  await new Promise<void>((resolve) =>
    rl.question("\nPress Enter once faucet tokens have been sent... ", () => {
      rl.close();
      resolve();
    }),
  );

  // Create second wallet
  console.log("\nCreating second wallet...");
  const secondWallet = (
    await client.createWallets({
      walletSetId: walletSet.id,
      blockchains: ["ARC-TESTNET"],
      count: 1,
      accountType: "EOA",
    })
  ).data?.wallets?.[0];
  if (!secondWallet) {
    throw new Error("Second wallet creation failed: no wallet returned");
  }
  console.log("Second wallet address:", secondWallet.address);

  // Send USDC to second wallet (Arc Testnet USDC token address)
  const ARC_TESTNET_USDC = "0x3600000000000000000000000000000000000000";
  console.log("\nSending 5 USDC to second wallet...");
  const txResponse = await client.createTransaction({
    blockchain: wallet.blockchain as TokenBlockchain,
    walletAddress: wallet.address,
    destinationAddress: secondWallet.address,
    amount: ["5"],
    tokenAddress: ARC_TESTNET_USDC,
    fee: { type: "level", config: { feeLevel: "MEDIUM" } },
  });
  const txId = txResponse.data?.id;
  if (!txId) throw new Error("Transaction creation failed: no ID returned");
  console.log("Transaction ID:", txId);

  // Poll until transaction reaches a terminal state
  const terminalStates = new Set(["COMPLETE", "FAILED", "CANCELLED", "DENIED"]);
  let currentState: string | undefined = txResponse.data?.state;
  while (!currentState || !terminalStates.has(currentState)) {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    const poll = await client.getTransaction({ id: txId });
    const tx = poll.data?.transaction;
    currentState = tx?.state;
    console.log("Transaction state:", currentState);
    if (currentState === "COMPLETE" && tx?.txHash) {
      console.log(`Explorer: https://testnet.arcscan.app/tx/${tx.txHash}`);
    }
  }
  if (currentState !== "COMPLETE") {
    throw new Error(`Transaction ended in state: ${currentState}`);
  }

  // Verify both wallets' token balances
  console.log("\nSource wallet balances:");
  const srcBalances = (await client.getWalletTokenBalance({ id: wallet.id }))
    .data?.tokenBalances;
  for (const b of srcBalances ?? []) {
    console.log(`  ${b.token?.symbol ?? "Unknown"}: ${b.amount}`);
  }
  console.log("\nSecond wallet balances:");
  const secondBalances = (
    await client.getWalletTokenBalance({ id: secondWallet.id })
  ).data?.tokenBalances;
  for (const b of secondBalances ?? []) {
    console.log(`  ${b.token?.symbol ?? "Unknown"}: ${b.amount}`);
  }
  console.log("\nDone!");
}

main().catch((err) => {
  console.error("Error:", err.message || err);
  process.exit(1);
});
The script performs the following actions:
  • Reads your API key from .env (via --env-file) or the environment.
  • Generates a 32-byte Entity Secret, registers it with Circle (recovery file is saved to output/), and appends CIRCLE_ENTITY_SECRET to your project .env.
    See Entity Secret Management for more details on generation, rotation, and recovery.
  • Initializes the dev-controlled wallets client with your API key and Entity Secret.
  • Creates a wallet set and a wallet on Arc Testnet; appends CIRCLE_WALLET_ADDRESS and CIRCLE_WALLET_BLOCKCHAIN to .env and writes output/wallet-info.json.
  • Prompts you to fund the wallet at the Circle Faucet (Arc Testnet), then waits until you press Enter to continue.
  • Creates a second wallet in the same wallet set, sends USDC to it, polls until the transaction is complete, then prints both wallets’ token balances.

Step 3. Run the script

3.1. Launch in your terminal

From your project directory, run:
node --env-file=.env --import=tsx create-wallet.ts
The script registers the Entity Secret, creates the wallet, appends credentials to your .env, writes the recovery file and wallet-info.json to output/, then pauses. You should see output like this:
Registering Entity Secret...
Entity Secret registered.

Creating Wallet Set...
Wallet Set ID: ...

Creating Wallet on ARC-TESTNET...
Wallet ID: ...
Address: 0x...

Before continuing, request test USDC from the faucet:
  1. Go to https://faucet.circle.com
  2. Select "Arc Testnet" network
  3. Paste your wallet address: 0x...
  4. Click "Send USDC"

Press Enter once faucet tokens have been sent...

3.2. Fund the wallet

While the script is waiting, fund the wallet using the Circle Faucet:
  1. Copy the wallet address from the script output (or from .env or output/wallet-info.json).
  2. Visit the official Circle Faucet.
  3. Select Arc Testnet as the blockchain network.
  4. Paste the wallet address in the Send to field.
  5. Click Send USDC.
After the faucet has sent the tokens, return to the terminal and press Enter. The script continues and creates a second wallet, sends USDC to it, then prints both wallets’ token balances. You should see output similar to:
Creating second wallet...
Second wallet address: 0x...

Sending 5 USDC to second wallet...
Transaction ID: ...
Transaction state: COMPLETE
Explorer: https://testnet.arcscan.app/tx/0x...

Source wallet balances:
  USDC: 14.95...

Second wallet balances:
  USDC: 5

Done!
The script verifies both wallets’ balances at the end. You can also verify any address on the Arc Testnet explorer by pasting it in the search field.

Output files

After a successful run:
  • .env: The script appends CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ADDRESS, and CIRCLE_WALLET_BLOCKCHAIN to your existing .env. Use this same .env for follow-up guides (for example, Send a Transaction to Transfer Assets).
  • output/: The SDK writes the recovery file here; the script writes wallet-info.json (wallet details, including address and blockchain).
Never commit .env or output/ to version control. Store the Entity Secret and recovery file in a secure password manager.

Summary

In this guide, you created a dev-controlled wallet on Arc Testnet. The script appends credentials to your project .env, writes the recovery file and wallet-info.json to output/, and in a single run sends USDC to a second wallet and verifies both balances. Main points to remember:
  • Wallet ready: This guide creates a wallet (and a second one in the same wallet set) that you can use in successive guides.
  • Single run: The script pauses so you can fund the first wallet at the Circle Faucet; after you press Enter, it creates the second wallet, sends USDC to it, and prints both wallets’ token balances.
  • Credentials: Entity Secret and wallet address/blockchain are in your project .env; the recovery file and wallet details are in output/. Never commit them and always keep them secure.
  • Follow-up guides: Use the same project and .env.