All articles
Smart Contracts·May 10, 2026·4 min read

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_heap is the correct heap for your market — using a stale or wrong heap silently drops fill events
  • Set expiry_timestamp to 0 only if you want a GTC order; post-only IOC orders need an explicit expiry or they reject
  • Validate price_lots against the market's base_lot_size and quote_lot_size before 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.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Smart Contracts#Solana#Anchor#OpenBook#CPI#DEX#Trading Bots