CPI Into OpenBook V2 From an Anchor Program
Cross-Program Invocation lets your settlement program call OpenBook V2 order-placement instructions atomically without a client round-trip. This tutorial walks through building the CPI call, passing the correct account metas, and handling error propagation safely.
OpenBook V2 is one of the few remaining central-limit order books on Solana that exposes a clean Anchor IDL, which makes CPI straightforward once you understand the account layout. If you are building a settlement layer, a liquidation engine, or any on-chain bot that needs to place or cancel orders atomically inside a single transaction, calling OpenBook directly via CPI is the correct path — not routing through a client and hoping the intermediate instruction lands in the same slot.
Why CPI Instead of a Client-Side Instruction
The core reason is atomicity. If your program needs to check a condition (say, a position is underwater) and then place a protective order, those two steps must either both execute or both revert. A client that sends two separate transactions gets a race window where the chain state changes between them. CPI collapses that window to zero: the order placement instruction is a sub-call inside your program's execution context, and if it fails, your entire transaction rolls back.
There is also a composability argument. Other programs can call your settlement contract and inherit the OpenBook interaction without knowing anything about it. That layering is how you build strategies that are hard to replicate — the logic lives on-chain, not in a bot process that can be front-run or go offline.
Setting Up the Dependency
Add the OpenBook V2 crate to Cargo.toml. At the time of writing the published crate is openbook-v2 at version 0.1.x. The IDL-generated client types live in openbook_v2::cpi:
[dependencies]
anchor-lang = "0.29"
openbook-v2 = { version = "0.1", features = ["cpi"] }
The cpi feature re-exports a cpi module with typed context structs for every instruction. Do not reach for invoke_signed with raw bytes here — the typed path catches account ordering mistakes at compile time.
Constructing the Account Metas
OpenBook V2's place_order instruction requires roughly 20 accounts. The most common mistake is mis-ordering the open_orders and open_orders_admin accounts, or forgetting that the market account must be writable even for read-heavy operations. The IDL defines the ordering, so cross-reference openbook_v2::accounts::PlaceOrder directly rather than relying on documentation that may lag a deployment.
Your Anchor context struct needs to include all of those accounts, typically grouped behind a remaining_accounts slice or as explicit named fields. For a single-market integration, explicit fields are worth the verbosity — the compiler enforces completeness. For a multi-market router you will want remaining_accounts plus a manual AccountMeta construction loop:
let order_accounts = openbook_v2::cpi::accounts::PlaceOrder {
signer: ctx.accounts.authority.to_account_info(),
open_orders_account: ctx.accounts.open_orders.to_account_info(),
open_orders_admin: None,
user_token_account: ctx.accounts.base_vault.to_account_info(),
market: ctx.accounts.market.to_account_info(),
bids: ctx.accounts.bids.to_account_info(),
asks: ctx.accounts.asks.to_account_info(),
event_heap: ctx.accounts.event_heap.to_account_info(),
market_vault: ctx.accounts.market_vault.to_account_info(),
oracle_a: None,
oracle_b: None,
token_program: ctx.accounts.token_program.to_account_info(),
};
Pass None for optional oracle fields unless your market uses them — Anchor will omit those account slots from the instruction properly.
Signing with a PDA
If your settlement program owns the open_orders account under a PDA, you need invoke_signed semantics. The typed CPI helpers support this through CpiContext::new_with_signer:
let seeds: &[&[&[u8]]] = &[&[b"open_orders", market_key.as_ref(), &[bump]]];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.openbook_program.to_account_info(),
order_accounts,
seeds,
);
openbook_v2::cpi::place_order(cpi_ctx, side, price_lots, max_base_lots, max_quote_lots, client_order_id, order_type, expiry_timestamp, self_trade_behavior, limit)?;
The ? propagates the OpenBook error back up as your program's error. That is intentional — you want the caller to see PlaceFailed or InvalidPrice as a top-level transaction error, not swallow it and commit a partial state.
Error Propagation and Testing
One subtlety: OpenBook uses a custom error enum, and Anchor maps CPI errors to ErrorCode::AccountNotEnoughKeys or a raw numeric code depending on the version. In your integration tests (running against a local validator with the OpenBook program deployed), assert on AnchorError.error.errorCode.number rather than the string — the number is stable across IDL versions, the string is not.
For unit tests without a validator, mock the CPI by wrapping the call behind a trait and injecting a stub. The real CPI path only fires in an on-chain context anyway; trying to run it in #[cfg(test)] without solana-program-test will panic.
Key things to get right before mainnet:
- Confirm the
event_heapis the correct heap for your market — using a stale or wrong heap silently drops fill events - Set
expiry_timestampto0only if you want a GTC order; post-only IOC orders need an explicit expiry or they reject - Validate
price_lotsagainst the market'sbase_lot_sizeandquote_lot_sizebefore the CPI call, not inside it — lot conversion errors surface as opaque panics on-chain
Slot Constraints and Compute Budget
A transaction that includes a CPI into OpenBook will consume roughly 80,000–120,000 compute units depending on order type and heap state. Budget accordingly with ComputeBudgetInstruction::set_compute_unit_limit prepended to your transaction, and set a priority fee if you are competing for inclusion during congestion. The OpenBook program itself does not charge a protocol fee at the CPI level — fees are settled when fills are consumed from the event heap, which happens in a separate crank transaction unless you consume events in the same tx.
If you are building settlement logic or liquidation infrastructure on Solana and want a second set of eyes on the account layout and PDA design, reach out to us — this is core to how we build and operate strategies at TierZero.
Building this for production?
We turn this architecture into tested, non-custodial software with monitoring, documentation and deployment support.
Related technical guides
Tact vs FunC: Which Is Safer to Audit on TON?
Tact vs FunC security compared: which TON contract language cuts audit bug surface, and which one just moves the risk to the compiler.
Read articleWhat a Smart Contract Audit Certificate Actually Proves
What a smart contract audit certificate really covers: commit hash, scope, and findings — and what "audited" can't guarantee.
Read articleIndependent Auditor vs In-House Review: What's Enough?
Independent smart contract auditor vs in-house review: what investors and exchanges require, real cost/turnaround tradeoffs, and when in-house is enough.
Read article