Skip to main content
Plugging Picon DLMM into a router or aggregator as one routable pool type typically splits into two pieces: an off-chain quoting/route-building library linked into the router’s own backend, and — if the router’s design calls for it — a small on-chain CPI adapter that invokes the real swap_exact_in/swap_exact_out instruction on the router’s behalf. This page covers the integration-specific pitfalls that aren’t obvious from the instruction reference alone.

Off-chain: decode via the IDL, not hand-mirrored structs

Don’t hand-mirror the Pool/BinArray/instruction-argument structs as local Borsh types — a future field reorder on-chain would silently break decoding instead of failing to compile. Use anchor_lang::declare_program! against the published IDL instead: this generates real typed structs with correct discriminators and field layouts straight from the same IDL published on mainnet, so a schema mismatch becomes a compile error. BinArray/Bin specifically are #[repr(C)] zero-copy accounts on-chain, so a direct bytemuck::try_pod_read_unaligned reinterpret of the raw account bytes is sound and fast for those two types. Pool is not laid out identically to picon-dlmm-quote’s PoolState (different field sets, not #[repr(C)] on either side), so converting between them needs an explicit field-by-field copy, not a cast.

Fetch the right bin-array window, not a symmetric one

This is the single easiest mistake to make when integrating a router venue. MAX_BIN_ARRAYS_PER_TRAVERSAL (6) is the cap per swap direction, not “arrays fetched on each side of the active bin.” Fetching active ± 6 (13 arrays total) and handing that whole window to a single-direction swap instruction overshoots the on-chain cap by one and fails with InvalidBinArray. The correct window, matching the SDK’s own generateBinArrayIndicesForSwap: each direction’s window is the active array itself, plus up to 4 arrays behind it on the deep-traversal side, plus exactly 1 ahead of it as a drift margin — asymmetric per direction. Fetch the union of both directions once per refresh (9 arrays total), then slice down to the exact MAX_BIN_ARRAYS_PER_TRAVERSAL-sized directional window before quoting or building an instruction for a specific direction.

Account ordering

The swap instruction’s token-account and token-program pairs are always in X/Y order on-chain, never input/output order. A router that places “input mint’s account” at the X slot happens to work for x_to_y swaps (where input coincidentally is X) and fails outright for y_to_x swaps.

Transfer hooks: resolve them yourself outside the SDK

Picon DLMM’s on-chain program fully supports Token-2022 transfer-hook mints in the swap instruction — but if you’re hand-building the instruction outside the SDK (which resolves this automatically), you need to resolve the hook’s ExtraAccountMetaList yourself and append the resulting accounts to the TransferHookX/TransferHookY remaining-accounts group. The spl-transfer-hook-interface/spl-tlv-account-resolution crates do this natively in Rust; the TypeScript SDK’s own hand-rolled resolver (resolveTransferHookAccountsX/ resolveTransferHookAccountsY, written because @solana/kit doesn’t ship one either) is a working reference if you’re porting the logic elsewhere.

Test against real execution, not just review

Quoting correctly and building a valid, executable instruction are different claims, and only one of them is checkable without actually running the transaction. Bugs in bin-array windowing and account ordering — the two pitfalls above — are exactly the kind of thing that passes code review and unit tests that only check quote math in isolation, but fails the moment a real instruction executes. Budget for an execution-level test (e.g. against a local validator simulator like LiteSVM, with a real dumped program binary) before treating a router integration as done.