Skip to main content

Wallet Modes

WizPay supports two signing models. The mode determines who holds the signing key and where transaction construction happens.

Mode Selection

The wallet mode is read from task.payload.walletMode by ExecutionRouterService:
if (walletMode === 'PASSKEY') → PasskeyEngineService
elseAgentRouterService  // default: W3S
When walletMode is absent (all tasks created before the field was introduced), the system defaults to W3S. Zero breaking changes.

W3S (Custodial)

Circle Wallet-as-a-Service. The backend has signing authority. Signing model:
  • Backend holds a Circle API key and entity secret.
  • The entity secret allows the backend to sign transactions on behalf of developer-controlled wallets.
  • The user does not approve individual transactions.
Flow:
  1. User authenticates (Google/Email) → receives Circle userToken + encryptionKey.
  2. Frontend sends userToken to backend with each wallet operation.
  3. Agent calls CircleService.transfer() → Circle signs and submits on-chain.
Characteristics:
  • Backend signs. No client-side signing.
  • userToken required for wallet identification.
  • walletId required per operation.
  • Supports EVM (ARC-TESTNET, ETH-SEPOLIA) and Solana (SOLANA-DEVNET).
Mobile session recovery: On mobile browsers, Circle SDK sessions can silently expire when the browser is backgrounded or the device goes offline. The frontend provider layer handles this transparently:
  • useMobileRecovery listens to visibilitychange, focus, pageshow, and online events. On each trigger (throttled to prevent flooding) it calls rearmSdkForSession to re-attach the current auth token to the SDK instance.
  • ensureSessionReady() is called before every Circle-mode operation (transaction execution, bridge, typed data signing). If the session is stale, it re-arms the SDK and refreshes the wallet list before the operation proceeds.
  • If a Circle operation returns a recoverable session error (code 155706 or an invalid-device code), withRecoveredSession calls ensureSessionReady and retries the operation exactly once. The retry is transparent to the caller.
Wallet provisioning endpoints:
EndpointPurpose
POST /wallets/initializeCreate wallet set + wallets for a user
POST /wallets/syncSync existing wallets from Circle
POST /wallets/ensureGet or create wallet for a chain (EVM/SOLANA)

PASSKEY (Client-Controlled)

Circle modular Account Abstraction wallet. The user holds the signing key. Signing model:
  • User authenticates with a WebAuthn passkey (biometric or hardware key).
  • The backend has no signing authority over the user’s wallet.
  • For operations requiring the user’s signature, the backend returns unsigned intents.
Per-operation behavior:
OperationBackend ActionClient Action
BridgeRecords CCTP intent, returns parametersUser signs and submits burn tx via AA wallet
Payroll (EVM)Submits ERC-20 transfers from backend treasury (BACKEND_PRIVATE_KEY)None — treasury pre-funded by company
Payroll (Solana)Builds unsigned SPL transfer intentsUser signs and broadcasts each intent
SwapPrepares swap payload via DexServiceUser submits via AA wallet
Characteristics:
  • No Circle userToken, tokenId, or W3S session credentials.
  • No walletId required.
  • Passkey AA wallets are EVM-only (ARC-TESTNET, ETH-SEPOLIA).
  • Solana operations require client-side signing.

Comparison

AspectW3SPASSKEY
Key holderBackend (Circle entity secret)User (passkey)
Client signingNeverBridge, Solana payroll, swap
Circle sessionRequired (userToken)Not used
walletIdRequiredNot required
ChainsEVM + SolanaEVM (AA) + Solana (client-sign)
Bridge executionBackend calls Circle Bridge KitFrontend executes CCTP directly
Payroll executionCircleService.transfer()Treasury key (EVM) / unsigned intents (Solana)
DefaultYesMust be explicitly set

External Signer Bridge

External browser wallets are not a third walletMode. They are a bridge execution mode used when the source wallet is a connected EVM wallet or an injected Solana wallet.
  • The browser executes the burn, attestation, and mint flow with public Circle bridge tooling.
  • The backend still accepts a best-effort POST /tasks audit record for traceability.
  • These audit tasks require walletAddress but do not require walletId.
  • Solana support is provider-agnostic: any compatible injected Solana wallet can be used, not just Phantom.
  • NEXT_PUBLIC_CIRCLE_API_PROXY_ENABLED=true enables the same-origin /api/circle/proxy fallback when the deployed Next.js runtime serves that route. When the flag is unset, bridge clients use direct Circle API requests only.
  • Production reverse proxies must route /api/circle/proxy to the frontend Next.js app before any generic backend /api/ rule; otherwise external ETH-SEPOLIA -> ARC-TESTNET transfers can silently fall back to slow full-finality burns.

Isolation

The ExecutionRouterService is the only component aware of wallet modes.
  • Agents do not check walletMode. They receive tasks through the router and execute.
  • The orchestrator does not check walletMode. It calls the execution router.
  • Workers do not check walletMode. They call the orchestrator.
Adding a new wallet mode requires:
  1. Extend the WalletMode type union in task.types.ts.
  2. Add a case in ExecutionRouterService.resolveWalletMode().
  3. Implement the engine service.
No changes to agents, orchestrator, or workers.