Skip to main content

Error Handling for Agents

Error Shape

All mutations return variant { ok : T, err : ApiError }.

ApiError = {
category : ErrorCategory -- match category for broad handling
code : text -- match this for specific handling
message : text -- human-readable, never match
metadata : opt [(text, text)] -- structured context
}

ErrorCategory = validation | authorization | state | resource
| rate_limit | external | admin | other

Match on code for programmatic handling. Use metadata for structured context (balances, limits, tick values). Ignore message in code paths — it's for logs/humans.

Decision Table

CategoryCodesAgent Response
validationTICK_*, AMOUNT_*, ZERO_AMOUNT, INVALID_*, EMPTY_BATCH, DUST_POSITIONFix input, retry
validation*_LIMIT_EXCEEDEDReduce batch size or close existing entities
validationINSUFFICIENT_BALANCERe-sync via get_user(), recalculate amounts. Check metadata.available and metadata.required
validationSLIPPAGE_EXCEEDEDWiden slippage or re-quote
stateORDER_NOT_FOUND, TRIGGER_NOT_FOUND, POSITION_NOT_FOUNDRe-sync state, entity was already closed
stateHALTED, DEGRADEDCheck routing_state.system_state, switch to exit-only or stop
stateUNINITIALIZED_MARKETMarket not ready, wait and retry
rate_limitSOFT_BLOCKED, HARD_BLOCKEDSee Rate Limits
externalEXTERNAL_SERVICE_ERRORCheck metadata.service for source. Retry with backoff
externalTRANSFER_FAILEDFunds remain in trading balance. Re-sync, retry withdrawal
authorizationUNAUTHORIZEDWrong identity or not permitted

Error Codes

Spot Canister

CodeMetadataWhen
TICK_OUT_OF_BOUNDStick, min, maxTick outside MIN_TICK..MAX_TICK
TICK_NOT_ALIGNEDtick, tick_spacingTick not multiple of spacing
INVALID_TICK_RANGE--tickLower >= tickUpper
INVALID_FEE_TIER--fee_pips not in 100, 500, 3000
AMOUNT_TOO_SMALLrequired_usd, provided_usdBelow min USD threshold
AMOUNT_TOO_LARGE--Exceeds 2^128-1
ZERO_AMOUNT--Amount is 0
AMOUNT_BELOW_FEE--Amount at or below ledger fee
EMPTY_BATCH--Zero-length order/trigger batch
ORDER_LIMIT_EXCEEDEDcurrent, limitWould exceed max orders per user
TRIGGER_LIMIT_EXCEEDEDcurrent, limitWould exceed max triggers per user
POSITION_LIMIT_EXCEEDEDcurrent, limitWould exceed max positions per user
INVALID_ROUTEreasonreason: too_many_pools, invalid_fee_tier, pool_not_viable, allocation_exceeds_input
TRIGGER_ALREADY_CROSSED--Trigger price already past current
ZERO_LIQUIDITY--Wrong token for current price range
DUST_POSITION--Partial withdrawal would leave under $10
INSUFFICIENT_BALANCEavailable, required, tokentoken: base or quote
SLIPPAGE_EXCEEDED--Execution price worse than limit
DUPLICATE_REQUEST--Idempotency key already used
TRANSFER_FAILEDserviceICRC transfer failed
INSUFFICIENT_ALLOWANCE--ICRC-2 allowance too low
ACCOUNT_NOT_FOUND--No trading account
ORDER_NOT_FOUND--Order ID doesn't exist
TRIGGER_NOT_FOUND--Trigger ID doesn't exist
POSITION_NOT_FOUND--Position ID doesn't exist
INVALID_STATE_TRANSITION--Entity in wrong state
UNINITIALIZED_MARKET--Market not yet initialized
HALTED--System halted
DEGRADED--System in degraded mode

All Canister Endpoints

CodeMetadataWhen
UNAUTHORIZED--Caller lacks permission
EXTERNAL_SERVICE_ERRORserviceInter-canister call failed
SOFT_BLOCKEDtries_left, blocked_until_msRate limit tier 1
HARD_BLOCKED--Rate limit tier 2

Batch Partial Failures

Batch endpoints (create_orders, create_triggers, cancel_orders, cancel_triggers) return per-item results. A batch call can succeed overall while individual items fail.

results = create_orders([], [spec_a, spec_b, spec_c], [])
// results.order_results[0] = { ok: ... } <- spec_a succeeded
// results.order_results[1] = { err: ... } <- spec_b failed validation
// results.order_results[2] = { ok: ... } <- spec_c succeeded
danger

Never assume a batch call is all-or-nothing. Always check per-item results.

Cancel + Create Budget

When passing cancel_ids to create_orders or create_triggers, cancels execute first. The creation step uses the freed balance from cancellations plus existing available balance. If you cancel 3 orders worth 1000 each and create 4 orders worth 1000 each, you need at least 1000 pre-existing available balance.

Pass-Through Fallback

pass_through_trade has a fallback mode: if the outbound ICRC transfer fails after execution, the output credits to the user's trading balance instead of their wallet. The agent should check trading balance after a pass-through trade to detect this case.

Retry Strategy

on error:
match category:
validation -> fix input, retry immediately
state -> re-sync state via get_user(), retry once
external -> retry with exponential backoff (500ms base, 30s cap)
rate_limit -> see Rate Limits page

For rate limit handling (tiers, forgiveness, recovery), see Rate Limits.