Skip to content

Autopilot API

All under /api/defi/autopilot. Controller: bz-backend/controllers/defi/autopilotController.js.

Mutating routes require a wallet signature

Unlike the legacy DCA routes, a forged mutation here would change what the executor does with escrowed funds. middleware/walletAuth.js guards create, update, pause and resume.

Product page: Autopilot. Contract: Autopilot vault. Policy rules: policy engine.

Route summary

MethodPathAuth
POST/defi/autopilot/preview
POST/defi/autopilot/createwalletAuth('autopilot-create')
POST/defi/autopilot/confirm
POST/defi/autopilot/update/:idwalletAuth('autopilot-update')
POST/defi/autopilot/pause/:idwalletAuth('autopilot-pause')
POST/defi/autopilot/resume/:idwalletAuth('autopilot-resume')
GET/defi/autopilot/price/:chainId/:token
POST/defi/autopilot/sync/:chainId/:agentId
GET/defi/autopilot/list/:wallet
GET/defi/autopilot/:chainId/:agentId
GET/defi/autopilot/:chainId/:agentId/activity

Route order matters

/defi/autopilot/price/:chainId/:token, /sync/:chainId/:agentId, /list/:wallet and /:chainId/:agentId/activity are all registered before the catch-all /:chainId/:agentId. Adding a new two-segment GET route after it will be shadowed.

Wallet-signature auth

Every mutating request carries:

body = { ...payload, wallet, timestamp, signature }
signedBody = body minus `signature`
digest     = sha256(utf8(JSON.stringify(sortDeep(signedBody))))
message    = `Blazpay <action> ${digest} ${timestamp}`
signature  = personal_sign(message)           // EIP-191
PropertyValue
Window±5 minutes
Replay guardRedis, TTL 600 s (single-use)
Scopeaction is in the message — a signed pause cannot replay as create
Resultreq.authWallet, lowercased and verified

The action strings are exactly autopilot-create, autopilot-update, autopilot-pause, autopilot-resume.

The frontend mirror must be byte-identical

defi-dex/src/utils/autopilotPolicy.ts mirrors both this digest computation and the policy canonicalisation. Any difference in key order or whitespace produces a digest the backend rejects.

POST /defi/autopilot/preview

Validate a draft without writing anything.

Body: { policyDraft }

Runs validatePolicy, then usdLimitsToTokenUnits(policy) to convert the USD limits in the policy into the budgetToken units the contract needs.

Response: the validated policy plus the resolved limits, or the validation error list.

This is where USD becomes token units

The policy speaks USD (maxPerTradeUsd, maxPerDayUsd) because that is what a user thinks in. The contract's Limits struct is denominated in budgetToken units, because the contract is deliberately oracle-free. usdLimitsToTokenUnits bridges the two at creation time — which means a large price move after creation changes the effective USD cap.

POST /defi/autopilot/create

Body: { policy, policyHash, policySignature, depositAmount, wallet, timestamp, signature }

  1. walletAuth verifies the request signature.
  2. validatePolicy(policy) — structural validation.
  3. verifyPolicySignature — the user signed Blazpay Autopilot policy {hash} for agent new on chain {chainId}.
  4. usdLimitsToTokenUnits resolves limits.
  5. Returns the parameters the frontend needs for the on-chain createAgent call.

Two signatures, two purposes

signature authenticates the request. policySignature binds the user to the policy hash that goes on-chain. They are not interchangeable.

POST /defi/autopilot/confirm

Body: { chainId, txHash }

Called after the on-chain createAgent transaction confirms. Reads the receipt, extracts the AgentCreated event's agentId, and links the Mongo document to the on-chain agent. No auth — a forged confirm can only associate a real on-chain event.

POST /defi/autopilot/update/:id

Body: { policy, policyHash, policySignature, wallet, timestamp, signature }

Same validation chain as create. The on-chain updatePolicy call is the user's own transaction; this records the new policy so the engine can act on it.

The engine refuses to act on a hash mismatch

If the Mongo policy's hash differs from AgentAccount.policyHash on-chain, autopilotCron skips the agent. So an update that never lands on-chain silently pauses the agent rather than executing an unsigned policy.

POST /defi/autopilot/pause/:id · resume/:id

Both are setEngineStatus(status, verb):

js
export const pauseAutopilot  = setEngineStatus('paused', 'paused')
export const resumeAutopilot = setEngineStatus('active', 'resumed')

