Execution Flow
Every request follows the same pipeline: Payload → Validation → Queue → Execution → Settlement.Step-by-Step
1. Payload Ingestion
The frontend submits a structured payload to one of the task endpoints:2. Validation
TaskController validates the request body using class-validator (whitelist mode, strict). Type-specific validation:
- Payroll —
PayrollValidationServicechecks recipient addresses, amounts, token compatibility. Invalid entries reject the entire payload. - Bridge —
OrchestratorService.normalizeBridgePayload()validates chains, addresses, amounts. Rejects same-chain bridges, non-USDC tokens, and invalid execution modes. ForbridgeExecutionMode: "external_signer",walletAddressis required whilewalletIdis optional. - Swap — Requires
tokenIn,tokenOut,amountIn,recipient.
WIZPAY_ENABLE_LEGACY_FX=true or
WIZPAY_ENABLE_LEGACY_LIQUIDITY=true must only be used for isolated
non-production testing. Official StableFX RFQ failures are terminal for the
request; the backend must not fall back to synthetic pricing or internal
reserves.
3. Task Creation
TaskService inserts a Task row with status created:
TaskUnit records are created atomically in a Prisma $transaction.
4. Queue Dispatch
OrchestratorService.handleTask() transitions the task to assigned and enqueues a job:
All jobs: 3 attempts,
removeOnComplete: 100, removeOnFail: 500.
5. Worker Pickup
A BullMQWorker picks the job and calls its Processor:
6. Idempotency Guard
executeTask() checks current status:
7. Agent Execution
The orchestrator routes through two layers:-
ExecutionRouterService— checkswalletMode:W3S(default) →AgentRouterServicePASSKEY→PasskeyEngineService
-
AgentRouterService— dispatches to the type-specific agent.
AgentExecutionResult.
External-wallet bridge tasks are a narrow exception: the browser can execute the bridge first, then submit POST /tasks with bridgeExecutionMode: "external_signer" so the backend stores validation output and audit metadata without requiring a Circle walletId.
8. Settlement
Sync path (swap, bridge, FX, liquidity):End-to-End Example: Payroll
A company pays 50 employees in USDC on ARC-TESTNET. 1. Init — Frontend callsPOST /tasks/payroll/init with 50 recipients.
2. Validation — Backend validates all addresses and amounts. Splits into 2 batches of 25.
3. Task Created — Task with totalUnits: 2, two TaskUnit records (index 0, 1).
4. Confirm — Frontend calls POST /tasks with the full payload. Task transitions: created → assigned → enqueued.
5. Worker — PayrollWorker picks the job. Orchestrator marks in_progress.
6. Agent — PayrollAgent iterates batch 0 (25 recipients):
- For each:
CircleService.transfer()→TaskService.appendTransaction()→QueueService.enqueueTransactionPoll() - Then batch 1 (25 recipients): same flow.
- Agent returns. Task stays
in_progress.
TxPollWorker processes 50 poll jobs over the next 30–120 seconds:
- Each job calls Circle API for tx status.
completed→ updateTaskTransaction, check if all terminal.- Still pending → re-enqueue with delay.
- 50/50 completed → task status:
executed - 48 completed, 2 failed → task status:
partial
GET /tasks/:id. Renders final status with per-recipient tx hashes.
External Wallet CCTP V2 Bridge
1. The Swap page validates an Arc Testnet hub-and-spoke route and creates a durable/bridge/intents record.
2. The connected browser wallet switches to the source testnet, approves the registered TokenMessenger V2 only when needed, and submits depositForBurn.
3. The backend verifies the source receipt and exact DepositForBurn event, then polls Circle’s sandbox attestation API and validates the returned CCTP message against the persisted intent.
4. The same connected browser wallet switches to the destination testnet and submits receiveMessage to the registered MessageTransmitter V2.
5. The backend verifies the successful destination receipt, exact receiveMessage(message, attestation) calldata, the V2 MessageReceived event from the configured MessageTransmitterV2, the consumed nonce, and the matching configured-USDC mint transfer before marking the lifecycle complete.
Transaction hashes are stored immediately in browser recovery storage and then bound to the backend intent. Reload restoration continues attestation polling or verifies an already-submitted destination transaction. When a new destination wallet authorization is still required, the UI offers the precise Complete mint on <network> action only after the backend proves the nonce is unused. A confirmed source burn is never submitted again.
Design Tradeoffs
Why async settlement for payroll but not for bridge?
Payroll involves N independent transfers. Each transfer is a separate on-chain transaction with its own confirmation timeline. Blocking the worker for all N confirmations would hold the queue slot for minutes. Instead, the agent submits all transfers rapidly and delegates confirmation to thetx_poll queue. This keeps worker concurrency high.
Bridge is not a worker task. It is a browser-signed, persisted state machine (approval → burn → attestation → mint) with backend receipt validation at each transition.
Why an idempotency guard instead of BullMQ’s built-in deduplication?
BullMQ’sjobId-based deduplication prevents duplicate enqueue, but does not prevent duplicate execution after a crash-restart. If a worker crashes after marking a task in_progress but before completing execution, BullMQ retries the job. The idempotency guard (check status === ASSIGNED) ensures the task is not re-executed if it has already progressed past the assignment phase.
Why route through OrchestratorService instead of calling agents directly from workers?
Centralized execution ensures:- Every task passes through the same idempotency guard
- Every status transition is logged
- Error handling is uniform (best-effort status update + re-throw)
- Adding new wallet modes requires changes in
ExecutionRouterServiceonly — not in every worker