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:| Endpoint | Task Type | Purpose |
|---|---|---|
POST /tasks | Any | Generic task creation (bridge, generic) |
POST /tasks/payroll/init | Payroll | Validate + batch before execution |
POST /tasks/swap/init | Swap | Legacy swap task, disabled by default |
POST /tasks/liquidity/init | Liquidity | Legacy LP task, disabled by default |
POST /tasks/fx/execute | FX | Execute FX trade |
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:
| Task Type | Queue | Backoff |
|---|---|---|
payroll | payroll | 1s exponential |
swap | swap | 1s exponential |
bridge | bridge | 5s exponential |
liquidity | swap | 1s exponential |
fx | swap | 1s exponential |
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):| Condition | Final Status |
|---|---|
All completed | executed |
All failed | failed |
| Mixed | partial |
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.
Failure Scenario: Bridge Timeout
A user bridges 100 USDC from ETH-SEPOLIA to SOLANA-DEVNET. 1.POST /tasks with type: "bridge". Task created, assigned, enqueued to bridge queue.
2. BridgeWorker picks the job. BridgeAgent calls CircleBridgeService.initiateBridge().
3. The CCTP burn transaction is submitted to Sepolia. Sepolia requires 65-block confirmation (~13 minutes).
4. The Bridge Kit’s internal timeout (configured at 600s) expires before confirmation completes.
5. CircleBridgeService throws. The error propagates:
failed.
8. The idempotency guard prevents double-execution: if attempt 2 somehow reaches a task already marked in_progress, it is skipped.
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 a single multi-step operation (burn → attest → mint). The Bridge Kit manages the step progression internally. There is no benefit to splitting it into separate poll jobs — the entire operation either succeeds or fails as a unit. Sync execution simplifies the result contract.
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