Appearance
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
| Method | Path | Auth |
|---|---|---|
POST | /defi/autopilot/preview | — |
POST | /defi/autopilot/create | walletAuth('autopilot-create') |
POST | /defi/autopilot/confirm | — |
POST | /defi/autopilot/update/:id | walletAuth('autopilot-update') |
POST | /defi/autopilot/pause/:id | walletAuth('autopilot-pause') |
POST | /defi/autopilot/resume/:id | walletAuth('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| Property | Value |
|---|---|
| Window | ±5 minutes |
| Replay guard | Redis, TTL 600 s (single-use) |
| Scope | action is in the message — a signed pause cannot replay as create |
| Result | req.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 }
walletAuthverifies the request signature.validatePolicy(policy)— structural validation.verifyPolicySignature— the user signedBlazpay Autopilot policy {hash} for agent new on chain {chainId}.usdLimitsToTokenUnitsresolveslimits.- Returns the parameters the frontend needs for the on-chain
createAgentcall.
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", … } ]
}| Constraint | Value |
|---|---|
allowTokens | ≤ 20, must include budgetToken, no duplicates |
strategies | ≤ 10, unique ids, order is precedence |
expiry | Future, ≤ 180 days |
slippageBps | 10–300 |
safety.minTokenScore | 0–100 |
| Every token a strategy touches | Must be allow-listed |
| Amounts | Positive 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 runsvalidatePolicy. An LLM therefore cannot produce a signable policy.v1/services/autopilot.service.js → handleAutopilot— sub-actionscreate,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
| Suite | Command |
|---|---|
| Policy toolkit | node scripts/test-policy.js |
| Wallet auth | node scripts/test-wallet-auth.js |
| Trigger engine | node scripts/test-autopilot-engine.js — 13 passing |
| Contract | npx hardhat test test/autopilot.test.cjs — 61 passing |
| Seed settings | node scripts/seed-autopilot-settings.js |
Failure modes
| Symptom | Cause |
|---|---|
401 signature expired (±5min window) | Clock skew, or a stale request |
401 on a valid-looking signature | The frontend digest diverged from walletAuth.js |
| Validation errors on a reasonable policy | Check allowTokens includes budgetToken, weights sum to 10000, amounts are strings |
| Agent never trades | Routers not allow-listed — check first |
| Agent silently stops | Policy hash mismatch between Mongo and chain |
RouterNotAllowed revert | The route's targetContract or spender is not allow-listed |
OutputBelowMinimum revert | Intent/solver route — the probe should have filtered it |
ExceedsMaxPerDay revert | Rolling 24-hour cap reached |
PolicyExpired revert | Past limits.expiry |
| Stale UI after a user transaction | Call POST /sync/:chainId/:agentId |