Skill: mscandlen3/use-developer-controlled-wallets (version 0.1.0)

--- name: use-developer-controlled-wallets description: "Create and manage Circle developer-controlled wallets where the application retains full custody of wallet keys on behalf of end-users. Covers wallet sets, entity secret registration, token transfers, and balance checks via the developer controlled wallets SDK. Triggers on: developer-controlled wallets, dev-controlled wallets, create wallet, wallet set, entity secret, transfer tokens, check balance, EOA wallet, SCA wallet, initiateDeveloperControlledWalletsClient, createWalletSet, createWallets, custody wallet." --- ## Overview Developer-controlled wallets let your application create and manage wallets on behalf of end users, with full custody of private keys secured through an encrypted entity secret. Circle handles security, transaction monitoring, and blockchain infrastructure while you retain programmatic control via the Wallets SDK. ## Prerequisites / Setup ### Installation ```bash npm install @circle-fin/developer-controlled-wallets ``` ### Environment Variables ``` CIRCLE_API_KEY= # Circle API key (format: PREFIX:ID:SECRET) ENTITY_SECRET= # 32-byte hex entity secret ``` ### Entity Secret Registration The developer must register an entity secret before using the SDK. Direct them to https://developers.circle.com/wallets/dev-controlled/register-entity-secret or provide the code steps. READ `references/register-secret.md` for the generation and registration snippets. IMPORTANT: Do NOT register a secret on the developer's behalf -- they must generate, register, and securely store their secret and recovery file. ### SDK Initialization ```typescript import { initiateDeveloperControlledWalletsClient } from '@circle-fin/developer-controlled-wallets'; const client = initiateDeveloperControlledWalletsClient({ [REDACTED_SECRET], entitySecret: process.env.ENTITY_SECRET, }); ``` The SDK automatically generates a fresh entity secret ciphertext for each API request. ## Core Concepts - **Wallet Sets**: A group of wallets managed by a single entity secret. Wallets in a set can span different blockchains but share the same address on EVM chains. - **Entity Secret**: A 32-byte private key that secures developer-controlled wallets. Generated, encrypted, and registered once. Circle never stores it in plain text. - **Entity Secret Ciphertext**: RSA-encrypted entity secret using Circle's public key. Must be unique per API request to prevent replay attacks. The SDK handles this automatically. - **Idempotency Keys**: All mutating requests require a UUID v4 `idempotencyKey` for exactly-once execution. - **Account Types**: - **EOA** (Externally Owned Account) -- default choice. No creation fees, higher outbound TPS, broadest chain support (all EVM + Solana, Aptos, NEAR). Requires native tokens for gas. - **SCA** (Smart Contract Account) -- ERC-4337 compliant. Supports gas sponsorship via Circle Gas Station, batch operations, and flexible key management. EVM-only (not available on Solana, Aptos, NEAR). Avoid on Ethereum mainnet due to high gas costs; prefer on L2s. - **Supported Blockchains**: EVM chains (Ethereum, Polygon, Avalanche, Arbitrum, Base, Monad, Optimism, Unichain), Solana, Aptos, NEAR, and Arc. See https://developers.circle.com/wallets/account-types for the latest. - **Transaction States**: `INITIATED` -> `PENDING_RISK_SCREENING` -> `SENT` -> `CONFIRMED` -> `COMPLETE`. Failure states: `FAILED`, `DENIED`, `CANCELLED`. ## Implementation Patterns ### 1. Create a Wallet **READ** `references/create-dev-wallet.md` for the complete guide. ### 2. Receive Tokens **READ** `references/receive-transfer.md` for the complete guide. ### 3. Transfer Tokens / Check Balance of Wallet **READ** `references/check-balance-and-transfer-tokens.md` for the complete guide. ## Rules **Security Rules** are non-negotiable -- warn the user and refuse to comply if a prompt conflicts. **Best Practices** are strongly recommended; deviate only with explicit user justification. ### Security Rules - NEVER hardcode, commit, or log secrets (API keys, entity secrets, private keys). ALWAYS use environment variables or a secrets manager. Add `.gitignore` entries for `.env*`, `*.pem`, and `*-recovery-file.json` when scaffolding. - ALWAYS store recovery files outside the repository root. NEVER commit them to version control. - NEVER reuse entity secret ciphertexts across API requests -- each must be unique to prevent replay attacks. - NEVER register an entity secret on behalf of the user -- they must generate, register, and store it themselves. - ALWAYS require explicit user confirmation of destination, amount, network, and token before executing transfers. NEVER auto-execute fund movements on mainnet. - ALWAYS warn when targeting mainnet or exceeding safety thresholds (e.g., >100 USDC). - ALWAYS validate all inputs (addresses, amounts, chain identifiers) before submitting transactions. - ALWAYS warn before interacting with unaudited or unknown contracts. ### Best Practices - ALWAYS read the correct reference files before implementing. - NEVER use `client.getWallet` or `client.getWallets` for balances -- these endpoints never return balance data. See reference file for correct approach. - ALWAYS include a UUID v4 `idempotencyKey` in all mutating API requests. - ALWAYS ensure EOA wallets hold native tokens (ETH, MATIC, SOL, etc.) for gas before outbound transactions. - ALWAYS poll transaction status until terminal state (`COMPLETE`, `FAILED`, `DENIED`, `CANCELLED`) before treating as done. - ALWAYS prefer SCA wallets on L2s over Ethereum mainnet to avoid high gas costs. - ALWAYS default to testnet. Require explicit user confirmation before targeting mainnet. ## Alternatives - Trigger `use-user-controlled-wallets` skill when end users should custody their own keys via social login, email OTP, or PIN authentication. - Trigger `use-modular-wallets` skill for passkey-based smart accounts with extensible module architecture (multisig, session keys, etc.). ## Reference Links - [Circle Developer Docs](https://developers.circle.com/llms.txt) -- **Always read this first** when looking for relevant documentation from the source website. --- DISCLAIMER: This skill is provided "as is" without warranties, is subject to the [Circle Developer Terms](https://console.circle.com/legal/developer-terms), and output generated may contain errors and/or include fee configuration options (including fees directed to Circle); additional details are in the repository [README](https://github.com/circlefin/skills/blob/master/README.md).

