whisk
Concepts

Server-side sends

Drive Whisk from a backend with Circle Developer-Controlled Wallets, no React or user signature.

<WhiskSend> is built for a connected user wallet. Most integrations look like that — the visitor opens your page, clicks Send, and their wallet pops a signature prompt. But not every USDC payment is user-initiated. Treasury moves, scheduled disbursements, batch payouts, marketplace settlement, employee payroll — these are all flows where your backend needs to send USDC on a schedule or in response to a server event. No browser, no user signature.

Whisk supports this path natively because the engine and the React layer are decoupled. You import the engine on the server, hand it a Circle Developer-Controlled Wallet adapter, and call the same send() / bridge() methods the widget calls.

When this is the right tool

ScenarioServer-side DCWWidget (user wallet)
Customer pays you for an order
Donor sends to your treasury
You pay out 200 contractors at end of month
Scheduled stablecoin disbursement (DAO grant, salary)
Marketplace splits settlement to sellers
Internal treasury rebalancing across chains

If a user needs to sign, the widget is right. If your service needs to sign on behalf of a wallet you control, you want this page.

The pieces you need

  1. A Circle Developer-Controlled Wallet with USDC funded on the chain you'll be sending from. Created via the Circle Console or the Circle API. Each wallet has an address per chain.
  2. A Circle API key that authorises your backend to sign transactions on that wallet.
  3. The @usewhisk/core package. No React. The engine is pure TypeScript.
  4. The Circle Wallets adapter package, @circle-fin/adapter-circle-wallets.

Install

$ pnpm add @usewhisk/core @circle-fin/adapter-circle-wallets

Wire it up

Three steps: create the adapter, create the engine, send.

server/usdc-sender.ts
import { createWhisk } from "@usewhisk/core";
import { createCircleWalletsAdapter } from "@circle-fin/adapter-circle-wallets";

const adapter = await createCircleWalletsAdapter({
  apiKey: process.env.CIRCLE_API_KEY!,
  entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
  walletId: process.env.CIRCLE_WALLET_ID!,
});

const engine = createWhisk({
  chains: ["Arc_Testnet", "Base_Sepolia"],
  defaultSourceChain: "Arc_Testnet",
});

export async function sendUsdc(toAddress: string, amount: string) {
  const recipient = await engine.resolve(toAddress, "Base_Sepolia");

  const quote = await engine.quote({
    recipient,
    amount,
    sourceChain: "Arc_Testnet",
    adapter,
  });

  const result = await engine.send({ quote, adapter });
  return result;
}

That's the entire integration. No React provider, no wallet connect modal, no signature popup — Circle signs via its API on the server when engine.send fires, and the function returns the final tx hash.

A real example: scheduled payouts

Drop the helper into a route handler or a cron job and you have a payroll runner.

app/api/payroll/run/route.ts
import { sendUsdc } from "@/server/usdc-sender";
import { db } from "@/server/db";

export async function POST() {
  const payees = await db.payee.findMany({
    where: { active: true, paidThisMonth: false },
  });

  const results: Array<{ id: string; ok: boolean; tx?: string }> = [];

  for (const p of payees) {
    try {
      const r = await sendUsdc(p.walletAddress, p.salary);
      if (r.kind === "success") {
        await db.payee.update({
          where: { id: p.id },
          data: { paidThisMonth: true, lastTx: r.finalTxHash },
        });
        results.push({ id: p.id, ok: true, tx: r.finalTxHash });
      } else {
        results.push({ id: p.id, ok: false });
      }
    } catch (err) {
      results.push({ id: p.id, ok: false });
    }
  }

  return Response.json({ results });
}

Hit /api/payroll/run from a Vercel cron, GitHub Actions workflow, or any scheduler — it iterates the payee list, signs each tx via the Circle DCW, and updates your database with the on-chain receipt. The widget itself is never involved.

What you don't get

The server-side path skips a few things the widget gives you for free. Worth knowing what you're trading away:

  • Per-user balance visibility. The DCW holds your money, not the recipient's. Use engine.quote to see fees + amounts before you commit, but you can't read someone else's wallet from this path.
  • Recipient confirmation UI. No user clicks Send; you commit the moment your code calls engine.send. Build idempotency keys and dry-run flags into your route handler if double-sending is a business risk.
  • Wallet ergonomics like ENS chip rendering. You're not in React; the resolver still works (it's pure TS), but the chip doesn't exist outside the widget.

Don't try to mount the widget against a DCW

A user-controlled wallet on the browser and a server-held DCW are fundamentally different signing models. Wiring a DCW adapter into <WhiskProvider> doesn't work because the React layer expects an appKitAdapter resolved from a connected client wallet (wagmi or Solana wallet-adapter), not a server credential. The architectural seam is: use the widget for user-initiated payments, the engine for backend-initiated payments.

User-Controlled Wallets (UCW)

UCW is the other half of Circle Wallets — embedded wallets where each user gets their own wallet authenticated via email, passkey, or OAuth, with Circle's MPC splitting the key share between Circle and the user. UCW is a fit for the widget (it's a client-side wallet from React's perspective), but support hasn't landed in Whisk yet. See the Components → ConnectModal roadmap note for status.

  • Engine — what createWhisk returns and how the React layer wraps it.
  • Routing — same-chain transfer vs CCTP bridge logic. Both paths are available server-side via engine.send.
  • Errors — typed errors thrown by engine.send during failure. Handle them the same way the widget does.

On this page