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
| Scenario | Server-side DCW | Widget (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
- 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.
- A Circle API key that authorises your backend to sign transactions on that wallet.
- The
@usewhisk/corepackage. No React. The engine is pure TypeScript. - 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.
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.
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.quoteto 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.
Related
Error handling & recovery
How Whisk keeps user funds safe when something fails — the recovery primitives that defend against burned-but-not-minted, dropped tabs, and stuck attestations.
User-controlled wallets
Onboard users with email, Google, or PIN — give them a Circle MPC wallet, then drive sends from your backend with @usewhisk/core handling the routing.