This is the engine status, not the contract's active flag

Pausing here stops the backend from evaluating triggers. The contract's setActive(false) is a separate, stronger switch the user calls directly — the executor cannot trade an inactive agent even if the backend tried. Belt and braces.

GET /defi/autopilot/price/:chainId/:token

Current USD price for a token, from the Autopilot price service (CoinMarketCap batched into a Redis rolling series). Used by the creation wizard to show what a limit means in token terms.

POST /defi/autopilot/sync/:chainId/:agentId

Force an immediate on-chain → Mongo sync via services/autopilotSync.js, rather than waiting for the next cron tick. Mirrors AgentAccount state (limits, policyHash, active, spentToday, dayStart, tradesToday) and balances.

Useful after a user's own transaction — deposit, withdraw, setActive — so the UI does not show stale state.

GET /defi/autopilot/list/:wallet

Every agent belonging to a wallet, across chains.

GET /defi/autopilot/:chainId/:agentId

One agent: policy, synced on-chain state, balances, lastFillAt per strategy.

GET /defi/autopilot/:chainId/:agentId/activity

The activity feed — from models/autopilotTradeModel.js and autopilotEventModel.js. Includes executed trades, failures, safety blocks and policy changes.

Policy document

Full field-by-field rules: policy engine. Summary:

json
{
  "version": 1,
  "wallet": "0x…",
  "chainId": 42161,
  "budgetToken": "0x…",
  "allowTokens": ["0x…"],
  "limits": { "maxPerTradeUsd": 100, "maxPerDayUsd": 500,
              "maxTradesPerDay": 4, "expiry": 1790000000 },
  "safety": { "minTokenScore": 70 },
  "slippageBps": 100,
  "strategies": [ { "id": "s1", "type": "dca",  } ]
}
ConstraintValue
allowTokens≤ 20, must include budgetToken, no duplicates
strategies≤ 10, unique ids, order is precedence
expiryFuture, ≤ 180 days
slippageBps10–300
safety.minTokenScore0–100
Every token a strategy touchesMust be allow-listed
AmountsPositive numeric strings

Strategy types: dca, dip_buy, take_profit, stop_loss, rebalance.

Deployment gate

No trade executes until the owner allow-lists routers

setRouters([...], true) must be called on the vault. It is fail-safe: with an empty allowedRouter map every trade reverts with RouterNotAllowed. This is the current state on Robinhood Chain (4663). Use contract-deployment/scripts/probe-autopilot-routers.cjs to find which routers deliver in-transaction output on a given chain.

AUTOPILOT_ADDRESSES in utils/constants.js — the cron skips chains with an empty address, so Base (8453) is inert.

In BlazAI

The AUTOPILOT intent, action autopilot, card swapAI/actions/AutopilotCard.tsx.

  • v1/services/autopilotPolicy.service.js → buildPolicyDraft — the drafting model in JSON mode emits symbols and numbers only; the backend resolves addresses and runs validatePolicy. An LLM therefore cannot produce a signable policy.
  • v1/services/autopilot.service.js → handleAutopilot — sub-actions create, status, pause, resume, close, help. Read-and-guide sub-actions never auto-execute.
  • Create runs inline in chat (sign → create → confirm); management deep-links to /defi/autopilot.

Testing

SuiteCommand
Policy toolkitnode scripts/test-policy.js
Wallet authnode scripts/test-wallet-auth.js
Trigger enginenode scripts/test-autopilot-engine.js — 13 passing
Contractnpx hardhat test test/autopilot.test.cjs — 61 passing
Seed settingsnode scripts/seed-autopilot-settings.js

Failure modes

SymptomCause
401 signature expired (±5min window)Clock skew, or a stale request
401 on a valid-looking signatureThe frontend digest diverged from walletAuth.js
Validation errors on a reasonable policyCheck allowTokens includes budgetToken, weights sum to 10000, amounts are strings
Agent never tradesRouters not allow-listed — check first
Agent silently stopsPolicy hash mismatch between Mongo and chain
RouterNotAllowed revertThe route's targetContract or spender is not allow-listed
OutputBelowMinimum revertIntent/solver route — the probe should have filtered it
ExceedsMaxPerDay revertRolling 24-hour cap reached
PolicyExpired revertPast limits.expiry
Stale UI after a user transactionCall POST /sync/:chainId/:agentId

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.