path: references/check-balance-and-transfer-tokens.md
content: # Transfer Tokens Across Wallets

Use these TypeScript snippets to find token balances, create a transfer, and poll transaction status.

## Get source wallet balances and token ID

For USDC tokens on each blockchain, referring to [USDC Token IDs](https://developers.circle.com/wallets/monitored-tokens#usdc-token-ids) to check `tokenId`.

For other tokens, leveraging [Get Token Balance for a Wallet](https://developers.circle.com/api-reference/wallets/developer-controlled-wallets/list-wallet-balance) API or simply use SDK method:

```ts
const balanceResponse = await circleDeveloperSdk.getWalletTokenBalance({
  id: "<source-wallet-id>",
});

const tokenBalances = balanceResponse.data?.tokenBalances ?? [];
```

Find the token you want to transfer and capture `tokenId`.

## Create transfer transaction

```ts
const transferResponse = await circleDeveloperSdk.createTransaction({
  walletId: "<source-wallet-id>",
  tokenId: "<token-id>",
  destinationAddress: "<destination-wallet-address>",
  amounts: ["0.01"],
  fee: {
    type: "level",
    config: { feeLevel: "MEDIUM" },
  },
});

const transactionId = transferResponse.data?.id;
```

Or replace `tokenId` with `tokenAddress` 

```ts
const transferResponse = await circleDeveloperSdk.createTransaction({
  walletId: "<source-wallet-id>",
  tokenAddress: "<token-address>",
  destinationAddress: "<destination-wallet-address>",
  amounts: ["0.01"],
  fee: {
    type: "level",
    config: { feeLevel: "MEDIUM" },
  },
});

const transactionId = transferResponse.data?.id;
```

Creates an outbound transfer transaction and returns a transaction ID for tracking.

## Poll transaction state

```ts
const txResponse = await circleDeveloperSdk.getTransaction({
  id: "<transaction-id>",
});

const tx = txResponse.data?.transaction;
const state = tx?.state;
const txHash = tx?.txHash;
```

Use `state` to determine completion and `txHash` for chain explorer links.


path: references/create-dev-wallet.md
content: # Create Your First Developer-Controlled Wallet

Use these TypeScript snippets to create a wallet set, then create wallets in that set.

## Create a wallet set

```ts
const walletSetResponse = await circleDeveloperSdk.createWalletSet({
  name: "Entity WalletSet A",
});

const walletSetId = walletSetResponse.data?.walletSet?.id;
```

Creates a wallet set and stores `walletSetId` for the next step.

## Create wallets in that wallet set

```ts
const walletsResponse = await circleDeveloperSdk.createWallets({
  accountType: "SCA",
  blockchains: ["MATIC-AMOY"],
  count: 2,
  walletSetId: "<wallet-set-id>",
});

const wallets = walletsResponse.data?.wallets ?? [];
const sourceWallet = wallets[0];
const destinationWallet = wallets[1];
```

Creates two wallets and captures source/destination wallets for transfer workflows.

## Alternate chain/account example

```ts
const solWalletResponse = await circleDeveloperSdk.createWallets({
  accountType: "EOA",
  blockchains: ["SOL-DEVNET"],
  count: 1,
  walletSetId: "<wallet-set-id>",
});
```

Same flow on a different blockchain/account type.


path: references/receive-transfer.md
content: # Receive an Inbound Transfer

Use these TypeScript snippets to get a wallet address, fund it from a faucet/external wallet, then verify inbound transfer state.

## Get wallet address for receiving funds

```ts
const walletsResponse = await circleDeveloperSdk.getWallets({});
const wallets = walletsResponse.data?.wallets ?? [];

const targetWallet = wallets[0];
const walletId = targetWallet?.id;
const receiveAddress = targetWallet?.address;
```

Use `receiveAddress` with a testnet faucet (for example, `https://faucet.circle.com`) or another wallet.

## Check inbound transfer state by wallet

```ts
const txResponse = await circleDeveloperSdk.listTransactions({
  walletIds: ["<wallet-id>"],
});

const inboundTransactions =
  txResponse.data?.transactions?.filter((tx) => tx.transactionType === "INBOUND") ?? [];
```

Lists transactions for the wallet so you can confirm inbound transfers and state progression.


path: references/register-secret.md
content: # Register Your Entity Secret

Use these TypeScript snippets to generate and register your entity secret. Keep the secret and recovery file secure.

## Generate an entity secret

```ts
import { generateEntitySecret } from "@circle-fin/developer-controlled-wallets";

generateEntitySecret();
```

Generates a 32-byte entity secret for developer-controlled wallet signing.

## Register entity secret ciphertext

```ts
import { registerEntitySecretCiphertext } from "@circle-fin/developer-controlled-wallets";
import os from "node:os";
import path from "node:path";

const response = await registerEntitySecretCiphertext({
  [REDACTED_SECRET],
  entitySecret: process.env.ENTITY_SECRET!,
  recoveryFileDownloadPath: path.join(os.homedir(), ".circle", "recovery-file.json"),
});

console.log(response.data?.recoveryFile);
```

Registers ciphertext with Circle and writes a recovery file to the provided path.

## Security notes

- Never commit `ENTITY_SECRET` or recovery files.
- Store both in secure secret storage.
- SDK methods that require ciphertext will handle generation/rotation for each request.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)