Beta. On-chain CPI composability is newly available. The
POST /api/v1/cpi/route-accounts
endpoint and the vulcx-aggregator on-chain helpers may change before general availability. If you
hit a rough edge, reach out on Telegram.route instruction via Cross-Program Invocation (CPI),
invoke_signed with your PDA seeds. Because it happens inside your transaction, the swap and the logic
around it (repay a loan, rebalance a vault, credit a merchant) either all succeed or all revert.
Why call route on-chain
Typical use cases: lending liquidations and collateral swaps, vault auto-compounding and rebalancing,
pay-with-any-token settlement, perps cross-collateral conversion, DAO treasury diversification, and
zaps (swap-then-LP in one transaction).
When to use CPI vs the HTTP paths
All three start from the same routing engine. Only the CPI path signs with a PDA and returns an
instruction shaped to be forwarded from inside your program.
Flow
Step 1 — Get the account list (off-chain)
Your backend callsPOST /api/v1/cpi/route-accounts with your program’s PDA as the authority.
The response contains the route instruction (with the PDA already wired in as the swap authority)
plus the token accounts that PDA must own.
data payload:
Route corridors — constrain what the router may touch
Open routing gives the best price; a CPI integrator often wants determinism instead — the route must stay inside the ATAs and DEX flows you pre-provisioned and pre-audited. Three optional request fields constrain routing per request:allowedIntermediateMintscloses therequiredTokenAccountsset: multi-hop routes may only pass through these mints (input and output are implicitly allowed). Your PDA’s ATA set becomes static.excludeDexes: ["moonit"]also lifts the authority-writable requirement — one less thing for your auditors.- Constrained requests bypass the route caches and never split. If no route exists inside the
constraints, the endpoint returns
404with a message telling you to widen them.
Your integrator fee
Add"referrer": "<wallet>" and "integratorFeeBps": <rate> to charge your own fee on top of
Vulcx’s. You keep 100% of it, paid in the output token directly by the aggregator program.
It is added to Vulcx’s platformFeeBps rather than carved out of it, and the sum may not exceed
100 bps. See Fees. The response then includes referrerAta — the
referrer’s output-mint ATA appended to the account list. It must exist before the CPI runs
(create it once per output mint); the fee transfer fails otherwise.
Step 2 — Call route from your program (on-chain)
Add the Vulcx aggregator as a library dependency and use itsroute_cpi helper to assemble the
account list and invoke route, signing with your PDA seeds.
The on-chain helpers ship as the standalone The crate’s major version tracks the deployed program’s instruction layout.
vulcx-cpi crate — typed per-hop builders, arg
encoding pinned byte-for-byte to the deployed program by CI parity tests, and (behind the api
feature, off-chain only) a from_api_response bridge that parses the Step 1 JSON straight into
typed RouteAccounts + RouteArgs and fails loudly on any layout drift:Cargo.toml
keeper.rs (off-chain, api feature)
num_steps = 1 plus a populated token_accounts/hops.
lib.rs
&[&[SEED, &[bump]]]) is the whole point: route accepts your program’s
PDA as user_transfer_authority, so the swap is authorized by your program, not a wallet.
Account layout reference
route reads a fixed set of accounts followed by remaining_accounts (your token accounts, then one
group per hop). RouteAccounts::account_metas() produces this exact order and flags for you — you
rarely assemble it by hand — but here it is for reference and debugging.
Fixed accounts (always first, in this order):
Then, in
remaining_accounts: all your token_accounts (writable), then each hop’s account group
in route order, then optional protocol_fee_ata / referrer_ata (writable).
Per-hop account groups (count and flags depend on the DEX):
- Vortex (7)
- Vortex V2 (10)
- Fluxbeam (14)
- Moonit (14)
Streaming route templates (zero round-trips at fire time)
PollingPOST /cpi/route-accounts before every action costs a round-trip you may not have — on
Fogo’s ~40 ms blocks, a liquidation won or lost in one block. The
WebSocket stream can push the route template to you instead:
{"type":"route", "seq":…, "slot":…, "route":{…}} — the same payload as
POST /cpi/route-accounts — only when the template actually changes (route plan, account list,
or LUTs; price ticks alone never push). For a corridor-constrained pair that’s rare, so your keeper
holds the latest template in memory and fires with zero HTTP round-trips when its trigger hits.
seqincrements by 1 per change; a gap means you missed a push — resubscribe.- Amounts inside the template are as-of the last change, not tick-fresh; re-quote (or redeem a firm quote) if you need the current price.
- Max 32 route subscriptions per connection.
Gotchas
simulate: falsefor real execution.simulate: truemakesroutebail early — it’s only for probing the account wiring without touching liquidity.- Moonit hops need a writable authority. If your route includes a Moonit hop, the authority must be
a writable signer.
RouteAccountsdetects this and sets the flag; make sure your PDA account is markedmutin the context (harmless for non-Moonit routes). - Split routes aren’t supported yet. A split route returns
422fromPOST /api/v1/cpi/route-accounts. Retry with a pair that routes through a single path (any corridor constraint also disables splits). - Multi-hop ExactOut is rejected with a
400. An exact-output swap can only bound its input on a single hop. For exact-out across hops use the ExactIn-with-buffer pattern: quote ExactOut to size the input, execute ExactIn atinput × (1 + buffer), then sweep the surplus output. - Keep the layout in sync. If you hand-assemble accounts instead of forwarding what the endpoint
returns, a single wrong writable/signer flag surfaces as an opaque privilege-escalation or
not-writable failure deep inside the CPI. Prefer forwarding
routeInstruction.accountsverbatim.
The liquidation kit
Liquidation kit — worked example
A complete reference integration: a minimal Anchor
liquidate instruction that CPIs route via
vulcx-cpi (PDA signing, simulate-refusal, balance-delta min-out), plus a TypeScript keeper bot
that consumes subscribe_route templates and fires liquidations with zero HTTP round-trips. Every
gotcha on this page appears in it as code, not prose. Ask on Telegram for access.