# grapl documentation — complete public content Public documentation. URLs use the configured canonical origin. # grapl documentation Learn swaps and liquidity, or build with the grapl SDK and custom hooks on Solana. Source: https://docs.grapl.io/ Explore the protocol. Build your next market. Learn swaps and liquidity, or bring grapl into your application with the SDK and custom hooks. [Explore User Guides](https://docs.grapl.io/user-guides) · [Build with grapl](https://docs.grapl.io/developers) ## Use grapl [Swap tokens](https://docs.grapl.io/user-guides/swaps): Choose a pool, set limits, and verify your trade. [Provide liquidity](https://docs.grapl.io/user-guides/liquidity): Choose a range and manage your position. [Understand fees](https://docs.grapl.io/user-guides/fees): Know what you pay, earn, and can collect. [Review pool rules](https://docs.grapl.io/user-guides/hooks-and-risks): Understand hooks, permissions, and liquidity risk. ## Build with grapl [Start with the SDK](https://docs.grapl.io/developers/sdk): Connect RPC and prepare wallet-ready transactions. [Create a market](https://docs.grapl.io/developers/create-market): Issue a token, initialize a pool, and add liquidity. [Build a hook](https://docs.grapl.io/hooks/first-hook): Define the rules and behavior of your pool. [Integrate routing](https://docs.grapl.io/integrations/metis): Explore quotes, wallet context, and the Metis adapter. # Getting started for agents Give your coding assistant current grapl documentation through Markdown or read-only MCP. Source: https://docs.grapl.io/agents Start with the documentation for the task you are implementing. The same public source powers these pages, Markdown exports and documentation tools, including SDK examples and the OpenAPI/AsyncAPI contracts. [Documentation sources](https://docs.grapl.io/agents/sources) Find llms.txt, the complete export and individual Markdown pages. [Documentation MCP](https://docs.grapl.io/agents/mcp) Search and read the public collection from a compatible client. ## Available today Copy Markdown beside any article title, read the [page index](https://docs.grapl.io/llms.txt), or connect to the docs server's read-only MCP. In a local preview, external hosted assistants cannot retrieve your localhost URL; paste the page text or use a client running where the server is reachable. Ask the assistant to read the relevant guide, confirm the SDK methods and deployment assumptions, and cite the page it used. Documentation access does not grant a wallet signature or transaction permission. ## Planned plugins The [Claude Code](https://docs.grapl.io/agents/claude-code) and [Codex](https://docs.grapl.io/agents/codex) pages reserve the future grapl plugin/skill integrations. No grapl plugin package, marketplace entry or installation command is published here. Current Markdown and MCP access works independently of those planned packages. # Claude Code Use the documentation MCP now; the grapl plugin remains planned. Source: https://docs.grapl.io/agents/claude-code ## Use documentation today From a project where Claude Code can reach the local preview, use its official HTTP MCP command: ```sh claude mcp add --transport http graplDocs http://127.0.0.1:3410/api/mcp claude mcp get graplDocs ``` For a remote docs deployment, replace the entire URL with that deployment's `/api/mcp` endpoint. Running the command writes client configuration; reading this page does not. Review the resulting connection in Claude Code before using the [three documentation tools](https://docs.grapl.io/agents/mcp). The syntax and connection checks follow the [official Claude Code MCP guide](https://code.claude.com/docs/en/mcp). You can instead paste the Markdown for a focused page or use [llms.txt](https://docs.grapl.io/llms.txt) to find the right source. ## Plugin and skills roadmap A grapl plugin for Claude Code is planned. No grapl marketplace entry, packaged skills or installation command is available in this documentation. Documentation MCP access is usable independently of that future package. The future page will record the actual installation source, version, included skills and permission boundaries once implemented. Do not infer wallet access, signing or transaction execution from the current read-only documentation connection. # Codex Connect today's documentation sources and track the planned grapl plugin. Source: https://docs.grapl.io/agents/codex ## Use documentation today For a locally running preview reachable from Codex, add the documentation MCP with the official CLI syntax: ```sh codex mcp add graplDocs --url http://127.0.0.1:3410/api/mcp codex mcp list ``` For a remote docs deployment, replace the entire URL with that deployment's `/api/mcp` endpoint. This command writes MCP configuration when you deliberately run it; it does not install a grapl plugin. You can also paste a page's Markdown without configuring MCP. OpenAI documents this HTTP connection pattern in its [official MCP quickstart](https://developers.openai.com/learn/docs-mcp). The grapl endpoint provides [documentation tools](https://docs.grapl.io/agents/mcp) only. Confirm it is reachable from the environment where Codex runs. ## Plugin and skills roadmap A grapl plugin for Codex is planned. Its package, installation source, version and bundled skills have not been published in this documentation. There is no plugin installation command to run yet. The intended area is integration guidance backed by the public SDK and contract docs. Specific bundled capabilities and permissions will be documented when an implementation exists. Until then, use the current Markdown/MCP sources and keep wallet authorization in your application. # Documentation MCP Connect a client to three read-only tools for the public grapl documentation. Source: https://docs.grapl.io/agents/mcp The documentation server exposes Streamable HTTP MCP at `/api/mcp`. Configure a compatible client with the origin where these docs run. The local preview uses `http://127.0.0.1:3410/api/mcp`; that address works only where the client can reach the same machine. | Tool | Input | Result | | ------------ | -------------------------------- | ----------------------------------- | | `list_pages` | No arguments | Public page paths | | `get_page` | `url`, such as `/developers/sdk` | Processed Markdown | | `search` | `query`; optional `locale` | Public documentation search results | Start with a search, then retrieve the relevant page before changing an integration. Paths outside the public collection return a page-not-found result. These tools cannot fetch arbitrary URLs, create pools, read wallets, submit transactions or expose integration keys. ## Connect your client Use the verified CLI examples in [Codex](https://docs.grapl.io/agents/codex) or [Claude Code](https://docs.grapl.io/agents/claude-code). They configure a documentation connection, not a grapl plugin. No client configuration is changed by visiting this page. The MCP endpoint has been exercised with its three tools over local HTTP. The documented client configuration syntax follows the clients' official guides; a full interactive session in each client is not part of that server validation. # Documentation sources Use the public page index, complete text export or focused Markdown pages. Source: https://docs.grapl.io/agents/sources | Source | Use it for | | ------------------------------------------------------------------ | --------------------------------------------- | | [llms.txt](https://docs.grapl.io/llms.txt) | Discover public pages and their Markdown URLs | | [llms-full.txt](https://docs.grapl.io/llms-full.txt) | Read the complete public collection | | [SDK Getting started as Markdown](https://docs.grapl.io/markdown/developers/sdk.md) | Load one focused task into context | | [API Reference as Markdown](https://docs.grapl.io/markdown/developers/api/reference.md) | Read every public operation and its contract | Other pages use `/markdown/.md`; the home is `/markdown/index.md`. Use **Copy Markdown** beside a title for exactly that processed content. Diagrams have text equivalents, and API fields, constraints and captured examples remain readable. These endpoints use the official Fumadocs llms integration. Public filtering applies before search and export; private, internal, admin and maintenance content is excluded. Compatibility URLs contain short pointers rather than duplicated guides. Prefer focused pages for a specific task. The complete export also includes the full API screen and its individual endpoint pages, so endpoint contracts appear in both reading contexts. No production documentation hostname is assumed; use the origin of the deployment you are reading. # How a swap gets its price Work through the swap equation, fees, price impact, and the virtual reserves inside a CLMM range. Source: https://docs.grapl.io/concepts/amm-math An automated market maker quotes trades from its state rather than matching two orders. The familiar **constant-product market maker (CPMM)** is a useful starting point. grapl's liquidity pool is a **CLMM**: the same local curve idea, applied to overlapping price ranges with changing active liquidity. ## A constant-product swap Let `x` be the input reserve and `y` the output reserve, in token atoms. Ignoring fees and rounding, the curve is: **The constant-product invariant** x × y = k A trade adds one asset and removes the other while preserving the curve, before fees and rounding. After adding `Δx` and removing `Δy`, `(x + Δx) × (y − Δy) = k`. Solving for the output gives: **Output from net input** Δy = y × Δx / (x + Δx) x and y are pricing reserves; Δx is the input that reaches the curve. For reserves of 1,000 A and 1,000 B, a 100 A input buys approximately 90.909 B. The average execution price differs from the starting marginal price because the trade moves along the curve. A larger trade relative to reserves moves the price more. These decimal numbers explain the model. Implementations use integer atoms, wide intermediate arithmetic, and explicit rounding. ## Follow the input through the fee For fee rate `f` in parts per million and gross input budget `a`, first calculate the affordable net budget: ```text reserved_fee = ceil(a × f / 1,000,000) net_budget = a − reserved_fee quoted_out = floor(y × net_budget / (x + net_budget)) ``` The interactive example varies reserves, gross input and fee to illustrate price impact. The fixed worked example and integer equations below define its baseline; it is not an executable CLMM quote. With `x = y = 1,000,000` atoms, `a = 100,000`, and `f = 3,000` (0.3%), the reserved fee is 300 atoms, the affordable net budget is 99,700, and quoted output is 90,661 atoms. Rounding keeps the curve solvent. Fees are tracked separately from tradable principal; a physical vault balance is not automatically a pricing reserve. Execution can consume less than the budget because of a boundary or integer precision. The fee on actual consumed net input `n` is `ceil(n × f / (1,000,000 − f))`; unspent input remains explicit. The curve recomputes the smallest gross input sufficient for its affordable net input, which can retain a rounding remainder. Do not equate the budget with the wallet's final debit. Inspect `SwapOutcome`. The Pool 6 source increment retains 10% of the input trading fee with fractional carry, then splits the remainder into the configured LP share and up to four beneficiary shares. A hook may also request a separately bounded **output fee**. See [fee accounting](https://docs.grapl.io/concepts/fees) and [swap modes](https://docs.grapl.io/concepts/swaps) before combining those amounts: they can be denominated in different tokens. ## What changes in a CLMM? A position supplies liquidity only between two prices. Write `s = sqrt(P)`, where `P` is the atom price B per A, and let `L` be active liquidity. Within one segment: **One active CLMM segment** (L / s) × (L × s) = L² s is the square-root atom price and L is active liquidity. The segment behaves like virtual reserves `A = L / s` and `B = L × s`. These are **virtual mathematical reserves**, not the balances of the pool's token accounts. For a position bounded by `s_lower` and `s_upper`, its principal while the price is inside the range is: ```text A = L × (1/s − 1/s_upper) B = L × (s − s_lower) ``` The range endpoints subtract the liquidity that an infinite constant-product curve would need outside the position's chosen interval. That is how a narrower range can make the same capital provide more active liquidity near its price. ## Physical balances are not pricing reserves Suppose `L = 1,000`, `s = 1`, `s_lower = 0.5`, and `s_upper = 2`. Ignoring integer scaling for this illustration, the position holds 500 A and 500 B. Its virtual reserves are 1,000 A and 1,000 B. A net input of 100 A decreases the square-root price: ```text s_next = L × s / (L + net_A × s) = 0.909090… B_out = L × (s − s_next) = 90.909090… ``` Pricing against the **physical** 500/500 balances would incorrectly quote 83.333… B. Always use active `L`, square-root price, and tick boundaries for the CLMM. For B input, the square-root price rises: ```text s_next = s + net_B / L A_out = L × (1/s − 1/s_next) ``` At an initialized tick, positions enter or leave the active set. grapl applies the signed liquidity delta and continues with the next segment's `L`. One global `x × y = k` calculation cannot replace that traversal. ## Ticks, decimals, and market capitalization The core stores `sqrt(P) × 2^64` in Q64.64. Ticks discretize prices approximately as `P = 1.0001^tick`; tick spacing controls valid position boundaries. It does not round every swap to a tick. To convert atom price to a displayed B-per-A price: ```text display_price = atom_price × 10^(decimals_A − decimals_B) market_cap = display_price × circulating_supply_A ``` Market capitalization needs a supply definition and a quote currency. The tick domain limits representable prices, not a universal market-cap interval. Read [tick arrays and full range](https://docs.grapl.io/concepts/tick-arrays) for the supported range and storage model. ## Where the same formula appears again The [bonding curve](https://docs.grapl.io/concepts/bonding-curve) uses configured virtual reserves to shape price discovery. It also tracks real inventory, real quote principal, and fee liabilities independently. Virtual liquidity never authorizes a transfer of tokens the vault does not hold. **Source:** `programs/grapl-amm/src/math/{curve.rs,liquidity.rs,swap/}`. The equations above explain the model; current code and tests define checked bounds and exact rounding. ## Continue learning - [Concentrated liquidity](https://docs.grapl.io/concepts/concentrated-liquidity): what happens when positions enter or leave the active range. - [Ticks and full range](https://docs.grapl.io/concepts/tick-arrays): how the core finds the next boundary. - [Integer numerics](https://docs.grapl.io/concepts/numerics): checked bounds, fixed-point representations, and rounding. # Architecture Separate pure math, settlement, policies, and off-chain routing. Source: https://docs.grapl.io/concepts/architecture **Design.** grapl uses concentrated liquidity in the initial core. A constant-product-only bootstrap is not the architecture baseline. Implementation coverage is tracked in [status](https://docs.grapl.io/start/status). ```mermaid flowchart TB Client[Application or SDK] --> Core[grapl core] Adapter[Metis adapter] --> Quote[Snapshot and pure math] Router[Router - integration required] --> Core Core --> Math[Integer CLMM math] Core --> Positions[Positions and ticks] Core --> Fees[Fee liabilities] Core --> Hook[Pool hook - declared capabilities] Core --> Token[Token program and market vaults] ``` ## Responsibilities | Layer | Responsibility | Must not assume | | -------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Math | Tick/price conversion, token deltas, rounding, swap steps, fee arithmetic | Account ownership or wallet authorization | | Core program | Account checks, state transitions, solvency, user limits, settlement | That SDK validation protects direct callers | | Hook interface | Typed, bounded policy requests and responses | Unrestricted access to vault authority | | SDK | Safe inputs, account discovery, construction, user-facing output | A quote guarantees inclusion or execution | | Metis adapter | Snapshot loading, contextual hook evaluation, deterministic quoting, account metas | That claimed quote identity authenticates a wallet or establishes router admission | ## Markets own their state The implementation gives each market its own vaults and accounting. A common program does not require a global mutable treasury touched by every swap. Shared configuration should be read-only on the swap path where possible. Token A/B ordering is a technical identity. The launch's base and quote roles must be explicit. A token symbol or frontend label must never determine mint ordering. ## Accounting precedes the vault balance The spendable tokens in a vault can back principal, LP fees, protocol claims, beneficiary claims, and other identified allocations. Those amounts are not interchangeable. A direct transfer into a vault does not change the CLMM price. Continue with [fees](https://docs.grapl.io/concepts/fees), [accounts](https://docs.grapl.io/reference/accounts), and [invariants](https://docs.grapl.io/security/invariants). # Virtual reserves and graduation Follow a token from finite curve inventory to a full-range CLMM without changing its pool or hook. Source: https://docs.grapl.io/concepts/bonding-curve A grapl bonding curve is a pool mode with **configurable virtual reserves and real token inventory**. The virtual reserves define the price path. Real balances determine what can actually be paid out. Once the configured net quote target is reached, a separate atomic instruction turns the same pool into a CLMM. ```mermaid flowchart LR Mint[Create or bring a token] --> Curve[Fund curve inventory] Curve --> Trading[Buy and sell against virtual reserves] Trading --> Target[Reach net quote target] Target --> Graduate[Atomic graduation] Graduate --> CLMM[Same pool and hook / full-range CLMM] ``` ## Configure price, inventory, and the target separately | Field | Meaning | | ------------------------ | --------------------------------------------------------------------- | | `virtual_a`, `virtual_b` | Positive virtual reserves used to calculate output and marginal price | | `base_is_a` | Identifies which ordered pool mint is the launched base token | | `inventory` | Actual base atoms the creator deposits | | `bootstrap_base` | Minimum real base retained for graduation | | `target_quote` | Required real quote principal, excluding fees | | `position_id` | Identifier assigned to the creator's graduation position | `inventory` must exceed the positive bootstrap floor. Virtual base must cover the initial inventory, and validation checks that the target can be reached without selling the reserved bootstrap base. It also checks that the resulting price can support the configured full-range position. The quote can be WSOL, USDC, or another supported classic SPL token. A `target_quote` denominated in WSOL is a SOL amount, not a fixed USD valuation. No oracle conversion is implicit. ## Price each trade For virtual input reserve `v_in`, virtual output reserve `v_out`, and input after fees `n`: ```text output = floor(v_out × n / (v_in + n)) ``` ExactOut reverses the equation, rounding the required net input upward. Input fees, output fees, inventory floors, real quote backing, price limits, and user limits remain separate checks. See [AMM mathematics](https://docs.grapl.io/concepts/amm-math). At any point, `sqrt_price_x64` reflects the ratio of the current virtual reserves. It is not computed from the whole physical vault balance. ## Real reserves keep the curve solvent Track these buckets independently for each asset: ```text vault balance ≥ real curve principal + retained LP-share fees + protocol fee liabilities + beneficiary fee liabilities ``` Virtual quote is not collateral. Selling base into the curve can only withdraw real quote available after liabilities. Buying base cannot consume the reserved bootstrap inventory. User LP deposits are disabled during curve mode. The configured LP fee share accumulates for the future bootstrap rather than being attributed to nonexistent public LP positions. Protocol and beneficiary debts remain claimable liabilities and are excluded from graduation funding. Virtual reserves determine pricing but cannot pay withdrawals. Real vault balances back principal, retained LP fees, protocol claims, and beneficiary claims; only available principal can be spent. Conceptual transition from curve inventory to CLMM liquidity. The same pool, vaults, and hook remain bound through graduation. ## Graduate without replacing the market `graduate_curve` checks that the target is reached and creates a creator-owned full-range position. A caller may fund the instruction permissionlessly, but that does not give the caller ownership of the position. The operation preserves: - the pool address and token vaults; - both mint identities and the existing hook binding; - the current Q64.64 marginal price; - already accrued protocol and beneficiary liabilities. It uses real reserves plus retained LP-share fees to calculate affordable full-range liquidity. Rounding or token-ratio differences can leave a remainder; `collect_curve_remainder` allows the creator to withdraw only that recorded remainder after graduation. Graduation is a **separate atomic instruction**, not a swap that crosses two engines. A trade does not automatically continue into CLMM mode. Repeated graduation must reject. A failing hook or settlement check rolls back the operation. ## Policy continuity Curve swaps use `BeforeSwap` and `AfterSwap`. Graduation uses the add-liquidity callbacks and updates `pool_mode` from curve to CLMM. There is no dedicated `BeforeGraduate` or `AfterGraduate` callback in ABI 4. Keeping a hook attached preserves the policy program and configuration binding. A developer can still write a mode-dependent policy, so applications must inspect what that hook actually authorizes. See [lifecycle hooks](https://docs.grapl.io/hooks/lifecycle). **Source:** `instructions/curve/`, `state/curve.rs`, and `math/curve.rs` under `programs/grapl-amm/src/`. # Concentrated liquidity Price ranges determine where liquidity is active and which assets a position holds. Source: https://docs.grapl.io/concepts/concentrated-liquidity In a CLMM, each position contributes liquidity over a chosen interval. The pool's active liquidity is the sum of positions covering the current price. It is not the sum of token balances in the vaults. Range example: positions A and B contain the current price and contribute active liquidity. Position C is out of range and inactive. This is a schematic, not market data. ## Ticks and price Ticks discretize prices. Tick indices are signed; token quantities are unsigned. Position endpoints must be ordered and aligned to the pool's tick spacing. The exact tick domain and fixed-point constants belong to the [math reference](https://docs.grapl.io/reference/math), not a frontend approximation. The pool tracks a square-root price and active liquidity. For price expressed as token B per token A, an A-to-B swap moves price down; B-to-A moves it up. UI prices must account for mint decimals. ```mermaid flowchart LR Start[Current square-root price] --> Step[Compute swap within active interval] Step --> Bound{Reached initialized tick?} Bound -->|Yes| Cross[Apply signed liquidity change] Cross --> Step Bound -->|No| End[Input consumed or explicit limit] ``` ## Crossing a boundary A correct crossing updates active liquidity and the fee-growth boundary records. Direction matters: crossing the same tick in opposite directions applies opposite liquidity changes. A gap with no active liquidity must have a defined result; it cannot mint free output. ## Full range is a position choice Full range means using the supported outer ticks rounded inward to the pool spacing. Use `full_range_ticks(spacing)` and initialize only the two endpoint pages. At spacing 64 the endpoints are `[-443584, 443584]`. See [construction and storage](https://docs.grapl.io/concepts/tick-arrays). It remains a CLMM position. It does not create a fungible share over every other position or turn the implementation into a separate CPMM engine. These concepts follow established CLMM mechanics. grapl's own bounds, rounding, and account representation must be verified independently. See the [Uniswap concentrated-liquidity explanation](https://developers.uniswap.org/docs/get-started/concepts/liquidity-providers/concentrated-liquidity) and the [pinned implementation references](https://docs.grapl.io/reference/resources). # Fee accounting Keep protocol revenue, LP fees, beneficiary claims, and principal separate. Source: https://docs.grapl.io/concepts/fees The trading fee is charged in the received input asset. A dynamic-fee hook may select a rate within a core-enforced cap. Distribution answers a separate question: who receives that fee? For protocol, treasury, creator, and partner entitlements, read [Fees and revenue](https://docs.grapl.io/protocol/fees-and-revenue). It explains mandatory 10% retention, per-mint fractional carry, treasury claims, and the remainder split in the Pool 6 source increment. ## A worked example This is an illustration, not a configured default. With 1,000 input units and a 1% pool fee, 990 units price the swap and 10 units become fee liabilities. The protocol retains 1 unit. With a 70% LP share of the remaining 9 units, active positions receive 6.3 units and external beneficiaries receive 2.7 units. The example uses divisible token units; actual settlement uses integer atoms and carry. Illustrative 1% fee: 1,000 input units split into 990 for pricing and 10 in fee liabilities. Mandatory 10% protocol retention receives 1. With a 70% LP share of the remaining 9, active LPs receive 6.3 and beneficiaries receive 2.7. The trading rate and remainder weights are illustrative. ## LP fee growth The design accounts for fees per position, using global growth and growth outside initialized ticks to derive growth inside a range. The earned increment is based on the position's liquidity and the difference since its checkpoint. Positions earn only over swap segments where they are active. A checkpoint must advance when earned amounts are credited so a later collection cannot claim the same growth again. LP fees remain liabilities until collected or explicitly reinvested; they do not silently increase `L`. ## External splits and dust **Current core behavior:** `programs/grapl-amm/src/instructions/fees/accounting.rs` first reserves the mandatory protocol share with per-pool, per-mint carry, then converts the LP share and four beneficiary shares from bps to ppm, allocates the remainder of each swap-fee event, and assigns split residue to the first nonzero share (LP first). It credits beneficiary debt per token. A collection requires the configured beneficiary signer and clears that debt as part of the transfer transaction. Each beneficiary claims its own recorded debt. Collecting one recipient’s allocation does not distribute every other recipient’s balance. Integer division leaves remainders. Specify whether dust stays reserved, accumulates across claims, or belongs to a designated allocation. Never discard it from liabilities or allocate more than the fee collected. Changing weights must not transfer historical claims to new recipients. ## Separate costs | Amount | Treatment | | -------------------------- | ------------------------------------------------------ | | LP fee | Owed to positions active on the traded intervals | | Creator or partner fee | Beneficiary liability from the post-protocol remainder | | Token-2022 transfer fee | Mint-defined transfer cost, only if supported | | Frontend or aggregator fee | Route-level cost, not implicitly part of the pool fee | | Creation or graduation fee | Separate policy; no default specified here | A sell that inputs token A produces an input-denominated fee in A. Paying every beneficiary in SOL or USDC would require different fee semantics or a conversion. Neither is implicit. Read [numeric rules](https://docs.grapl.io/concepts/numerics) and [solvency invariants](https://docs.grapl.io/security/invariants). ## Fees during curve mode and zaps Curve-mode fees use the same input asset, mandatory protocol retention, and configured remainder shares. The LP share is retained for the future graduation position; protocol and beneficiary debts remain excluded from the bootstrap budget. See [curve accounting](https://docs.grapl.io/concepts/bonding-curve). A zap pays the ordinary fee on its swap leg. It does not turn a one-sided deposit into a free conversion, and it does not automatically compound existing position fees. [ExactOut](https://docs.grapl.io/concepts/swaps) includes both the input fee and any configured output fee when solving for the required input. # Pool lifecycle hooks Attach bounded policies to a pool without handing over token settlement. Source: https://docs.grapl.io/concepts/hooks A grapl pool can bind one hook program and an explicit set of callbacks. The core validates the operation, invokes enabled callbacks, and retains control of CLMM accounting and token transfers. A hook may serve several pools with separate configuration. Pools also work without a hook: bitmap `0` and the System Program sentinel. Allowlist, dynamic-fee schedule, trading-window, lifecycle, and rewards programs are optional examples in `examples/hooks`; none is a default policy or requirement of the core library/program. Universal account and accounting checks still apply to no-hook pools. The [Hooks section](https://docs.grapl.io/hooks/overview) covers policy configuration, authentication, and composition. Start with [Design your first hook](https://docs.grapl.io/hooks/first-hook) to select a responsibility and implement the callback contract. ## Five operation pairs The current core dispatches before/after callbacks for initialize, add liquidity, remove liquidity, swap, and donate. The [callback catalog](https://docs.grapl.io/hooks/lifecycle) specifies all ten phases and the exact state/amounts they see. Opening or closing a position and collecting fees have no callbacks. Curve graduation uses BeforeAddLiquidity and AfterAddLiquidity around the creation of the bootstrap CLMM position; it has no separate callback pair. Custom accounting and return-delta permissions reject. ## Bound policy and authority `PolicyOnly` permits a decision; `DynamicFee` also requires a bounded fee from BeforeSwap. Only the core's pool-scoped callback PDA signs the hook CPI. The pool stays read-only. Extra accounts have explicit owner/class checks and per-phase read/write ceilings; the hook never receives the actor's signer privilege or vault authority. `HookInvocation.actor` is trusted only through that authenticated core call. It identifies the authority validated by the core, which may be a PDA in a routed context; it is not universal proof of the original human wallet. See [authentication](https://docs.grapl.io/hooks/authentication). ## Compose rules inside one hook One selected program can combine eligibility, fee decisions, reward issuance, and its own state. The core provides typed operation context and bounded account access; rule order and economics belong to that program. Stateful effects remain provisional until the whole transaction succeeds. The [reward example](https://docs.grapl.io/hooks/rewards) builds on settled output and optional gates without making those rules mandatory for other pools. ## Scope and governance A pool hook affects operations in its bound pool. A Token-2022 transfer hook is invoked by a token program for a mint's transfers. Implementing a pool policy does not restrict transfers or other venues. A fixed program address does not make upgradeable code immutable. Hook/core upgrade authorities are separate governance decisions. A removal hook can reject withdrawals, and no emergency escape is implemented. The three economic examples enable only BeforeSwap; the lifecycle probe exercises the broader phase set. Continue with [permissions](https://docs.grapl.io/hooks/permissions), [configuration and bootstrap](https://docs.grapl.io/hooks/attach), and the [ABI reference](https://docs.grapl.io/reference/hook-abi). # Choose a market path Create a token independently, then choose a direct CLMM or a virtual-reserve curve. Source: https://docs.grapl.io/concepts/launches Token issuance and market creation are separate decisions. You can bring an existing supported token or use the [token factory](https://docs.grapl.io/guides/token-factory). Neither route automatically creates every possible token pair or supplies liquidity. ```mermaid flowchart LR Token[Create or bring token] --> Direct[Initialize CLMM / fund positions] Token --> Curve[Initialize virtual-reserve curve] Curve --> Target[Reach net quote target] Target --> Graduate[Separate atomic graduation] Direct --> CLMM[CLMM swaps and LP lifecycle] Graduate --> CLMM ``` ## Direct CLMM Choose initial square-root price, tick spacing, fee shares, and an optional hook. Open and fund positions around that price. Full-range and concentrated positions use the same accounting. An out-of-range position can be single-sided; a [zap](https://docs.grapl.io/guides/zaps) can convert part of one asset into an in-range deposit. This path makes sense when the creator can choose an initial price and provide both sides or a deliberate inventory range. A market still needs real quote liquidity before it can pay sellers. ## Virtual-reserve curve Choose real base inventory, virtual A/B reserves, a protected bootstrap allocation, and a target in net quote atoms. The pool prices buys and sells under those parameters, but virtual reserves cannot fund redemptions. See [bonding curve accounting](https://docs.grapl.io/concepts/bonding-curve). After the target is reached, `graduate_curve` uses real principal and retained LP-share fees to fund a creator-owned full-range CLMM position. Protocol and beneficiary fee debts stay reserved. The pool, vaults, mints, and hook binding remain the same. The Q64.64 marginal price is preserved; matching price does not imply matching depth or future slippage. ## Decide before initialization | Decision | Why it matters | | -------------------------------- | ------------------------------------------------------------------------- | | Quote asset | Defines the units of price and the curve target; WSOL is not a USD oracle | | Initial supply and authority | Defines issuance, not automatic liquidity or circulating supply | | Tick spacing | Defines valid CLMM range boundaries and graduation's full range | | Input fees and beneficiaries | Define costs and liabilities in the input token | | Optional hook and permissions | Define callbacks, custom policy, and allowed external effects | | Curve bootstrap floor and target | Constrain reachability and graduation backing | The hook configuration is bound to the pool. A curve's graduation preserves it rather than silently creating an unrestricted destination pool. Policy behavior can still depend on `pool_mode`, so inspect the hook implementation. Use [protocol capabilities](https://docs.grapl.io/start/status) to check the supported operations and interface versions for either path. # The LP lifecycle Follow principal, position liquidity, fee claims, and ownership from entry to exit. Source: https://docs.grapl.io/concepts/lp-lifecycle The current core exposes position opening, signed liquidity changes, fee collection, and closure. This page maps those operations to user-visible state. Use the matching IDL for account order and argument encoding. ```mermaid flowchart TD Empty[Open empty position] --> Fund[Add liquidity with token limits] Fund --> Hold[Active or out of range] Hold --> Collect[Collect earned fees] Collect --> Hold Hold --> Remove[Remove some or all liquidity] Remove --> Claims[Collect remaining fees] Claims --> Close[Close only when liquidity and debts are zero] ``` ## 1. Choose the range and budget Validate ordered ticks, spacing, mint order, price units, and the current pool policy. Use the range math to calculate both token obligations. At the relevant edge or outside the range a position may need only one asset; strictly inside it needs both. An LP interface should show the required A/B amounts, maximum deposit amounts, price snapshot, range, fee policy, and whether the range is currently active. A one-token budget is not a promise that an arbitrary range can be funded. ## 2. Open the position `open_position` creates an owner-bound record with zero liquidity and fee debt. Creating that record does not transfer principal, establish an LP share token, or earn fees. Ownership is stored in the `Position`; position transfer is not a current instruction. ## 3. Fund or add liquidity `modify_liquidity` with positive `liquidity_delta` calculates the A/B deposit with rounding up. `limit_a` and `limit_b` are maximum debits on this path. It updates the position, boundary ticks, applicable active liquidity, and fee checkpoints, then settles tokens. Enabled add-liquidity hooks can reject the operation. Adding later must crystallize fees earned by the previous liquidity before changing `L`. New liquidity must not receive historical fees. The current code performs this accounting in `instructions/positions/accounting.rs`. ## 4. Hold and observe An out-of-range position can hold principal but earn no swap fees until its liquidity is active. Token composition changes with price; `L` is not a stable token balance or a constant currency valuation. Activity elsewhere in the pool does not imply that this position earned fees. Show principal A/B, current range status, accrued fee claims, and valuation separately. The principal estimate uses current price and removal rounding. The fee estimate must include growth since the stored checkpoint, not only `fees_owed_a/b` already crystallized. ## 5. Collect fees `collect_position_fees` crystallizes current inside growth, clears owed amounts as part of the transaction, and pays the owner's token accounts. It leaves liquidity unchanged. A failed transfer must roll back both debt clearing and token movement. Collection is a movement of an existing claim, not a second earning event. Performance calculations count fees earned once, whether they remain owed or have already been collected. ## 6. Reinvest explicitly Automatic compounding is not implemented. Reinvestment would collect, obtain the range's required token composition, and add liquidity with new limits. Conversion fees, slippage, network costs, and time out of range affect the outcome. A displayed APY must state that it assumes this future reinvestment behavior. ## 7. Remove liquidity Use a negative `liquidity_delta`. The magnitude must fit the owned liquidity; `i128::MIN` is rejected. The core calculates principal with rounding down, and `limit_a/b` are minimum receipts on removal. Removal crystallizes prior fees and updates boundary liquidity. A full removal can leave fee debts on a zero-liquidity position. Those claims remain collectible. A partial removal preserves the remaining range and liquidity; it does not create a new owner. ## 8. Close the empty account `close_position` requires liquidity, A fee debt, and B fee debt all to be zero. It returns the account rent to the owner. This is distinct from withdrawing principal or collecting fees. See [APR and APY](https://docs.grapl.io/concepts/lp-performance), [instruction reference](https://docs.grapl.io/reference/instructions), and [integration validation](https://docs.grapl.io/guides/local-tests) for the full sequence and rejected transitions. # LP performance: APR and APY Separate observed fee earnings, annualization, compounding assumptions, and total return. Source: https://docs.grapl.io/concepts/lp-performance **Implemented off-chain calculations; synthetic inputs.** The optional `integrations/analytics` module calculates fee APR and projected APY. There is no production indexer or live yield feed. A fee-growth checkpoint supports entitlement accounting; it does not provide a history of valuations, cash flows, collections, or time in range. See the [analytics API](https://docs.grapl.io/reference/analytics). ## Observed fee APR Use **annualized observed fee APR** for a historical window. The fees and window are observed; the annualized rate is a calculation, not a year of realized performance. Let `F` be fees earned during the window, valued in one explicit quote unit. Let `C_avg` be time-weighted position principal value over that window, including time out of range. Let `T` be window seconds and `Y = 31536000` seconds for a fixed 365-day year. ```text window_fee_return = F / C_avg annualized_observed_fee_APR = (F / C_avg) × (Y / T) ``` Use the position's earnings and capital, not pool volume divided by its current liquidity. Do not exclude inactive hours from the denominator while still calling the result a full-window APR. Report missing valuations or zero denominators as unavailable, not zero. ## Avoid double counting collections For a fixed position with no ownership changes, calculate the earned token increment as: ```text fees_earned = collected_during_window + uncollected_at_end - uncollected_at_start ``` Uncollected amounts include owed balances plus fees from growth not yet crystallized. Apply the same token valuation policy throughout. Subtract external deposits/withdrawals from return calculations; they are cash flows, not profit. Keep donations or incentives separate from organic swap-fee earnings when the evidence allows that classification. ## Estimated APY APY models reinvestment. If annualized fee APR `r` is assumed constant, compounding occurs `n` times per year, and each reinvestment costs proportion `c` of then-current capital: ```text estimated_APY = (1 + r / n - c)^n - 1 ``` This assumes fees can be collected and reinvested at that cadence, at the stated cost, with sufficient liquidity and continued earning conditions. grapl does not automatically compound. A short-window APR cannot establish future volume, in-range time, or a stable reinvestment rate. The implementation accepts 1–365 periods, uses ppb fixed-point rates with downward rounding, and returns signed APY because costs can exceed earned fees. A native-token transaction fee must first be valued and converted into the proportional cost; it is not a ppb rate by itself. Label compounding frequency, gross/net treatment, valuation source, observation window, and estimation time alongside the number. Do not call projected APY “observed yield.” ## An illustrative calculation The following inputs are synthetic quote valuations, not market observations. Keep decimal quantities exact when reproducing the calculation. | Example input | Value | | --------------------------------------------- | ----------------------- | | Window | 86,400 seconds | | Average principal | 10,000 quote units | | Uncollected at start/end | 1.00 / 2.50 quote units | | Collected in window | 0.50 quote units | | Fees earned | 2.00 quote units | | Window fee return | 0.02% | | Annualized observed fee APR | 7.30% | | Estimated APY, daily reinvestment, zero costs | About 7.5723% | Earned fees are `0.50 + 2.50 − 1.00 = 2.00`. Dividing by 10,000 gives a daily return of 0.02%; annualizing over 365 days gives 7.30% APR. Daily reinvestment at that constant rate and zero cost gives the illustrated APY. ## Fee yield is not total return Principal can lose quote value while fees accumulate. Total position P&L must reconcile current principal, fee claims, collections, deposits, withdrawals, costs, and price changes using an explicit valuation policy. Divergence from holding the original assets is another comparison with its own benchmark. Neither is captured by fee APR alone. A future indexer must provide slot/timestamped snapshots, complete fee and cash-flow history, token decimals, valuation sources, and reorg/finality handling. Synthetic fixtures validate arithmetic only; they do not supply those observations. # Integer math and rounding Treat units, precision, and rounding as part of the protocol contract. Source: https://docs.grapl.io/concepts/numerics Amounts are integer token base units. Prices use fixed-point representation. Ticks and liquidity changes may be negative; balances and liquidity stocks may not. Never pass financial amounts through JavaScript `Number`. ## Units are explicit | Value | Representation | | --------------------------- | ---------------------------------------------------- | | Token quantity | Unsigned integer in mint base units | | Human input | Decimal string, validated before conversion | | TypeScript financial amount | `bigint` | | Tick | Signed integer in the supported domain | | Liquidity change | Signed integer; positive adds and negative removes | | Liquidity stock | Unsigned integer with checked updates | | Price | Fixed-point square-root price in ordered-token units | Changing mint decimals changes the human interpretation of a raw price. Reversing A/B inverts the price. Neither transformation belongs in a floating-point settlement path. ## Round for the obligation Required deposit amounts must not be understated. Paid output must not exceed the amount owed. Liquidity calculated from a budget must not require a later unauthorized debit. Fee splits must conserve the collected fee and account for dust. | Operation | Required property | | ----------------------------- | ------------------------------------------------------ | | Multiply then divide | Use sufficient intermediate width; reject overflow | | Denominator | Reject zero explicitly | | Signed-to-unsigned conversion | Validate the sign before casting | | Narrowing conversion | Reject values beyond the target type | | Fee accrual | Preserve liabilities through truncation and collection | | Quote | Use exactly the execution domain and rounding | ## Shared math is necessary, not sufficient The core and an adapter should share deterministic primitives. That prevents divergent formulas but can share the same bug. Independent rational or high-precision fixtures are needed to check the implementation. Do not transplant Uniswap Q64.96 constants into a Q64.64 implementation. The format, domain, tick factors, intermediate width, and rounding must be chosen together. Current definitions belong in [math reference](https://docs.grapl.io/reference/math). # Positions and single-asset liquidity Distinguish a range deposit, an edge launch, a zap, and a strategy vault. Source: https://docs.grapl.io/concepts/positions **Design semantics.** A position identifies an owner, a lower and upper tick, liquidity, fee checkpoints, and tokens owed. Public instruction availability is separate; see [instructions](https://docs.grapl.io/reference/instructions). ## What a range needs For a range `[lower, upper]`, with price measured as B per A: | Current price | Composition for the range | | ------------------------------- | ------------------------- | | At or below the lower boundary | Token A only | | Strictly between the boundaries | Both token A and token B | | At or above the upper boundary | Token B only | At boundaries, fixed-point rounding and the program's active-tick convention still apply. A one-sided budget does not fund an arbitrary in-range position. ## Four different operations **Single-asset range deposit** Choose a range whose composition at the current price needs only the supplied asset. The position may remain inactive until price enters its range. **Direct range launch** Initialize price at the relevant edge and fund inventory in prescribed ranges. The inventory does not create quote reserves for buybacks before buyers provide quote assets. **Atomic zap** Swap part of a single-asset budget, then fund an in-range position. The swap has fees, price impact, and user limits. The `zap_in` and `zap_out` instructions compose those operations; see [zaps](https://docs.grapl.io/guides/zaps) for limits and verification. **Strategy vault — planned** A separate layer owns a portfolio of positions and accounts for shares. Shares require valuation and withdrawal rules; they are not a native global CLMM LP token. ## Fees and principal Collecting fees transfers an existing claim and does not increase liquidity. Compounding requires an explicit reinvestment operation, potentially including a swap to obtain the range's required composition. Removing liquidity returns principal and must settle position checkpoints without deleting an existing fee claim. See [fee accounting](https://docs.grapl.io/concepts/fees) and [launch modes](https://docs.grapl.io/concepts/launches). # Exact input and exact output swaps Choose which amount to fix, protect the other amount, and account for fees on both sides. Source: https://docs.grapl.io/concepts/swaps grapl exposes two swap instructions over the same pool, hook interface, and settlement checks. Both operate on integer token atoms and enforce a square-root price limit. | Mode | You choose | Your protection | Result | | --------------------------- | -------------------------- | ----------------------------- | --------------------------------------- | | `swap` / ExactIn | Input budget `amount_in` | `minimum_out` and price limit | Actual consumed input and net output | | `swap_exact_out` / ExactOut | Exact **net** `amount_out` | `maximum_in` and price limit | Required input, bounded by your maximum | Use the [instruction reference](https://docs.grapl.io/reference/instructions) to match each mode to its account and argument contract. ## ExactIn: start from a budget For an A→B swap, the program validates accounts and asks `BeforeSwap` for approval and any permitted fees. It applies the input fee, walks the curve or CLMM segments, then settles actual input and net output. `AfterSwap` sees the settled amounts. On the direct CLMM instruction, a price limit can leave input unspent. `SwapOutcome.amount_unspent` is explicit; do not assume the full budget moved. The standard Metis adapter requires a complete ExactIn fill and rejects an incomplete quote. ## ExactOut: start from what the recipient receives An ExactOut request for 10,000 B atoms means **10,000 atoms after a configured hook output deduction**. The program solves for sufficient gross output, then sufficient input including the input fee. It rounds costs upward and enforces `maximum_in` before committing settlement. ```text required net output → gross output sufficient for the output fee → net input sufficient for the curve traversal → gross input sufficient for the input fee → require gross input ≤ maximum_in ``` Insufficient liquidity, an unreachable price limit, or a maximum input that is too small fails the transaction. ExactOut does not silently downgrade to a partial delivery. ## The output fee is separate For gross output `g` and output rate `r` in ppm: ```text output_fee = floor(g × r / 1,000,000) net_output = g − output_fee ``` For example, at 5,000 ppm (0.5%), gross output 10,050 gives an output fee of 50 and a net output of 10,000. The inverse calculation must respect integer rounding; dividing a human decimal by `1 − rate` and truncating is unsafe. The input fee remains in the input asset. The output fee is in the output asset and sent by the core to the declared recipient. Adding those atom counts together would mix units. ## Hooks receive the swap mode ABI 4 adds `HookSwapDetails.exact_output`. A hook can distinguish an input budget from an output target rather than inferring intent from the quantity alone. The snapshot also contains `pool_mode`, so the same policy can handle a virtual-reserve curve and its graduated CLMM. Rewards should use `HookSettledSwap.actual_output` or the relevant actual input, rather than the user's requested budget. The core validates the lifecycle envelope and the hook's account permissions in both modes. ## Transaction construction and quotes Use the current generated IDL for account order. A CLMM trade supplies canonical tick pages followed by hook extras; each operation validates its own account list. The native Metis encoder supports both modes. Public routing availability depends on the routing provider’s integration. Read [instructions](https://docs.grapl.io/reference/instructions), [hook authentication](https://docs.grapl.io/hooks/authentication), and [Jupiter integration](https://docs.grapl.io/integrations/metis) for their separate contracts. # Tick arrays and full range Allocate only useful boundaries, discover pages by bitmap, and build full-range positions. Source: https://docs.grapl.io/concepts/tick-arrays Paginated tick storage, introduced in Pool version 3 and retained in the current Pool version 6, replaces the 32-boundary tick book with pages covering the supported tick domain. Hook policies remain external programs. Pagination changes storage and transaction construction, while preserving concentrated-liquidity accounting. ## How pages work Each page represents 64 aligned tick slots. Compute its signed index with floor division: ```text array_index = floor(tick / (64 * tick_spacing)) ``` Use `array_index` in the Rust library; truncating division gives the wrong page for negative ticks. The pool's `TickDirectory` has a two-level bitmap, so swaps skip empty pages and uninitialized slots. Boundaries with zero net liquidity but positive gross liquidity remain initialized and must still update fee checkpoints when crossed. ```mermaid flowchart LR Pool[Pool: price and active liquidity] --> Directory[TickDirectory: nonempty pages] Directory --> Lower[Lower endpoint page] Directory --> Upper[Upper endpoint page] Lower --> RecordA[Initialized slot and fee growth] Upper --> RecordB[Initialized slot and fee growth] ``` ## Choose a page format | Format | Bytes per page | Allocation | | ------- | -------------------: | ------------------------------------------------- | | Fixed | 4152 | All 64 record slots reserved | | Compact | `56 + 64 × capacity` | Packed initialized records; capacity from 0 to 64 | A compact page with capacity two occupies 184 bytes. The 1816-byte directory is shared by every position in the pool. Capacity grows through `reserve_tick_array`, funded by a signer before adding liquidity. Removing a boundary clears its slot and reuses capacity; it does not shrink the account or refund rent. Page addresses are shared, so the first initialization selects the format for that page. ## Add full-range liquidity 1. Read the pool's immutable spacing and call `full_range_ticks(spacing)`. 2. Derive the two endpoint page PDAs. Initialize missing pages or reserve enough compact capacity for new endpoints. 3. Call `open_position` with those ticks. 4. Call `modify_liquidity` with positive liquidity, token maximums, endpoint pages in ascending signed index order, and any declared hook extras afterward. 5. Collect fees, remove liquidity, and close the empty position through the ordinary LP lifecycle. For spacing 64, the widest valid range is **[-443584, 443584]**. Full range creates only two boundary records; the interior contains no position-specific tick records. Other positions may share the same boundaries. With the current price inside the range, adding full-range liquidity normally requires **both assets**. One-sided positions use a range outside the current price. A [zap](https://docs.grapl.io/guides/zaps) can swap part of a single-asset budget and then deposit both assets atomically. The caller chooses the swap amount and protects the result with a minimum liquidity limit. Wide coverage also spreads capital over more prices, so it does not imply the same fee return per unit of capital as a narrow range. Crossing upward applies a tick's signed liquidity net: L_after = L_before + delta_L. Crossing downward applies the opposite change. ## Transaction and router budgets Swaps accept at most 16 page accounts, independently of the hook account limit. This is a ceiling, not a promise that every combination fits the transaction packet or CU budget. The Metis adapter caches up to 16 nonempty pages per direction, refreshes newly discovered addresses, and builds execution metas from the pages actually visited by the quote. A missing initialized page rejects; it never silently changes liquidity. The traversal can conservatively require the next initialized page beyond the requested price limit. Extra hook state and Merkle proofs consume the same transaction envelope. Every proof sibling adds 32 bytes, so measure the complete account and payload plan. A proof format’s maximum depth does not guarantee that every transaction carrying it fits. ## Plan account and compute budgets Measure the exact position ranges, page format, hook composition, proof depth, and transaction format used by your application. Setup costs and steady-state liquidity changes are different workloads. Reserve sufficient budget for the largest supported traversal. # Developer Guides Integrate grapl with an RPC endpoint, a wallet, and explicit transaction control. Source: https://docs.grapl.io/developers Use the TypeScript SDK to prepare protocol operations in your application. RPC reads and transaction preparation run off-chain; the Solana program enforces settlement and pool rules. The grapl API is optional. [SDK](https://docs.grapl.io/developers/sdk) Connect directly to RPC and prepare unsigned token, pool, liquidity, swap, and fee operations. [API Guides](https://docs.grapl.io/developers/api) Discover pools, compare direct quotes, and observe transaction outcomes through HTTP. [Hooks](https://docs.grapl.io/hooks/overview) Build authenticated lifecycle policies, custom fees, eligibility rules, and rewards. [API Reference](https://docs.grapl.io/developers/api/reference) Explore the full public OpenAPI contract, grouped by resource. [On-chain Reference](https://docs.grapl.io/reference/instructions) Find instruction accounts, layouts, errors, math and the hook ABI. Each section has its own guides and navigation. Start with the SDK for wallet execution, add the API when you need its optional services, or use Hooks to author a pool policy. ## Protocol areas | Area | Start here | Reference | | --------------- | ------------------------------------------- | ---------------------------------------------- | | AMM | [Pricing and CLMM math](https://docs.grapl.io/concepts/amm-math) | [Integer math](https://docs.grapl.io/reference/math) | | Hooks | [Lifecycle and examples](https://docs.grapl.io/hooks/overview) | [Hook ABI](https://docs.grapl.io/reference/hook-abi) | | Market creation | [Build a market](https://docs.grapl.io/guides/overview) | [Instructions](https://docs.grapl.io/reference/instructions) | | Routing | [Metis adapter](https://docs.grapl.io/integrations/metis) | [Wallet context](https://docs.grapl.io/integrations/wallet-context) | | Data services | [Optional API](https://docs.grapl.io/developers/api) | [Accounts](https://docs.grapl.io/reference/accounts) | For larger compositions, read [Address Lookup Tables](https://docs.grapl.io/developers/address-lookup-tables). Use [AI documentation tools](https://docs.grapl.io/agents) to copy Markdown or provide context to an assistant. Current capabilities are recorded in [Protocol status](https://docs.grapl.io/start/status); source availability does not imply npm publication, public deployment, or Jupiter admission. The [grapl application](https://grapl.io) uses the [development sandbox](https://docs.grapl.io/developers/sandbox), with verified RPC/faucet connections, a persistent validator restart and a completed public UI flow on the previous application hostname: two claims plus seven SDK actions. The test covers CLMM with classic SPL tokens, no hook and an ephemeral wallet. The current backend endpoints and discovery of the newest test pool were verified on October 12, 2026. External wallet compatibility remains unverified, and the indexed receipt history still has an explicit gap. ## Protocol revenue [Fees and revenue](https://docs.grapl.io/protocol/fees-and-revenue) explains mandatory protocol retention, immutable remainder splits, treasury and beneficiary claim authorization, and why accrued tokens are not an automatically converted USDC balance. # Address Lookup Tables Compress account addresses in prepared v0 transactions without changing operation semantics. Source: https://docs.grapl.io/developers/address-lookup-tables An Address Lookup Table (ALT) stores account addresses on-chain. A v0 transaction can reference eligible addresses by index instead of repeating their full bytes. This helps account-heavy compositions, but it does not compress instruction payloads or signatures, and it does not remove compute limits. ## Supply tables explicitly Each direct preparation intent accepts `transaction.lookupTables`, an array of table addresses: ```typescript import type { GraplClient, PreparedTransaction, SwapExactInIntent, } from "@grapl/sdk"; import type { Address } from "@solana/kit"; export async function prepareSwapWithTable( client: GraplClient, reviewedIntent: SwapExactInIntent, lookupTable: Address, ): Promise { return client.prepareSwapExactIn({ ...reviewedIntent, transaction: { ...reviewedIntent.transaction, lookupTables: [lookupTable] }, }); } ``` The SDK fetches tables over RPC and validates their owner, layout, discriminator, active status, and warmup. It conservatively rejects deactivating or deactivated tables. In a table's extension slot, only the previously active address prefix is usable; new entries become usable after warmup. ## Create and discover tables for a pool The SDK's locally implemented pool ALT increment exposes explicit creation, extension planning, and discovery helpers. Follow [Create and discover pool ALTs](https://docs.grapl.io/developers/pool-lookup-tables) for method signatures and lifecycle rules. Your application still owns signatures, submission, confirmation, rent funding, and authority selection. These are separate setup operations, not implicit side effects of a swap. ## Check the result Compare `serializedSize` and `simulation` for equivalent operations with and without the table. A smaller message does not promise lower compute usage or a cheaper transaction after setup and rent costs. The final serialized transaction must still fit 1,232 bytes, including signatures. Oversized compositions fail explicitly; they are never silently split. An ALT is optional, and a supplied table that lacks useful addresses may provide no compression. See [transaction lifecycle](https://docs.grapl.io/developers/transactions). ## Measured local example The isolated Pool 6 integration run on 2026-10-12 compared the same donation instructions with and without an active table containing 12 addresses: | Measurement | Without ALT | With ALT | | ------------------------- | -------------: | -------------: | | Serialized v0 transaction | 607 bytes | 300 bytes | | Executed compute units | 45,072 | 45,072 | | Network fee | 5,000 lamports | 5,000 lamports | This saved 307 bytes. Setup separately required 3,953,280 lamports of rent funding and 10,000 lamports in transaction fees. These are local fixture measurements, not universal costs or a compute reduction promise. Creation, extension, discovery, a full 256-entry table, and explicit replacement/fallback were also exercised; the SDK still leaves selection and signing to the application. # Agent documentation has moved Documentation sources and MCP now live in Agents. Source: https://docs.grapl.io/developers/ai-tools Read [Agents](https://docs.grapl.io/agents) for Markdown, llms.txt, the documentation MCP and planned Claude Code/Codex plugins. This compatibility URL redirects there in the browser. # grapl API Use indexed pool discovery, native direct-pool quotes, and transaction observation as separate services. Source: https://docs.grapl.io/developers/api The SDK's direct client works with RPC and a wallet integration. The HTTP API is an optional service for indexed discovery, quoting, and transaction observation; it is not a required signing or settlement layer. No public service origin or general API-key onboarding is announced here. The source API's OpenAPI documents are served by its configured deployment. Do not infer an API hostname from the documentation hostname. [Authentication and limits](https://docs.grapl.io/developers/api/authentication) Use integration keys and inspect shared request capacity. [Pool discovery](https://docs.grapl.io/developers/api/discovery) Find pools by mint and paginate the catalog. [Direct-pool quotes](https://docs.grapl.io/developers/api/quotes) Compare complete ExactIn and ExactOut results. [Status streams](https://docs.grapl.io/developers/api/status-streams) Poll outcomes or consume resumable SSE events. [API Reference](https://docs.grapl.io/developers/api/reference) Browse the generated public OpenAPI contract. ## Public trading surface | Resource | Current purpose | | ---------------------------- | ------------------------------------------------------------------------------ | | `GET /v1/pools` | Discover indexed pools by mint, with optional quote mint and cursor pagination | | `GET /v1/swap/quote` | Compare eligible direct pools for an ExactIn or ExactOut amount | | Transaction status resources | Observe indexed finalized or failed results and stream updates | Quotes compare complete executable input/output, including fees, price movement, and available liquidity. They do not simply choose the lowest percentage fee. A catalog entry does not guarantee sufficient liquidity or hook-aware quotability. ## Current boundaries The HTTP quote path uses the native Rust engine for direct-pool candidates. Multihop and split routing are not implemented. The HTTP bridge does not currently supply hook adapters; it rejects unsupported policies. `priceImpactPct: null` means that metric is unavailable, not zero price impact. Build, order, and submit resources are not connected to a trading gateway and currently reject with service-unavailable responses. SDK execution and API observation are separate flows. The API cannot sign for a user's wallet. For native router integration, read [Metis](https://docs.grapl.io/integrations/metis). For the protocol's stable on-chain interfaces, use [Instructions](https://docs.grapl.io/reference/instructions) and [Accounts](https://docs.grapl.io/reference/accounts). Team administration and internal service operations are outside this public guide. # Authentication and request limits Authenticate integration requests and understand shared team quotas. Source: https://docs.grapl.io/developers/api/authentication Use the API origin supplied by your integration deployment. Keep `x-api-key` in a request header, never in a URL or public client bundle. No public origin or self-service key issuance is announced here. Your integration key authorizes API requests; it cannot sign a wallet transaction. [API Reference](https://docs.grapl.io/developers/api/reference) lists only the public integration contract. Account management and platform administration are separate interfaces. Use `GET /v1/rate-limits` to inspect available request capacity and `GET /v1/usage` to inspect consumption. Both require an integration key in `x-api-key`. Limits apply to the authenticated key's team and are shared by all that team's integration keys. Neither route accepts a team identifier. `/v1/rate-limits` returns `scope: "team"`, `unit: "requests"`, `observedAt`, `canRequest`, `retryAfterSeconds`, and `minute`/`month` windows containing `limit`, `remaining` and `resetsAt`. `/v1/usage` returns the same scope/unit/observation timestamp with `used` and `resetsAt` for each window. Responses use the standard `{ "success": true, "data": ... }` envelope. Windows are fixed UTC calendar minutes and months, rather than rolling durations. `canRequest` describes the snapshot, not reserved capacity; another request can consume capacity immediately afterward. Remaining capacity never becomes negative if an administrator lowers a limit below current usage. These two authenticated reads do not consume trading quota and remain available when it is exhausted. Trading/status calls, including each SSE connection, count when admitted; SSE frames do not. An exhausted trading request receives HTTP 429 with `Retry-After`. Introspection returns HTTP 200 with the current blocked state. Revoked or expired keys cannot query either endpoint. Usage and limit responses are private and non-cacheable. They contain no key secrets, other teams' usage, commercial pricing, limit-request workflow or administration operations. Concurrent SSE connection caps are independent of request quotas and are not included in these minute/month snapshots. # Discover pools by mint Find indexed pools for a mint or direct pair, with explicit pagination. Source: https://docs.grapl.io/developers/api/discovery ## Discover pools for a mint `GET /v1/pools?mint=` lists indexed grapl pools containing a mint on either side. Add `quoteMint=` to restrict the list to a direct pair. Results contain `pools` and `nextCursor`. Use `limit` (default 20, maximum 100) and pass the previous `nextCursor` as `cursor`. Ordering is by pool address. Pagination reads a live catalog, so a new pool inserted before your cursor appears when you restart the scan. Each pool includes both mints, its address, base fee in parts per million, mode, native liquidity units, hook program and observed slot. `liquidity` is not TVL and is not comparable as a standalone ranking metric across prices or ranges. Discovery includes pools without liquidity and hook pools whose quote adapter is not installed. Discovery and quote operations require `x-api-key` and each admitted request consumes one unit of the team's request quota, including subsequent validation or provider failures. ## From catalog to execution A listed pool may be empty or use a policy unsupported by HTTP quoting. Use [direct-pool quotes](https://docs.grapl.io/developers/api/quotes) to compare a complete requested amount. Pass the selected pool and explicit execution bounds into your [SDK flow](https://docs.grapl.io/developers/swaps); discovery does not create, sign, or submit a transaction. Use the configured API origin and `x-api-key`. The exact query fields and response shape are in [API Reference](https://docs.grapl.io/developers/api/reference#pools). # Events & AsyncAPI The public transaction-status stream: six SSE messages, payload schemas, resumption, and observed examples. Source: https://docs.grapl.io/developers/api/events For decoded on-chain pool, swap and liquidity observations, use the [core event reference](https://docs.grapl.io/reference/events). Track one transaction signature through HTTP server-sent events. This observer does not sign or submit transactions. [AsyncAPI JSON](https://docs.grapl.io/asyncapi.json) · [YAML](https://docs.grapl.io/asyncapi.yaml) · [HTTP handshake](https://docs.grapl.io/developers/api/reference/streamTransactionStatus) ## snapshot Current observation when there is no durable event and no resume cursor. It does not advance the cursor. | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | payload.signature | string | Yes | | | payload.status | string; one of not_observed, finalized, failed | Yes | | | payload.commitment | string; one of finalized | Yes | | | payload.slot | string / null; maxLength: 39 | Yes | | | payload.observedAt | string / null; date-time | Yes | | ### Observed examples ```json { "signature": "1111111111111111111111111111111111111111111111111111111111111111", "status": "not_observed", "commitment": "finalized", "slot": null, "observedAt": null } ``` ## transaction.status Durable finalized or failed observation. The SSE id equals payload.id; retain it as a decimal string for resumption. | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | payload.signature | string | Yes | | | payload.status | string; one of finalized, failed | Yes | | | payload.commitment | string; one of finalized | Yes | | | payload.slot | string / null; maxLength: 39 | Yes | | | payload.observedAt | string / null; date-time | Yes | | | payload.id | string | Yes | | ### Observed examples ```json { "signature": "1111111111111111111111111111111111111111111111111111111111111111", "status": "finalized", "commitment": "finalized", "slot": "9007199254740993", "observedAt": "2026-10-12T12:00:00.000Z", "id": "9007199254740994" } ``` ```json { "signature": "1111111111111111111111111111111111111111111111111111111111111111", "status": "failed", "commitment": "finalized", "slot": "9007199254740993", "observedAt": "2026-10-12T12:00:00.000Z", "id": "9007199254740994" } ``` ## complete Terminal observation; close the client connection. A resumed terminal cursor can receive complete without replaying transaction.status. | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | payload.signature | string | Yes | | | payload.status | string; one of finalized, failed | Yes | | | payload.commitment | string; one of finalized | Yes | | | payload.slot | string / null; maxLength: 39 | Yes | | | payload.observedAt | string / null; date-time | Yes | | ### Observed examples ```json { "signature": "1111111111111111111111111111111111111111111111111111111111111111", "status": "finalized", "commitment": "finalized", "slot": "9007199254740993", "observedAt": "2026-10-12T12:00:00.000Z" } ``` ```json { "signature": "1111111111111111111111111111111111111111111111111111111111111111", "status": "failed", "commitment": "finalized", "slot": "9007199254740993", "observedAt": "2026-10-12T12:00:00.000Z" } ``` ## heartbeat Connection liveness only. This timestamp is not a transaction observation and does not advance the cursor. | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | payload.observedAt | string; date-time | Yes | | ### Observed examples ```json { "observedAt": "1970-01-01T00:00:00.020Z" } ``` ```json { "observedAt": "1970-01-01T00:00:00.040Z" } ``` ## reconnect The connection lifetime elapsed. Reconnect after retryAfterMs with the last durable event ID, if present. | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | payload.reason | string | Yes | | | payload.retryAfterMs | integer | Yes | | ### Observed examples ```json { "reason": "connection_lifetime", "retryAfterMs": 3000 } ``` ## stream-error Safe error after response headers. No private exception text is published. Stop automatic retry when retryable is false. | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | payload.code | string | Yes | | | payload.retryable | boolean | Yes | | ### Observed examples ```json { "code": "STREAM_UNAVAILABLE", "retryable": true } ``` ```json { "code": "UNAUTHORIZED", "retryable": false } ``` ```json { "code": "FORBIDDEN", "retryable": false } ``` ```json { "code": "CURSOR_NOT_AVAILABLE", "retryable": false } ``` ## Connect and resume Send `GET /v1/swap/status/{signature}/events` with the `x-api-key` integration credential. Use an HTTP streaming client that supports custom headers; the browser's native `EventSource` does not accept an arbitrary API-key header. Keep backend credentials out of browser code. ```sh curl --no-buffer --fail-with-body \ "${GRAPL_API_ORIGIN}/v1/swap/status/${SIGNATURE}/events" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --header "Last-Event-ID: ${LAST_DURABLE_EVENT_ID}" ``` Omit `Last-Event-ID` on the first connection. The optional value is a positive decimal string of at most 19 digits. The cursor must belong to the same signature and configured observer; an unavailable or mismatched cursor returns **409 CURSOR_NOT_AVAILABLE** before the stream starts. See the [OpenAPI reference](https://docs.grapl.io/developers/api/reference/streamTransactionStatus) for handshake errors, rate limits and the complete request schema. Each SSE `data` field contains a JSON payload. Every frame carries `retry: 3000`. Only `transaction.status` has an `id` field, equal to `payload.id`. Slots and IDs remain strings, including values above JavaScript's safe integer range. ```text event: transaction.status id: 9007199254740994 retry: 3000 data: {"signature":"1111111111111111111111111111111111111111111111111111111111111111","status":"finalized","commitment":"finalized","slot":"9007199254740993","observedAt":"2026-10-12T12:00:00.000Z","id":"9007199254740994"} ``` This example was captured from the API publisher using an isolated test observation. It is not a public-network transaction. ## Handle the lifecycle | Message | Client action | | -------------------- | ---------------------------------------------------------------------------------------- | | `snapshot` | Show the current observation. Do not advance the resume cursor. | | `transaction.status` | Store the durable string ID and deduplicate repeated observations. | | `complete` | Close the connection: the terminal observation is finalized or failed. | | `heartbeat` | Keep waiting. This is connection liveness, not transaction confirmation. | | `reconnect` | Reconnect after `retryAfterMs`, using the last durable ID when present. | | `stream-error` | Read the safe code; retry only when `retryable` is true and your retry policy allows it. | The default publisher polls every 1 second, emits a heartbeat after 15 seconds without one, and closes a connection after 60 seconds. These are implementation defaults, not a delivery SLA. A resumed terminal cursor receives `complete` without repeating the acknowledged durable event. Delivery can repeat across reconnects: deduplicate by ID; exactly-once delivery is not promised. Authorization is checked during streaming. After headers are committed, errors become `stream-error` frames; authorization/cursor failures with status 401, 403 or 409 are non-retryable. Transport disconnects can end a stream without a final control frame. Before headers, failures remain normal HTTP responses. Transaction observation and the configured indexer are required; no public service hostname is announced. ## Contract conventions The AsyncAPI application is the grapl API server. Its `send` operation describes messages sent to your client. The schema uses the standard HTTP 0.3.0 `GET` binding. The documented `x-sse` extension records framing and resume behavior; it is not a standardized SSE binding, and frame fields are not HTTP headers. There are no WebSocket, Kafka, mint or pool subscription channels in this public contract. Continue with [status and streams](https://docs.grapl.io/developers/api/status-streams), [authentication](https://docs.grapl.io/developers/api/authentication), or the [API Reference](https://docs.grapl.io/developers/api/reference). # Quote a direct swap Compare complete ExactIn and ExactOut trades across supported pools. Source: https://docs.grapl.io/developers/api/quotes Call `GET /v1/swap/quote` with `inputMint`, `outputMint` and `amount`. The API discovers indexed pools for that direct pair and compares native quotes. A pool address is returned in `data.pool`; it is not a request parameter. Both mints are required because an input mint alone cannot identify which asset you want to receive. ## What makes a pool the best choice? The engine evaluates the complete requested swap against each supported pool's current state. It accounts for the effective trade fee, available token reserves, price movement, active liquidity and tick crossings. It does not rank pools by their nominal fee or their raw liquidity field. - **ExactIn:** fix input atoms and choose the largest output after trade fees. - **ExactOut:** fix output atoms and choose the smallest gross input, including trade fees. - **Ties:** use the lexically smallest pool address for deterministic selection. - **Incomplete fills:** exclude a candidate that cannot fill the entire amount. Never return a partial quote as a complete swap. A shallow 0.3% pool can beat a deeper 0.9% pool for a small order. For a larger order, price impact can make the deeper pool better even while both can fill it. At a still larger size, the shallow pool may no longer have enough liquidity. The preferred pool can therefore change with the amount and direction. Selection currently uses one direct pool. It does not split an order across pools, route through intermediate mints or discover unrelated DEX programs. ## Quote parameters | Parameter | Meaning | | ------------- | ------------------------------------------------------------------------------------------------- | | `inputMint` | Base58 SPL mint to sell. | | `outputMint` | Base58 SPL mint to receive; distinct from the input mint. | | `amount` | Positive u64 decimal string in token atoms. ExactIn uses input atoms; ExactOut uses output atoms. | | `swapMode` | `ExactIn` by default, or `ExactOut`. | | `slippageBps` | Integer basis points, 0–10000; defaults to 50 (0.5%). | | `taker` | Optional wallet context. An address is not proof that the caller controls it. | For example, with a six-decimal input token, `amount=1000000&swapMode=ExactIn` means one input token. With `ExactOut`, the same amount refers to the output token's decimals. `otherAmountThreshold` is rounded down for minimum output in ExactIn and rounded up for maximum input in ExactOut. Financial quantities remain integer strings. `fees[].kind=trade` reports the total input trade fee, not an extra fee and not a breakdown between LPs and protocol beneficiaries. `priceImpactPct` is currently null; this does not mean zero price impact. Price movement is included in the native quote itself. ## Freshness and supported policies The indexer supplies candidate addresses. Pricing independently reloads each candidate from confirmed RPC accounts, including its pool, mints, vaults, tick directory and required tick pages. Account dependencies are rediscovered and fetched together before evaluation. The RPC genesis must match the configured chain. Candidates can be evaluated at different slots. `contextSlot` belongs to the selected candidate; this is not a simultaneous market-wide snapshot. `expiresAt` is 10 seconds after selection starts; an expired comparison fails instead of returning an old quote. It is a short validity hint, not a reservation or settlement guarantee. Requote before execution if the state or amount has changed. The native API bridge currently quotes unhooked pools. Hook pools stay discoverable, but do not participate until their policy adapter is wired into this bridge. The underlying Metis adapter interface supports injected hook adapters; this HTTP bridge does not yet install them. A hook is never silently ignored. Build, order and submit have documented contracts but still return `503 TRADING_NOT_CONFIGURED` until the execution bridge is connected. A successful quote does not imply a transaction was built, signed or sent. ## Errors | Status and code | Meaning | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `400 VALIDATION` | Invalid or unknown parameters, identical mints or an invalid amount. | | `404 NO_POOLS` | The indexed catalog contains no direct pool for the pair. | | `422 NO_QUOTABLE_POOL` | No supported candidate can fill the complete requested amount. | | `422 TOO_MANY_POOLS` | More than 50 indexed candidates; the API rejects instead of claiming an arbitrary truncated subset is best. | | `422 AMOUNT_OVERFLOW` | The slippage-adjusted input threshold exceeds u64. | | `429 QUOTA_EXCEEDED` | The team request budget is exhausted. | | `503` | Required configuration, RPC, coherent snapshot or worker is unavailable. No partial candidate ranking is returned after a provider failure. | An empty discovery list is a successful `200` response. A quote for that same absent pair returns `404 NO_POOLS`. See [API Reference](https://docs.grapl.io/developers/api/reference) for the generated request and response contract. # API Reference Explore all nine public integration endpoints, grouped by resource. Source: https://docs.grapl.io/developers/api/reference Use your deployment origin and an `x-api-key` integration credential. This reference renders the complete reviewed public OpenAPI 3.1.0 contract. [Authentication guide](https://docs.grapl.io/developers/api/authentication) · [Download OpenAPI](https://docs.grapl.io/openapi.json). Build, order and submit currently return 503 TRADING_NOT_CONFIGURED. The interactive playground is unavailable until a service origin is configured. Request samples require your origin, key and valid input values. Captured response fixtures are labeled; responses without a verified example show Empty while retaining their schemas. ## Pools ### Discover pools by mint `GET /v1/pools` Lists pools discovered by the configured grapl indexer. Supply quoteMint to find a direct pair. Empty results return 200. Catalog inclusion is not a quote or an execution guarantee; use /v1/swap/quote to compare supported pools for an amount. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | mint | query | Yes | string; solana-address | Find indexed grapl pools containing this SPL mint on either side. | | quoteMint | query | No | string; solana-address | Optional second mint. Restricts discovery to the direct pair, in either order. | | limit | query | No | integer; minimum: 1; maximum: 100; default: 20 | Maximum pools per page, ordered by pool address. | | cursor | query | No | string; solana-address | Exclusive pool address cursor returned as nextCursor. Pagination is a live catalog view. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/pools" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --data-urlencode "mint=${MINT}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.pools | array | Yes | | | response.data.pools[].address | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.pools[].mintA | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.pools[].mintB | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.pools[].liquidity | string; maxLength: 39 | Yes | Native liquidity units, not TVL or a ranking score. | | response.data.pools[].observedSlot | string; maxLength: 39 | Yes | Slot of the indexer snapshot. Quotes independently refresh on-chain accounts. | | response.data.pools[].hookProgram | string; solana-address | Yes | System program address means no hook. Discovery does not imply quote support. | | response.data.pools[].tradeFeePpm | integer; minimum: 0 | Yes | Configured base fee in parts per million; hooks may change effective fees. | | response.data.pools[].mode | integer | Yes | 0: concentrated liquidity; 1: virtual-reserve bonding curve. | | response.data.nextCursor | string / null; solana-address | Yes | Base58-encoded Solana public key. | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ## Swaps ### Quote the best direct pool `GET /v1/swap/quote` Provide the input/output mint pair and amount; no pool address is required or accepted. Compares up to 50 indexed candidates using the native engine, including fees and liquidity. ExactIn maximizes output; ExactOut minimizes input. Ties use lexical pool address order. 404 NO_POOLS means the pair is absent; 422 NO_QUOTABLE_POOL means no supported pool can fill the whole amount. RPC failures abort selection with 503, never a partial ranking. Exceeding 50 candidates returns 422 TOO_MANY_POOLS. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | inputMint | query | Yes | string; solana-address | SPL mint to sell. Paired with outputMint for automatic pool selection. | | outputMint | query | Yes | string; solana-address | SPL mint to receive; must differ from inputMint. | | amount | query | Yes | string; maxLength: 20 | Positive u64 token atoms as a decimal string. Input atoms for ExactIn; output atoms for ExactOut. For 6 decimals, 1000000 represents one token. | | swapMode | query | No | string; one of ExactIn, ExactOut; default: ExactIn | ExactIn fixes input and maximizes output; ExactOut fixes output and minimizes input. | | slippageBps | query | No | integer; minimum: 0; maximum: 10000; default: 50 | Slippage tolerance in basis points: 50 = 0.5%. Sets the returned threshold; does not change which pool has the best nominal quote. | | taker | query | No | string; solana-address | Optional wallet context for future contextual adapters. Providing an address is not proof of authorization. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/quote" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --data-urlencode "inputMint=${INPUT_MINT}" \ --data-urlencode "outputMint=${OUTPUT_MINT}" \ --data-urlencode "amount=${AMOUNT}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.pool | string; solana-address | Yes | Selected direct pool. Output only; callers specify mints, not a pool. | | response.data.requestId | string | Yes | | | response.data.inputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.outputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.inAmount | string; maxLength: 39 | Yes | Gross input in token atoms, including the trade fee. | | response.data.outAmount | string; maxLength: 39 | Yes | Expected output in token atoms after the pool trade fee. | | response.data.otherAmountThreshold | string; maxLength: 39 | Yes | Minimum output for ExactIn; maximum input for ExactOut. | | response.data.swapMode | string; one of ExactIn, ExactOut | Yes | | | response.data.slippageBps | integer | Yes | | | response.data.priceImpactPct | string / null | Yes | Null when a price-impact metric is unavailable; never fabricated as zero. | | response.data.contextSlot | string; maxLength: 39 | Yes | Confirmed RPC snapshot slot for the selected pool. Other candidates may use different slots. | | response.data.expiresAt | string; date-time | Yes | UTC quote expiry, 10 seconds after selection starts. No liquidity is reserved. | | response.data.fees | array | Yes | | | response.data.fees[].mint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.fees[].amount | string; maxLength: 39 | Yes | | | response.data.fees[].kind | string; one of trade, lp, protocol, hook | Yes | trade is the total input trade fee; it is not an additional fee or an LP/protocol allocation breakdown. | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ## Isolated response fixture Captured from the API's isolated selection-test fakes. Test addresses and amounts illustrate the contract, not a market quote. Only the random request ID is normalized. ```json { "success": true, "data": { "pool": "11111111111111111111111111111111", "requestId": "isolated-selection-fixture", "inputMint": "11111111111111111111111111111111", "outputMint": "So11111111111111111111111111111111111111112", "inAmount": "1000", "outAmount": "990", "otherAmountThreshold": "985", "swapMode": "ExactIn", "slippageBps": 50, "priceImpactPct": null, "contextSlot": "42", "expiresAt": "2026-10-12T12:30:10.000Z", "fees": [ { "mint": "11111111111111111111111111111111", "amount": "3", "kind": "trade" } ] } } ``` ### Build unsigned swap instructions `GET /v1/swap/build` Returns account privileges and instruction bytes for wallet signing when the execution bridge is configured. Currently returns 503 TRADING_NOT_CONFIGURED. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | inputMint | query | Yes | string; solana-address | SPL mint to sell. Paired with outputMint for automatic pool selection. | | outputMint | query | Yes | string; solana-address | SPL mint to receive; must differ from inputMint. | | amount | query | Yes | string; maxLength: 20 | Positive u64 token atoms as a decimal string. Input atoms for ExactIn; output atoms for ExactOut. For 6 decimals, 1000000 represents one token. | | swapMode | query | No | string; one of ExactIn, ExactOut; default: ExactIn | ExactIn fixes input and maximizes output; ExactOut fixes output and minimizes input. | | slippageBps | query | No | integer; minimum: 0; maximum: 10000; default: 50 | Slippage tolerance in basis points: 50 = 0.5%. Sets the returned threshold; does not change which pool has the best nominal quote. | | taker | query | Yes | string; solana-address | Wallet that will sign the swap; required for instruction construction. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/build" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --data-urlencode "inputMint=${INPUT_MINT}" \ --data-urlencode "outputMint=${OUTPUT_MINT}" \ --data-urlencode "amount=${AMOUNT}" \ --data-urlencode "taker=${TAKER}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.pool | string; solana-address | Yes | Selected direct pool. Output only; callers specify mints, not a pool. | | response.data.requestId | string | Yes | | | response.data.inputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.outputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.inAmount | string; maxLength: 39 | Yes | Gross input in token atoms, including the trade fee. | | response.data.outAmount | string; maxLength: 39 | Yes | Expected output in token atoms after the pool trade fee. | | response.data.otherAmountThreshold | string; maxLength: 39 | Yes | Minimum output for ExactIn; maximum input for ExactOut. | | response.data.swapMode | string; one of ExactIn, ExactOut | Yes | | | response.data.slippageBps | integer | Yes | | | response.data.priceImpactPct | string / null | Yes | Null when a price-impact metric is unavailable; never fabricated as zero. | | response.data.contextSlot | string; maxLength: 39 | Yes | Confirmed RPC snapshot slot for the selected pool. Other candidates may use different slots. | | response.data.expiresAt | string; date-time | Yes | UTC quote expiry, 10 seconds after selection starts. No liquidity is reserved. | | response.data.fees | array | Yes | | | response.data.fees[].mint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.fees[].amount | string; maxLength: 39 | Yes | | | response.data.fees[].kind | string; one of trade, lp, protocol, hook | Yes | trade is the total input trade fee; it is not an additional fee or an LP/protocol allocation breakdown. | | response.data.computeBudgetInstructions | array | Yes | | | response.data.computeBudgetInstructions[].programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.computeBudgetInstructions[].accounts | array | Yes | | | response.data.computeBudgetInstructions[].accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.computeBudgetInstructions[].accounts[].isSigner | boolean | Yes | | | response.data.computeBudgetInstructions[].accounts[].isWritable | boolean | Yes | | | response.data.computeBudgetInstructions[].data | string | Yes | Base64 instruction bytes | | response.data.setupInstructions | array | Yes | | | response.data.setupInstructions[].programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.setupInstructions[].accounts | array | Yes | | | response.data.setupInstructions[].accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.setupInstructions[].accounts[].isSigner | boolean | Yes | | | response.data.setupInstructions[].accounts[].isWritable | boolean | Yes | | | response.data.setupInstructions[].data | string | Yes | Base64 instruction bytes | | response.data.swapInstruction | object | Yes | | | response.data.swapInstruction.programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.swapInstruction.accounts | array | Yes | | | response.data.swapInstruction.accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.swapInstruction.accounts[].isSigner | boolean | Yes | | | response.data.swapInstruction.accounts[].isWritable | boolean | Yes | | | response.data.swapInstruction.data | string | Yes | Base64 instruction bytes | | response.data.cleanupInstruction | object / null | Yes | | | response.data.cleanupInstruction.programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.cleanupInstruction.accounts | array | Yes | | | response.data.cleanupInstruction.accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.cleanupInstruction.accounts[].isSigner | boolean | Yes | | | response.data.cleanupInstruction.accounts[].isWritable | boolean | Yes | | | response.data.cleanupInstruction.data | string | Yes | Base64 instruction bytes | | response.data.otherInstructions | array | Yes | | | response.data.otherInstructions[].programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.otherInstructions[].accounts | array | Yes | | | response.data.otherInstructions[].accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.otherInstructions[].accounts[].isSigner | boolean | Yes | | | response.data.otherInstructions[].accounts[].isWritable | boolean | Yes | | | response.data.otherInstructions[].data | string | Yes | Base64 instruction bytes | | response.data.addressesByLookupTableAddress | object | Yes | | | response.data.blockhash | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.lastValidBlockHeight | string; maxLength: 39 | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ### Build an unsigned swap transaction `GET /v1/swap/order` Execution-bridge contract. Currently returns 503 TRADING_NOT_CONFIGURED; a quote alone does not produce a transaction. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | inputMint | query | Yes | string; solana-address | SPL mint to sell. Paired with outputMint for automatic pool selection. | | outputMint | query | Yes | string; solana-address | SPL mint to receive; must differ from inputMint. | | amount | query | Yes | string; maxLength: 20 | Positive u64 token atoms as a decimal string. Input atoms for ExactIn; output atoms for ExactOut. For 6 decimals, 1000000 represents one token. | | swapMode | query | No | string; one of ExactIn, ExactOut; default: ExactIn | ExactIn fixes input and maximizes output; ExactOut fixes output and minimizes input. | | slippageBps | query | No | integer; minimum: 0; maximum: 10000; default: 50 | Slippage tolerance in basis points: 50 = 0.5%. Sets the returned threshold; does not change which pool has the best nominal quote. | | taker | query | No | string; solana-address | Optional wallet context for future contextual adapters. Providing an address is not proof of authorization. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/order" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --data-urlencode "inputMint=${INPUT_MINT}" \ --data-urlencode "outputMint=${OUTPUT_MINT}" \ --data-urlencode "amount=${AMOUNT}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.pool | string; solana-address | Yes | Selected direct pool. Output only; callers specify mints, not a pool. | | response.data.requestId | string | Yes | | | response.data.inputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.outputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.inAmount | string; maxLength: 39 | Yes | Gross input in token atoms, including the trade fee. | | response.data.outAmount | string; maxLength: 39 | Yes | Expected output in token atoms after the pool trade fee. | | response.data.otherAmountThreshold | string; maxLength: 39 | Yes | Minimum output for ExactIn; maximum input for ExactOut. | | response.data.swapMode | string; one of ExactIn, ExactOut | Yes | | | response.data.slippageBps | integer | Yes | | | response.data.priceImpactPct | string / null | Yes | Null when a price-impact metric is unavailable; never fabricated as zero. | | response.data.contextSlot | string; maxLength: 39 | Yes | Confirmed RPC snapshot slot for the selected pool. Other candidates may use different slots. | | response.data.expiresAt | string; date-time | Yes | UTC quote expiry, 10 seconds after selection starts. No liquidity is reserved. | | response.data.fees | array | Yes | | | response.data.fees[].mint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.fees[].amount | string; maxLength: 39 | Yes | | | response.data.fees[].kind | string; one of trade, lp, protocol, hook | Yes | trade is the total input trade fee; it is not an additional fee or an LP/protocol allocation breakdown. | | response.data.transaction | string / null; minLength: 4; maxLength: 1644 | Yes | | | response.data.lastValidBlockHeight | string / null; maxLength: 39 | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ### Submit a signed transaction `POST /v1/swap/submit` Submit a signed transaction. Submission does not establish confirmation or settlement. **Authentication:** `x-api-key` integration credential. ## Parameters No query or path parameters. ## Request body Content-Type: `application/json`. | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | body.signedTransaction | string; minLength: 4; maxLength: 1644 | Yes | | ## Request example ```sh curl --fail-with-body --request POST "${GRAPL_API_ORIGIN}/v1/swap/submit" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --header "Content-Type: application/json" \ --data @signed-request.json ``` `signed-request.json` must contain the documented body with your application's signed transaction. This request currently returns 503; it is not a working submission flow. ## Responses | HTTP status | Meaning | | --- | --- | | 202 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 202 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.signature | string | Yes | | | response.data.status | string; one of submitted | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ## Transaction status ### Read an observed transaction outcome `GET /v1/swap/status/{signature}` Reads the configured observer. A not_observed result is not proof that a transaction is absent, pending or failed on chain. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | signature | path | Yes | string | | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/status/${SIGNATURE}" \ --header "x-api-key: ${GRAPL_API_KEY}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.signature | string | Yes | | | response.data.status | string; one of not_observed, finalized, failed | Yes | | | response.data.commitment | string; one of finalized | Yes | | | response.data.slot | string / null; maxLength: 39 | Yes | | | response.data.observedAt | string / null; date-time | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ### Stream transaction status updates `GET /v1/swap/status/{signature}/events` Stream finalized observations from the configured chain/program. Resume using Last-Event-ID. Not-observed is not proof of absence on chain. Each connection counts as one API request; frames do not. Stop on complete. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | signature | path | Yes | string | | | last-event-id | header | No | string | Last delivered transaction.status event ID. Replays newer persisted events before following live updates. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/status/${SIGNATURE}/events" \ --header "x-api-key: ${GRAPL_API_KEY}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | SSE: snapshot, transaction.status (with id), heartbeat, complete, reconnect or stream-error. Only finalized/failed outcomes are observed currently. | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ## Usage & limits ### Read remaining request quota `GET /v1/rate-limits` Read-only snapshot for the authenticated key’s team, shared by all its keys. Does not consume trading quota and remains available after exhaustion. Fixed UTC minute and calendar-month windows; concurrent requests can change these values immediately. **Authentication:** `x-api-key` integration credential. ## Parameters No query or path parameters. ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/rate-limits" \ --header "x-api-key: ${GRAPL_API_KEY}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.scope | string; one of team | Yes | | | response.data.unit | string; one of requests | Yes | | | response.data.observedAt | string; date-time | Yes | | | response.data.canRequest | boolean | Yes | | | response.data.retryAfterSeconds | integer; minimum: 0 | Yes | | | response.data.minute | object | Yes | | | response.data.minute.limit | integer; minimum: 0 | Yes | | | response.data.minute.remaining | integer; minimum: 0 | Yes | | | response.data.minute.resetsAt | string; date-time | Yes | | | response.data.month | object | Yes | | | response.data.month.limit | integer; minimum: 0 | Yes | | | response.data.month.remaining | integer; minimum: 0 | Yes | | | response.data.month.resetsAt | string; date-time | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ### Read current request consumption `GET /v1/usage` Read-only snapshot for the authenticated key’s team, shared by all its keys. Does not consume trading quota and remains available after exhaustion. Fixed UTC minute and calendar-month windows; concurrent requests can change these values immediately. **Authentication:** `x-api-key` integration credential. ## Parameters No query or path parameters. ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/usage" \ --header "x-api-key: ${GRAPL_API_KEY}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.scope | string; one of team | Yes | | | response.data.unit | string; one of requests | Yes | | | response.data.observedAt | string; date-time | Yes | | | response.data.minute | object | Yes | | | response.data.minute.used | integer; minimum: 0 | Yes | | | response.data.minute.resetsAt | string; date-time | Yes | | | response.data.month | object | Yes | | | response.data.month.used | integer; minimum: 0 | Yes | | | response.data.month.resetsAt | string; date-time | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | # Build unsigned swap instructions Returns account privileges and instruction bytes for wallet signing when the execution bridge is configured. Source: https://docs.grapl.io/developers/api/reference/buildSwapInstructions Currently returns 503 TRADING_NOT_CONFIGURED. The execution bridge is not implemented. `GET /v1/swap/build` Returns account privileges and instruction bytes for wallet signing when the execution bridge is configured. Currently returns 503 TRADING_NOT_CONFIGURED. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | inputMint | query | Yes | string; solana-address | SPL mint to sell. Paired with outputMint for automatic pool selection. | | outputMint | query | Yes | string; solana-address | SPL mint to receive; must differ from inputMint. | | amount | query | Yes | string; maxLength: 20 | Positive u64 token atoms as a decimal string. Input atoms for ExactIn; output atoms for ExactOut. For 6 decimals, 1000000 represents one token. | | swapMode | query | No | string; one of ExactIn, ExactOut; default: ExactIn | ExactIn fixes input and maximizes output; ExactOut fixes output and minimizes input. | | slippageBps | query | No | integer; minimum: 0; maximum: 10000; default: 50 | Slippage tolerance in basis points: 50 = 0.5%. Sets the returned threshold; does not change which pool has the best nominal quote. | | taker | query | Yes | string; solana-address | Wallet that will sign the swap; required for instruction construction. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/build" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --data-urlencode "inputMint=${INPUT_MINT}" \ --data-urlencode "outputMint=${OUTPUT_MINT}" \ --data-urlencode "amount=${AMOUNT}" \ --data-urlencode "taker=${TAKER}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.pool | string; solana-address | Yes | Selected direct pool. Output only; callers specify mints, not a pool. | | response.data.requestId | string | Yes | | | response.data.inputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.outputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.inAmount | string; maxLength: 39 | Yes | Gross input in token atoms, including the trade fee. | | response.data.outAmount | string; maxLength: 39 | Yes | Expected output in token atoms after the pool trade fee. | | response.data.otherAmountThreshold | string; maxLength: 39 | Yes | Minimum output for ExactIn; maximum input for ExactOut. | | response.data.swapMode | string; one of ExactIn, ExactOut | Yes | | | response.data.slippageBps | integer | Yes | | | response.data.priceImpactPct | string / null | Yes | Null when a price-impact metric is unavailable; never fabricated as zero. | | response.data.contextSlot | string; maxLength: 39 | Yes | Confirmed RPC snapshot slot for the selected pool. Other candidates may use different slots. | | response.data.expiresAt | string; date-time | Yes | UTC quote expiry, 10 seconds after selection starts. No liquidity is reserved. | | response.data.fees | array | Yes | | | response.data.fees[].mint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.fees[].amount | string; maxLength: 39 | Yes | | | response.data.fees[].kind | string; one of trade, lp, protocol, hook | Yes | trade is the total input trade fee; it is not an additional fee or an LP/protocol allocation breakdown. | | response.data.computeBudgetInstructions | array | Yes | | | response.data.computeBudgetInstructions[].programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.computeBudgetInstructions[].accounts | array | Yes | | | response.data.computeBudgetInstructions[].accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.computeBudgetInstructions[].accounts[].isSigner | boolean | Yes | | | response.data.computeBudgetInstructions[].accounts[].isWritable | boolean | Yes | | | response.data.computeBudgetInstructions[].data | string | Yes | Base64 instruction bytes | | response.data.setupInstructions | array | Yes | | | response.data.setupInstructions[].programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.setupInstructions[].accounts | array | Yes | | | response.data.setupInstructions[].accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.setupInstructions[].accounts[].isSigner | boolean | Yes | | | response.data.setupInstructions[].accounts[].isWritable | boolean | Yes | | | response.data.setupInstructions[].data | string | Yes | Base64 instruction bytes | | response.data.swapInstruction | object | Yes | | | response.data.swapInstruction.programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.swapInstruction.accounts | array | Yes | | | response.data.swapInstruction.accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.swapInstruction.accounts[].isSigner | boolean | Yes | | | response.data.swapInstruction.accounts[].isWritable | boolean | Yes | | | response.data.swapInstruction.data | string | Yes | Base64 instruction bytes | | response.data.cleanupInstruction | object / null | Yes | | | response.data.cleanupInstruction.programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.cleanupInstruction.accounts | array | Yes | | | response.data.cleanupInstruction.accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.cleanupInstruction.accounts[].isSigner | boolean | Yes | | | response.data.cleanupInstruction.accounts[].isWritable | boolean | Yes | | | response.data.cleanupInstruction.data | string | Yes | Base64 instruction bytes | | response.data.otherInstructions | array | Yes | | | response.data.otherInstructions[].programId | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.otherInstructions[].accounts | array | Yes | | | response.data.otherInstructions[].accounts[].pubkey | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.otherInstructions[].accounts[].isSigner | boolean | Yes | | | response.data.otherInstructions[].accounts[].isWritable | boolean | Yes | | | response.data.otherInstructions[].data | string | Yes | Base64 instruction bytes | | response.data.addressesByLookupTableAddress | object | Yes | | | response.data.blockhash | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.lastValidBlockHeight | string; maxLength: 39 | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Build an unsigned swap transaction Execution-bridge contract. Source: https://docs.grapl.io/developers/api/reference/buildSwapOrder Currently returns 503 TRADING_NOT_CONFIGURED. The execution bridge is not implemented. `GET /v1/swap/order` Execution-bridge contract. Currently returns 503 TRADING_NOT_CONFIGURED; a quote alone does not produce a transaction. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | inputMint | query | Yes | string; solana-address | SPL mint to sell. Paired with outputMint for automatic pool selection. | | outputMint | query | Yes | string; solana-address | SPL mint to receive; must differ from inputMint. | | amount | query | Yes | string; maxLength: 20 | Positive u64 token atoms as a decimal string. Input atoms for ExactIn; output atoms for ExactOut. For 6 decimals, 1000000 represents one token. | | swapMode | query | No | string; one of ExactIn, ExactOut; default: ExactIn | ExactIn fixes input and maximizes output; ExactOut fixes output and minimizes input. | | slippageBps | query | No | integer; minimum: 0; maximum: 10000; default: 50 | Slippage tolerance in basis points: 50 = 0.5%. Sets the returned threshold; does not change which pool has the best nominal quote. | | taker | query | No | string; solana-address | Optional wallet context for future contextual adapters. Providing an address is not proof of authorization. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/order" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --data-urlencode "inputMint=${INPUT_MINT}" \ --data-urlencode "outputMint=${OUTPUT_MINT}" \ --data-urlencode "amount=${AMOUNT}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.pool | string; solana-address | Yes | Selected direct pool. Output only; callers specify mints, not a pool. | | response.data.requestId | string | Yes | | | response.data.inputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.outputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.inAmount | string; maxLength: 39 | Yes | Gross input in token atoms, including the trade fee. | | response.data.outAmount | string; maxLength: 39 | Yes | Expected output in token atoms after the pool trade fee. | | response.data.otherAmountThreshold | string; maxLength: 39 | Yes | Minimum output for ExactIn; maximum input for ExactOut. | | response.data.swapMode | string; one of ExactIn, ExactOut | Yes | | | response.data.slippageBps | integer | Yes | | | response.data.priceImpactPct | string / null | Yes | Null when a price-impact metric is unavailable; never fabricated as zero. | | response.data.contextSlot | string; maxLength: 39 | Yes | Confirmed RPC snapshot slot for the selected pool. Other candidates may use different slots. | | response.data.expiresAt | string; date-time | Yes | UTC quote expiry, 10 seconds after selection starts. No liquidity is reserved. | | response.data.fees | array | Yes | | | response.data.fees[].mint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.fees[].amount | string; maxLength: 39 | Yes | | | response.data.fees[].kind | string; one of trade, lp, protocol, hook | Yes | trade is the total input trade fee; it is not an additional fee or an LP/protocol allocation breakdown. | | response.data.transaction | string / null; minLength: 4; maxLength: 1644 | Yes | | | response.data.lastValidBlockHeight | string / null; maxLength: 39 | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Discover pools by mint Lists pools discovered by the configured grapl indexer. Source: https://docs.grapl.io/developers/api/reference/discoverPools `GET /v1/pools` Lists pools discovered by the configured grapl indexer. Supply quoteMint to find a direct pair. Empty results return 200. Catalog inclusion is not a quote or an execution guarantee; use /v1/swap/quote to compare supported pools for an amount. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | mint | query | Yes | string; solana-address | Find indexed grapl pools containing this SPL mint on either side. | | quoteMint | query | No | string; solana-address | Optional second mint. Restricts discovery to the direct pair, in either order. | | limit | query | No | integer; minimum: 1; maximum: 100; default: 20 | Maximum pools per page, ordered by pool address. | | cursor | query | No | string; solana-address | Exclusive pool address cursor returned as nextCursor. Pagination is a live catalog view. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/pools" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --data-urlencode "mint=${MINT}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.pools | array | Yes | | | response.data.pools[].address | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.pools[].mintA | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.pools[].mintB | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.pools[].liquidity | string; maxLength: 39 | Yes | Native liquidity units, not TVL or a ranking score. | | response.data.pools[].observedSlot | string; maxLength: 39 | Yes | Slot of the indexer snapshot. Quotes independently refresh on-chain accounts. | | response.data.pools[].hookProgram | string; solana-address | Yes | System program address means no hook. Discovery does not imply quote support. | | response.data.pools[].tradeFeePpm | integer; minimum: 0 | Yes | Configured base fee in parts per million; hooks may change effective fees. | | response.data.pools[].mode | integer | Yes | 0: concentrated liquidity; 1: virtual-reserve bonding curve. | | response.data.nextCursor | string / null; solana-address | Yes | Base58-encoded Solana public key. | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Read remaining request quota Read-only snapshot for the authenticated key’s team, shared by all its keys. Source: https://docs.grapl.io/developers/api/reference/getRateLimits `GET /v1/rate-limits` Read-only snapshot for the authenticated key’s team, shared by all its keys. Does not consume trading quota and remains available after exhaustion. Fixed UTC minute and calendar-month windows; concurrent requests can change these values immediately. **Authentication:** `x-api-key` integration credential. ## Parameters No query or path parameters. ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/rate-limits" \ --header "x-api-key: ${GRAPL_API_KEY}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.scope | string; one of team | Yes | | | response.data.unit | string; one of requests | Yes | | | response.data.observedAt | string; date-time | Yes | | | response.data.canRequest | boolean | Yes | | | response.data.retryAfterSeconds | integer; minimum: 0 | Yes | | | response.data.minute | object | Yes | | | response.data.minute.limit | integer; minimum: 0 | Yes | | | response.data.minute.remaining | integer; minimum: 0 | Yes | | | response.data.minute.resetsAt | string; date-time | Yes | | | response.data.month | object | Yes | | | response.data.month.limit | integer; minimum: 0 | Yes | | | response.data.month.remaining | integer; minimum: 0 | Yes | | | response.data.month.resetsAt | string; date-time | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Read current request consumption Read-only snapshot for the authenticated key’s team, shared by all its keys. Source: https://docs.grapl.io/developers/api/reference/getRequestUsage `GET /v1/usage` Read-only snapshot for the authenticated key’s team, shared by all its keys. Does not consume trading quota and remains available after exhaustion. Fixed UTC minute and calendar-month windows; concurrent requests can change these values immediately. **Authentication:** `x-api-key` integration credential. ## Parameters No query or path parameters. ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/usage" \ --header "x-api-key: ${GRAPL_API_KEY}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.scope | string; one of team | Yes | | | response.data.unit | string; one of requests | Yes | | | response.data.observedAt | string; date-time | Yes | | | response.data.minute | object | Yes | | | response.data.minute.used | integer; minimum: 0 | Yes | | | response.data.minute.resetsAt | string; date-time | Yes | | | response.data.month | object | Yes | | | response.data.month.used | integer; minimum: 0 | Yes | | | response.data.month.resetsAt | string; date-time | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Read an observed transaction outcome Reads the configured observer. Source: https://docs.grapl.io/developers/api/reference/getTransactionStatus `GET /v1/swap/status/{signature}` Reads the configured observer. A not_observed result is not proof that a transaction is absent, pending or failed on chain. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | signature | path | Yes | string | | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/status/${SIGNATURE}" \ --header "x-api-key: ${GRAPL_API_KEY}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.signature | string | Yes | | | response.data.status | string; one of not_observed, finalized, failed | Yes | | | response.data.commitment | string; one of finalized | Yes | | | response.data.slot | string / null; maxLength: 39 | Yes | | | response.data.observedAt | string / null; date-time | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Quote the best direct pool Provide the input/output mint pair and amount; no pool address is required or accepted. Source: https://docs.grapl.io/developers/api/reference/quoteSwap `GET /v1/swap/quote` Provide the input/output mint pair and amount; no pool address is required or accepted. Compares up to 50 indexed candidates using the native engine, including fees and liquidity. ExactIn maximizes output; ExactOut minimizes input. Ties use lexical pool address order. 404 NO_POOLS means the pair is absent; 422 NO_QUOTABLE_POOL means no supported pool can fill the whole amount. RPC failures abort selection with 503, never a partial ranking. Exceeding 50 candidates returns 422 TOO_MANY_POOLS. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | inputMint | query | Yes | string; solana-address | SPL mint to sell. Paired with outputMint for automatic pool selection. | | outputMint | query | Yes | string; solana-address | SPL mint to receive; must differ from inputMint. | | amount | query | Yes | string; maxLength: 20 | Positive u64 token atoms as a decimal string. Input atoms for ExactIn; output atoms for ExactOut. For 6 decimals, 1000000 represents one token. | | swapMode | query | No | string; one of ExactIn, ExactOut; default: ExactIn | ExactIn fixes input and maximizes output; ExactOut fixes output and minimizes input. | | slippageBps | query | No | integer; minimum: 0; maximum: 10000; default: 50 | Slippage tolerance in basis points: 50 = 0.5%. Sets the returned threshold; does not change which pool has the best nominal quote. | | taker | query | No | string; solana-address | Optional wallet context for future contextual adapters. Providing an address is not proof of authorization. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/quote" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --data-urlencode "inputMint=${INPUT_MINT}" \ --data-urlencode "outputMint=${OUTPUT_MINT}" \ --data-urlencode "amount=${AMOUNT}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 200 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.pool | string; solana-address | Yes | Selected direct pool. Output only; callers specify mints, not a pool. | | response.data.requestId | string | Yes | | | response.data.inputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.outputMint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.inAmount | string; maxLength: 39 | Yes | Gross input in token atoms, including the trade fee. | | response.data.outAmount | string; maxLength: 39 | Yes | Expected output in token atoms after the pool trade fee. | | response.data.otherAmountThreshold | string; maxLength: 39 | Yes | Minimum output for ExactIn; maximum input for ExactOut. | | response.data.swapMode | string; one of ExactIn, ExactOut | Yes | | | response.data.slippageBps | integer | Yes | | | response.data.priceImpactPct | string / null | Yes | Null when a price-impact metric is unavailable; never fabricated as zero. | | response.data.contextSlot | string; maxLength: 39 | Yes | Confirmed RPC snapshot slot for the selected pool. Other candidates may use different slots. | | response.data.expiresAt | string; date-time | Yes | UTC quote expiry, 10 seconds after selection starts. No liquidity is reserved. | | response.data.fees | array | Yes | | | response.data.fees[].mint | string; solana-address | Yes | Base58-encoded Solana public key. | | response.data.fees[].amount | string; maxLength: 39 | Yes | | | response.data.fees[].kind | string; one of trade, lp, protocol, hook | Yes | trade is the total input trade fee; it is not an additional fee or an LP/protocol allocation breakdown. | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | ## Isolated response fixture Captured from the API's isolated selection-test fakes. Test addresses and amounts illustrate the contract, not a market quote. Only the random request ID is normalized. ```json { "success": true, "data": { "pool": "11111111111111111111111111111111", "requestId": "isolated-selection-fixture", "inputMint": "11111111111111111111111111111111", "outputMint": "So11111111111111111111111111111111111111112", "inAmount": "1000", "outAmount": "990", "otherAmountThreshold": "985", "swapMode": "ExactIn", "slippageBps": 50, "priceImpactPct": null, "contextSlot": "42", "expiresAt": "2026-10-12T12:30:10.000Z", "fees": [ { "mint": "11111111111111111111111111111111", "amount": "3", "kind": "trade" } ] } } ``` The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Stream transaction status updates Stream finalized observations from the configured chain/program. Source: https://docs.grapl.io/developers/api/reference/streamTransactionStatus `GET /v1/swap/status/{signature}/events` Stream finalized observations from the configured chain/program. Resume using Last-Event-ID. Not-observed is not proof of absence on chain. Each connection counts as one API request; frames do not. Stop on complete. **Authentication:** `x-api-key` integration credential. ## Parameters | Name | Location | Required | Type and constraints | Description | | --- | --- | --- | --- | --- | | signature | path | Yes | string | | | last-event-id | header | No | string | Last delivered transaction.status event ID. Replays newer persisted events before following live updates. | ## Request example ```sh curl --fail-with-body --get "${GRAPL_API_ORIGIN}/v1/swap/status/${SIGNATURE}/events" \ --header "x-api-key: ${GRAPL_API_KEY}" ``` ## Responses | HTTP status | Meaning | | --- | --- | | 200 | SSE: snapshot, transaction.status (with id), heartbeat, complete, reconnect or stream-error. Only finalized/failed outcomes are observed currently. | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Submit a signed transaction Submit a signed transaction. Source: https://docs.grapl.io/developers/api/reference/submitSignedTransaction Currently returns 503 TRADING_NOT_CONFIGURED. The execution bridge is not implemented. `POST /v1/swap/submit` Submit a signed transaction. Submission does not establish confirmation or settlement. **Authentication:** `x-api-key` integration credential. ## Parameters No query or path parameters. ## Request body Content-Type: `application/json`. | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | body.signedTransaction | string; minLength: 4; maxLength: 1644 | Yes | | ## Request example ```sh curl --fail-with-body --request POST "${GRAPL_API_ORIGIN}/v1/swap/submit" \ --header "x-api-key: ${GRAPL_API_KEY}" \ --header "Content-Type: application/json" \ --data @signed-request.json ``` `signed-request.json` must contain the documented body with your application's signed transaction. This request currently returns 503; it is not a working submission flow. ## Responses | HTTP status | Meaning | | --- | --- | | 202 | Successful request | | 400 | Invalid parameters, unknown fields or malformed request body. | | 401 | Missing, expired or revoked credential. | | 403 | Credential audience or team does not permit this operation. | | 404 | Requested resource or indexed mint pair was not found. | | 409 | Resource is no longer in the required state. | | 413 | Request body exceeds the documented size limit. | | 422 | No supported complete quote, too many candidates or amount limits exceeded. | | 429 | Team request quota or stream capacity exhausted. Respect Retry-After when supplied. | | 500 | Unexpected server failure. | | 503 | Required service, quote worker or execution bridge is unavailable. | ### 202 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of true | Yes | | | response.data | object | Yes | | | response.data.signature | string | Yes | | | response.data.status | string; one of submitted | Yes | | ### 400 fields | Field | Type and constraints | Required | Description | | --- | --- | --- | --- | | response.success | boolean; one of false | Yes | | | response.error | object | Yes | | | response.error.code | string | Yes | Stable machine-readable error code. | | response.error.message | string | Yes | Human-readable failure description; never contains credentials. | The interactive playground is unavailable because no service origin is configured. Replace `{GRAPL_API_ORIGIN}` and `{GRAPL_API_KEY}` with your deployment values, and provide valid values for the schema-generated request inputs before running a sample. Responses without captured examples show Empty; the response schema remains available. [All public endpoints](https://docs.grapl.io/developers/api/reference) · [OpenAPI JSON](https://docs.grapl.io/openapi.json) # Observe transaction status Read finalized outcomes and consume resumable authenticated event streams. Source: https://docs.grapl.io/developers/api/status-streams Subscribe to `GET /v1/swap/status/{signature}/events` with your integration key in the `x-api-key` header. The response is `text/event-stream`. It tracks observations for the configured grapl program and chain. Polling `GET /v1/swap/status/{signature}` returns the same current snapshot in the normal `{ "success": true, "data": ... }` envelope. ## Outcomes | Status | Meaning | | -------------- | -------------------------------------------------------------------------------------------------- | | `not_observed` | This observer has no committed receipt yet. It does not prove absence, failure or expiry on chain. | | `finalized` | A finalized receipt reports `meta.err: null`. | | `failed` | A finalized receipt reports an execution error. | The current observer does not emit `processed` or `confirmed`. Submission acceptance is separate from chain finality. Failed execution is not a successful swap, and finalized success does not itself describe amounts, rewards or LP movements. Those require decoded transaction events. ## Event contract | SSE event | Payload and client action | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `snapshot` | Initial status when no durable observation exists. No event ID. | | `transaction.status` | Observed outcome with a stable decimal-string `id`, signature, status, commitment, slot and observation timestamp. Save its SSE ID after processing. | | `heartbeat` | Connection liveness timestamp; no event ID or additional request charge. It does not certify indexer freshness. | | `complete` | Terminal snapshot. Stop reading and do not reconnect for this signature. | | `reconnect` | Connection lifetime reached. Reconnect after the indicated delay, preserving the last processed ID. | | `stream-error` | Safe `code` and `retryable` fields. Stop; retry only when indicated. | An illustrative durable event looks like this: ```text event: transaction.status data: {"id":"123","signature":"","status":"finalized","commitment":"finalized","slot":"456","observedAt":"2026-10-12T12:00:00.000Z"} id: 123 retry: 3000 ``` Keep IDs and slots as strings. The SSE payload is the event object directly, without the REST success envelope. An HTTP 200 stream may subsequently report `stream-error`; receiving headers alone does not mean the transaction succeeded. ## Reconnection and authentication Send the last processed ID in `Last-Event-ID` on reconnect. Resume is scoped to the same signature and observer. A previously acknowledged terminal event is not repeated; `complete` still supplies its current snapshot. Delivery can repeat after an interrupted connection if the client has not recorded the ID. Deduplicate by ID and never infer ordering from JavaScript floating-point conversion. An unknown, deleted or mismatched cursor returns HTTP 409 before opening the stream. Fetch a fresh REST snapshot and restart without a cursor if still needed. Honor HTTP 429 `Retry-After` and use backoff with jitter for transient failures. Reauthentication is required after revocation or expiry. Use an SSE-capable client supporting request headers, or an authenticated same-origin server proxy. Native browser `EventSource` cannot set `x-api-key`; never place API keys in a URL. With `fetch`, parse complete SSE frames across arbitrary chunk boundaries using a maintained parser; a network chunk is not one event. Cancel the reader after `complete` and when the view is closed. Each admitted connection, including reconnects, counts as one API request. Frames do not count separately. A separate shared team cap bounds concurrent streams. Heartbeats are sent every 15 seconds and connections rotate after 60 seconds by default. This feed follows chain finality and indexer observation; it does not promise immediate blockchain delivery. SSE framing and reconnect semantics follow the [server-sent events standard usage](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). The HTTP transport uses [Hono streaming](https://hono.dev/docs/helpers/streaming). See [Events & AsyncAPI](https://docs.grapl.io/developers/api/events) for every message schema, observed payload examples, and JSON/YAML downloads. # Create tokens and pools Prepare factory issuance and a direct CLMM pool with explicit mint ordering and price. Source: https://docs.grapl.io/developers/create-market Use the client from the [Getting started](https://docs.grapl.io/developers/sdk). All results are unsigned and require the [transaction lifecycle](https://docs.grapl.io/developers/transactions). ## Issue a token `prepareCreateToken` accepts `tokenId`, `decimals`, `initialSupply`, `mintAuthority`, and an optional `creator`. Supply is a `bigint` quantity in atoms. The factory creates a recipient PDA; it is not the creator's associated token account. Use the returned recipient as an explicit token-account override when spending that issuance. See [Token factory](https://docs.grapl.io/guides/token-factory) for supply and authority rules. ## Create a direct pool Canonical mint order is ascending by address bytes. `orderMints` orders the pair, but never silently inverts a price. The initial `sqrtPriceX64` always represents `sqrt(B atoms / A atoms) × 2^64` **after ordering**. ```typescript import { defaultPoolConfiguration, orderMints } from "@grapl/sdk"; import type { GraplClient, PreparedTransaction } from "@grapl/sdk"; import type { Address } from "@solana/kit"; export async function prepareEqualAtomPricePool( client: GraplClient, firstMint: Address, secondMint: Address, poolId: bigint, ): Promise { const [mintA, mintB] = orderMints(firstMint, secondMint); return client.prepareCreatePool({ poolId, mintA, mintB, sqrtPriceX64: 1n << 64n, configuration: defaultPoolConfiguration(), }); } ``` This example starts at equal **atom** price, which is equal displayed token price only if decimals match. The SDK helper's unhooked default uses tick spacing 64, a 3,000 ppm trade fee, a 10,000 ppm fee ceiling, and allocates the entire post-protocol remainder to LPs. Mandatory 10% protocol retention still applies in Pool 6. These are convenience defaults, not requirements for every grapl pool. Pool creation does not supply liquidity. Confirm creation, then [open and fund a position](https://docs.grapl.io/developers/liquidity). Reserves currently use classic SPL Token; Token-2022 reserve mints are unsupported. ## Curves and graduation The protocol has curve creation, trading, graduation, and remainder instructions. Use the existing generated builders with `prepareInstructions` when you need those operations; this direct client does not offer curve or zap convenience methods. Read [Bonding curves](https://docs.grapl.io/concepts/bonding-curve) and [Zaps](https://docs.grapl.io/guides/zaps) for semantics and the [instruction reference](https://docs.grapl.io/reference/instructions) for exact accounts. # Discover pools Find grapl pools directly from RPC by mint or pair, without an API key, wallet, or indexer. Source: https://docs.grapl.io/developers/discovery Use `findPools` to discover the current pool accounts owned by a configured grapl deployment. It accepts a mint, an optional paired mint, or no mint filter. The scan targets Pool6; older account versions are excluded by an explicit version filter. Pools with hooks, direct CLMM pools, and virtual-reserve curve pools share the same discovery path. ## Query a mint or pair Use deployment configuration from a trusted source. The expected genesis hash pins the network independently of the RPC URL. The SDK is currently a source package; follow [Getting started](https://docs.grapl.io/developers/sdk) for package availability. ```typescript import { createKitRpcPort, findPools } from "@grapl/sdk"; import type { Address } from "@solana/kit"; export async function discoverPair( rpcUrl: string, programAddress: Address, genesisHash: string, mint: Address, pairedMint: Address, ) { return findPools({ rpc: createKitRpcPort(rpcUrl), programAddress, genesisHash, mint, pairedMint, }); } ``` Omit `pairedMint` to find every pair containing `mint`. Omit both to scan all pools. An existing `GraplClient` also exposes `client.findPools({ mint, pairedMint })`; a fee payer is not required by the standalone function. Each result contains `address`, `slot: bigint`, and decoded `state`. The state includes mints, vaults, creator, configuration, hook program, mode, current tick, square-root price and active liquidity. Wide quantities remain `bigint`. ## Understand the result The default RPC adapter reads at **finalized** commitment. A mint search queries both mint fields, validates every returned account, filters the other mint if supplied, and deduplicates by pool address. If the two scans observe different slots, the newest observation wins for duplicate addresses. Each result retains its own slot; the result is not an atomic cross-pool snapshot. The decoder verifies owner, discriminator, Pool6 size/version, canonical PDA and bump. A custom RPC adapter must implement `getPoolAccounts`, supply matching account/context slots, and report whether its scan is complete. Incomplete scans and responses below `minContextSlot` reject; the SDK never silently labels a truncated provider response as complete. Standard RPC has no portable GPA pagination, so providers must support the complete filtered request. Discovery returns candidates, not executable quotes. A pool may have zero active liquidity, out-of-range liquidity, curve inventory constraints, or a hook requiring additional context. Refresh state and evaluate the requested amount and direction before choosing a route. Active liquidity alone is not TVL, and vault balances can include accrued fees or curve reserves. ## Keep a catalog current Bootstrap with a finalized account scan, then consume the [on-chain events](https://docs.grapl.io/reference/events) and periodically rescan. Use subscriptions as wake-up hints and recover missed receipts with paginated history. The grapl indexer uses the same SDK pool filters and PDA decoder. For a service-backed catalog, use [HTTP pool discovery](https://docs.grapl.io/developers/api/discovery). For an independent analytics integration, follow [DEX discovery and indexing](https://docs.grapl.io/integrations/discovery). # Resolve hooks in SDK operations Keep hook policy in external packages while preparing authenticated lifecycle context. Source: https://docs.grapl.io/developers/hooks A hookless pool is valid. When a pool has a hook, the direct client requires a matching `GraplHookResolver`. The resolver belongs to the hook's external package; the core SDK does not embed an allowlist, trading window, reward scheme, or dynamic-fee policy. ## Match the pool's contract Each resolved plan binds the configured hook program, ABI version 4, per-operation payload, access descriptors, and account metas. Dependencies carry explicit expected ownership and executable status. Missing resolvers, unknown programs, or incompatible dependencies reject preparation. The generated TypeScript declarations define the exact resolver interface. Read [Hook ABI](https://docs.grapl.io/reference/hook-abi) and [permissions](https://docs.grapl.io/hooks/permissions) before providing an implementation. ## Compose rules inside one hook One hook can combine several policies. Resolve all declared dependencies in the expected order and preserve each account's permitted access by phase. An allowlist proof does not replace caller authentication, and a time policy must use the authenticated on-chain clock. [Policy composition](https://docs.grapl.io/hooks/compose-policy) explains the program side. [Wallet context](https://docs.grapl.io/integrations/wallet-context) distinguishes the transaction signer, operation actor, source authority, and destination owner, especially for routers or sponsors. ## Prepare and verify Pass the resolver through the client configuration according to its current types. The transaction preparation step preserves hook payload and account ordering, then simulates the complete operation. On-chain callback authentication and permission checks remain authoritative. A rejected callback can roll back the operation; network fees may still be charged. The SDK resolver is not an independent security review or proof that an arbitrary hook can be routed by Jupiter. For large proofs or account lists, read [Address Lookup Tables](https://docs.grapl.io/developers/address-lookup-tables). Never shorten a proof, remove required accounts, or disable a callback just to fit a transaction. # Manage liquidity with the SDK Prepare position creation, liquidity changes, fee collection, and account closure. Source: https://docs.grapl.io/developers/liquidity Fetch the pool with `client.fetchPool(poolAddress)` before constructing a range. The client validates the pool's owner, layout version, PDA, mints, and vaults. The [user guide](https://docs.grapl.io/user-guides/liquidity) explains the economic choices; this page describes the operation contract. ## Open a range ```typescript import type { GraplClient, PreparedTransaction } from "@grapl/sdk"; import type { Address } from "@solana/kit"; export async function prepareFullRangePosition( client: GraplClient, pool: Address, positionId: bigint, ): Promise { return client.prepareOpenPosition({ pool, positionId, range: { fullRange: true }, }); } ``` For a concentrated position, supply `range: { tickLower, tickUpper }`. Ticks must be aligned to the pool's spacing and within its supported domain. Confirm the position creation before preparing a separate add transaction that reads it. ## Add or remove `prepareAddLiquidity` and `prepareRemoveLiquidity` take the same core fields: `pool`, `position`, positive `liquidity`, `limitA`, and `limitB`. `liquidity` is a liquidity unit amount, not token atoms. Limits are token atoms: | Operation | `limitA` and `limitB` mean | | --------- | ------------------------------------------------------------ | | Add | Maximum amounts of A and B you permit the operation to spend | | Remove | Minimum amounts of A and B you require back | The SDK resolves associated token accounts and prepares missing accounts idempotently. `actor`, `tokenAccountA`, and `tokenAccountB` can be explicit overrides. Custom accounts must match the actor and mint. Pool hooks still apply; unsupported or missing hook resolvers reject preparation. ## Collect fees and close Use `prepareCollectPositionFees({ pool, position })` for a position's accrued fees. Configured beneficiary fees use `prepareCollectBeneficiaryFees({ pool, beneficiaryIndex })`; they are a separate claim. Remove all liquidity and collect owed balances before `prepareClosePosition({ position })`. Close only after the prior transactions have confirmed. Inspect the actual rent refund and network fee independently; shared pool/tick storage is not automatically closed with a position. `prepareDonate({ pool, amountA, amountB })` donates into the pool's fee accounting; it is not an add-liquidity operation. For more detail, see [LP lifecycle](https://docs.grapl.io/concepts/lp-lifecycle), [positions](https://docs.grapl.io/concepts/positions), and [fees](https://docs.grapl.io/concepts/fees). # Create and discover pool ALTs Prepare shared lookup tables with the SDK, extend them in confirmed batches, and discover candidates by pool through RPC. Source: https://docs.grapl.io/developers/pool-lookup-tables The SDK's pool ALT increment adds creation, extension planning, and RPC discovery around the convention **first stored address = pool address**. The helpers are implemented in the source SDK; they do not imply a deployed protocol catalog. An authenticated protocol registry, automatic selection during swaps, and popularity-based maintenance remain separate proposals. A pool can have multiple tables or generations. The lookup starts with the selected pool; a mint can help discover pools, but does not combine all those pools' vaults and ticks into one shared table. ## The operation sequence ```text Existing pool → prepare empty ALT creation → application signs, submits, and confirms → prepare and confirm each extension batch → wait for new addresses to become active → discover and validate pool ALT candidates through RPC → choose useful tables for the operation → prepare the operation with transaction.lookupTables ``` Creation and extension are separate transactions. A failure after creation can leave an empty or partly populated table and its SOL deposit. Read actual state before resuming; a prepared transaction has not changed the chain. ## 1. Prepare creation `prepareCreatePoolLookupTable` requires an existing `pool`, a table `authority`, and a rent `payer`. The client's fee payer continues to fund transaction network fees. These roles may use different addresses, and `requiredSigners` describes the prepared transaction's signature requirements. ```typescript import type { GraplClient, PreparedPoolLookupTableCreation } from "@grapl/sdk"; import type { Address } from "@solana/kit"; /** Prepare an empty shared table; the application confirms it before extension. Example: preparePoolTable(client, pool, authority, payer). */ export async function preparePoolTable( client: GraplClient, pool: Address, authority: Address, payer: Address, ): Promise { return client.prepareCreatePoolLookupTable({ pool, authority, payer }); } ``` The result adds `tableAddress`, `recentSlot`, and `extensionPlan` to the ordinary prepared-transaction fields. It creates an empty ALT; it does not populate the table. Creation does not require the designated table authority to sign, but extension does. Follow the [transaction lifecycle](https://docs.grapl.io/developers/transactions) before continuing. The ALT address derives from its authority and recent slot, not directly from the pool. Reusing that authority/slot pair cannot create a second table. Use a fresh recent slot for another table or generation; the SDK rejects an address that already exists. The planned entries start with the pool, then canonical dependencies including mints, vaults, the tick directory, and hook-related addresses. Use `additionalAddresses` for reviewed operation-specific dependencies, such as initialized tick pages or extra hook accounts. Duplicate entries are removed. Inclusion in a table does not grant account access or guarantee that an address is compressible in every transaction. ## 2. Extend in batches `extensionPlan.batches` contains address lists, not signed transactions. Each list is input for a separate `prepareExtendPoolLookupTable` call after its prerequisites confirm. The default batch size is 20 addresses; `maxAddressesPerBatch` accepts 1–20 when planning. For later additions or recovery, `planPoolLookupTableExtension` re-reads the table, checks its authority and pool marker, removes existing entries, and returns the remaining batches. It rejects a resulting table above 256 addresses. Create a new table explicitly when another generation is needed. ```typescript import type { ExtendPoolLookupTableIntent, GraplClient, PreparedTransaction, } from "@grapl/sdk"; /** Re-read and simulate one batch after the previous transaction confirms. Example: preparePoolTableBatch(client, reviewedBatch). */ export async function preparePoolTableBatch( client: GraplClient, reviewedBatch: ExtendPoolLookupTableIntent, ): Promise { return client.prepareExtendPoolLookupTable(reviewedBatch); } ``` Supply `pool`, `table`, `authority`, `payer`, and that batch's `addresses`. Extension validates an active mutable table and its authority. A first extension anchors index zero to the pool; subsequent extensions preserve it. A batch with no new entries rejects instead of pretending to make progress. An empty caller address list can still seed the pool marker on the first extension. The full signed transaction size is checked. Batching does not bypass the transaction envelope, and extension does not silently split an oversized request. Re-prepare every batch against current state rather than collecting signatures for a stale sequence. ## 3. Discover tables by pool `findPoolLookupTables` queries the ALT program over RPC, filtering the first stored address at offset 56. No grapl API key or new core registry instruction is required. ```typescript import type { GraplClient, PoolLookupTableDiscovery } from "@grapl/sdk"; import type { Address } from "@solana/kit"; /** Find candidates from an authority the application has independently reviewed. Example: discoverPoolTables(client, pool, authority). */ export async function discoverPoolTables( client: GraplClient, pool: Address, trustedAuthority: Address, ): Promise { return client.findPoolLookupTables({ pool, trustedAuthorities: [trustedAuthority], }); } ``` Discovery returns `candidates`, `rejected` entries with reasons, `scannedCount`, and the RPC context `slot`. A candidate exposes its authority, stored and active addresses, `fullyWarmedUp`, `containsPoolAddresses`, and the caller's applied `trust` filter. Anyone can create an ALT whose first entry is a public pool address. This marker identifies candidates; it does not prove endorsement by the pool creator or grapl. Use independently trusted authorities or exact table addresses. When both `trustedAuthorities` and `trustedTables` are supplied, a table must satisfy both. A frozen table has no current authority and cannot prove historical authority provenance; it can match a trusted address filter. `containsPoolAddresses` reports coverage of the SDK's canonical pool dependencies. It does not guarantee coverage of every tick page, hook proof, or wallet account required by your operation. Inspect the actual needed accounts before selecting tables. ## 4. Confirm warmup and use explicitly New entries require warmup before they can be used. The SDK exposes only the active prefix for compression and conservatively rejects deactivating tables. Re-fetch discovery results until the entries you need are active; `fullyWarmedUp` reports whether the whole table is ready. Pass selected addresses in `transaction.lookupTables` when preparing the dependent operation. The SDK does not automatically create, discover, select, or extend tables during a swap. Compare actual serialized size for the operation; a table has message overhead and can provide no benefit when it covers too few eligible accounts. See [Address Lookup Tables](https://docs.grapl.io/developers/address-lookup-tables) for an operation example. Signers remain inline. Wallet-specific accounts and positions need not accumulate in a shared pool table, and hook permissions remain enforced independently of compression. ## RPC and lifecycle boundaries A custom RPC port needs optional `getSlot` support for creation and `getProgramAccounts` for discovery. The standard Kit port implements both. Existing operations remain compatible with custom ports that omit those capabilities. Discovery accepts `maxCandidates` from 1 to 1,000, defaulting to 100. An over-limit or explicitly incomplete response fails rather than returning a silently truncated list. The standard adapter relies on the provider's complete-response contract; it cannot cryptographically detect an RPC that silently omits matching accounts. ALTs are append-only. Full tables require a new generation using a fresh recent slot; entries cannot be evicted or reordered. Freeze, deactivate, and close helpers are outside this increment. Table ownership belongs to Solana's ALT program; the configured authority controls supported updates, and rent funding remains explicit. See [Solana's ALT guide](https://solana.com/developers/cookbook/transactions/lookup-tables) and [v0 lifecycle](https://docs.anza.xyz/proposals/versioned-transactions). ## Proposed: managed protocol tables The longer-term design is a grapl-maintained catalog keyed by deployment and pool, with explicit provenance, rotation, and table-retirement rules. Usage measurements could help create new generations containing frequently used account combinations. An RPC-readable authenticated registry could support that policy, with an optional API cache. Those services are not provided by the first-address convention or the SDK helpers. Protocol-funded setup requires a designated payer and signing workflow. Automatic selection should optimize the current transaction's actual bytes, preserve less-used pools, and never change the operation or bypass a hook when a table is unavailable. # Development sandbox Check sandbox service availability, network identity, test-asset claims and wallet integration prerequisites. Source: https://docs.grapl.io/developers/sandbox **Verified sandbox scope.** The deployed public application completed two faucet claims and seven SDK actions with finalized receipts. This verifies the tested CLMM flow with classic SPL tokens, no hook and an ephemeral Wallet Standard test wallet. External wallet extension compatibility remains unverified. Discovery of the new test pool is verified; the indexed receipt history still has an explicit gap. grapl runs a dedicated test network for application developers, with a faucet for **SOL (Test)** and **USDG (Test)**. This is a grapl environment, separate from Solana's official Devnet, Testnet and Mainnet. Test balances are disposable; USDG (Test) is a custom SPL token with no relationship to an official USDG deployment or issuer. ## Current connection addresses Open the [grapl application](https://grapl.io) to use the grapl Sandbox. The main application uses this test network; a separate sandbox website is deferred. RPC, WebSocket, faucet and HTTP API service addresses remain as listed below. **DNS, valid HTTPS/WSS and runtime configuration for these backend service addresses were confirmed on October 12, 2026.** The sandbox instance and genesis are unchanged. The previous addresses record the endpoints used before migration; they remain here as historical evidence, not a guarantee of permanent aliases. The public UI E2E below records the release and application hostname used for that test. | Service | Current address | Previously verified address | | -------------- | ---------------------------------- | --------------------------------------------------------------------- | | Application | [grapl.io](https://grapl.io) | Historical public UI E2E on release `1711006`; see verification below | | RPC | `https://rpc.sandbox.grapl.io/rpc` | `https://rpc-sandbox.grapl.io/rpc` | | WebSocket | `wss://rpc.sandbox.grapl.io/ws` | `wss://rpc-sandbox.grapl.io/ws` | | Faucet service | `https://faucet.grapl.io` | `https://faucet-sandbox.grapl.io` | | HTTP API | `https://api.sandbox.grapl.io` | `https://api-sandbox.grapl.io` | Read [the current sandbox configuration](https://faucet.grapl.io/v1/sandbox/config) and use its returned `rpcUrl` and `wsUrl`. The new configuration and RPC endpoints returned the same identity recorded below. Verify genesis when connecting; a hostname change is not permission to accept a different sandbox identity. The earlier checks covered HTTPS RPC, WSS slot notifications, finalized faucet claims and balances, idempotent replay, and authenticated API discovery of indexed pools. Requests to the API without a key returned `401`. After the indexer correction on October 12, 2026, public discovery returned three pools, including the pool created by the public UI E2E. The receipt-history gap remains explicit; discovery recovery does not establish complete transaction history or historical volume. A successful process health check alone does not establish chain readiness or API freshness. The transaction E2E used RPC and the faucet directly, without an HTTP API key supplied by the test runner. Its results apply to the tested build and endpoints; the new hostnames were verified separately through HTTPS, network identity checks and a WSS slot notification. The controlled validator restart preserved the tested state without establishing indefinite retention. ## RPC and network identity The `GET /v1/sandbox/config` contract returns `version: 1`, `network: "grapl-sandbox"` and an `instanceId` UUID. It supplies `rpcUrl`, `wsUrl`, `genesisHash`, `programId`, `usdGTestMint`, `usdGTestDecimals: 6`, and `explorerUrl`. The current explorer URL is `null`; applications must not invent a destination when it is absent. `scan.sandbox.grapl.io` is under development and is not yet an available explorer. | Identity field | Verified value | | ------------------ | ---------------------------------------------- | | `instanceId` | `16460ef3-f549-4042-a7c9-9a300353f69c` | | `genesisHash` | `3WqMx8fg1WJqxujF2T2mRTU2mcDMiLMjSER415fv6jTF` | | `programId` | `FF3sV6ho4jKo6Yf2746q9GM6B4mvEWpVdpb4QLvEbtL9` | | `usdGTestMint` | `28tB7PXgWf4GjXdKRdnSzTGqT4WQLV1XhB1rApVZ2eH5` | | `usdGTestDecimals` | `6` | The configuration and RPC independently returned the same genesis shown above before and after the hostname migration. Network identity failures return `503 SANDBOX_NOT_READY`. Do not configure an application from historical fixture identities or fall back to Mainnet when sandbox configuration is unavailable. The `/health` route reports process liveness only. Before configuring the SDK, query Solana's [`getGenesisHash`](https://solana.com/docs/rpc/http/getgenesishash) and compare the result with the verified deployment identity. Track `instanceId` as well so the application can identify a replacement sandbox instance. If the values change, stop and obtain the updated deployment identity; do not silently accept a new chain. A genesis hash alone does not describe the reset or retention policy. See Solana's [cluster reference](https://solana.com/docs/references/clusters) for the roles of its official networks. Selecting **Solana Devnet** connects to that public cluster, not to the grapl sandbox. The sandbox has its own ledger, genesis, programs and test balances. Devnet SOL does not fund this ledger, and sandbox faucet claims do not appear as Devnet or Mainnet balances. ## Wallet and custom RPC compatibility Connecting a wallet account does not establish that the wallet can sign for this isolated cluster. Changing the application's RPC also does not automatically change the wallet's own network, simulation service or balance display. For a direct SDK integration, use this sequence. The completed public UI test used an ephemeral Wallet Standard wallet. Compatibility with external wallet extensions still requires separate validation. 1. Read the sandbox configuration and compare its `instanceId` and genesis with the published deployment identity. Query `getGenesisHash` through the configured RPC before proceeding. 2. Configure the application with the exact `rpcUrl` and `wsUrl`. The [direct SDK client](https://docs.grapl.io/developers/sdk) supports an explicit RPC URL and expected genesis; it does not require the optional HTTP API. 3. Use a wallet or signer integration verified for this independent cluster and the prepared v0 transaction format. The deployed browser integration requires signing without automatic broadcasting, so the application can recheck network identity before submission. An integration exposing only `signAndSendTransaction` does not meet that requirement. 4. Keep simulation, blockhash retrieval, submission and receipt/balance checks on the same verified sandbox RPC. If the wallet rejects the cluster or cannot sign the transaction, stop that flow; selecting Devnet does not make it compatible. Only follow a wallet's custom-network setup when its own documentation supports independent Solana clusters. Support for another RPC provider on an existing network is not enough. Wallet-specific setup steps and a tested compatibility list remain pending. An ephemeral Wallet Standard test wallet does not establish compatibility with Phantom or other external extensions. Phantom's [official testnet instructions](https://help.phantom.com/articles/5997313271699) list Solana Devnet and Testnet. They do not establish support for adding this independent cluster. This guide therefore does not provide a Phantom custom-RPC menu path or treat its Testnet Mode as a sandbox connection. The faucet contract accepts a recipient public key without a recipient signature. Receiving a test allocation alone does not validate that the connected browser wallet can sign subsequent protocol transactions. Use the sandbox RPC to verify test balances, and use an explorer only when the configuration supplies a compatible URL. ## Test-asset faucet The faucet accepts two assets and fixes the amount on the server. Its configuration declares each allocation in `faucet.assets`; amounts remain decimal strings in base units. | Asset identifier | Display asset | Amount per admitted claim | `amountBaseUnits` | `decimals` | | ---------------- | ------------- | ------------------------- | ----------------- | ---------- | | `SOL` | SOL (Test) | 1 | `"1000000000"` | 9 | | `USDG_TEST` | USDG (Test) | 1,000 | `"1000000000"` | 6 | The same base-unit quantity represents different display amounts because the assets have different decimals. Verify the configured test mint and its decimals; a matching symbol alone is not a verified token identity. ### Quotas and IP identity | Scope | Cooldown | Rolling 24-hour limit | Configuration | | --------------------------------------------- | --------------------------------- | --------------------- | ----------------------------------------------------------------- | | Each wallet and asset | One admitted claim per hour | 5 admitted claims | Asset `walletCooldownSeconds: 3600`; `faucet.walletDailyLimit: 5` | | IP identity, shared across wallets and assets | One admitted claim per 30 seconds | 20 admitted claims | `faucet.ipCooldownSeconds: 30`; `faucet.ipDailyLimit: 20` | Replaying an existing claim consumes no additional quota. Failed claims still consume quota. A limited request returns `429`, a `Retry-After` header and `error.retryAfterSeconds`; the current daily-limit response uses a conservative 86,400-second delay. Respect the returned delay. `faucet.ipIdentity` declares how the service identifies an IP: `connection-peer` or `trusted-proxy-chain`. The verified deployment currently reports `connection-peer`. Clients behind the same ingress peer therefore share its quota; do not describe this as a separate allowance for each end user's IP. Individual client-IP enforcement requires a configured and tested proxy chain. Clients cannot choose their quota identity with forwarding headers. ### Submit and follow a claim Send `POST /v1/faucet/claims` to the confirmed faucet origin, with a UUID in the `Idempotency-Key` header and exactly these body fields: ```json { "wallet": "", "asset": "SOL" } ``` Use `USDG_TEST` to request the test token. Caller-supplied amounts and other extra fields are rejected; the request body limit is 4 KiB. 1. Admission returns `202` while the claim is `queued` or `submitted`. The response contains `id`, `status`, `asset`, `amountBaseUnits`, nullable `signature` and nullable `failureCode`. 2. Poll `GET /v1/faucet/claims/:id`. The four states are `queued`, `submitted`, `confirmed` and `failed`. A send acknowledgement is not confirmation: `confirmed` requires finalized transaction metadata with no execution error. 3. Retry an uncertain request with the same key, wallet and asset. It returns the original claim; a terminal replay returns `200`. Changing the payload for that key returns `409 IDEMPOTENCY_CONFLICT`. 4. A failed claim reports a code such as `BLOCKHASH_EXPIRED` or `TRANSACTION_FAILED`. The service does not automatically create a replacement payment. Reconcile the configured genesis, receipt, destination, mint/decimals and resulting balance before presenting success. The service persists signed transaction bytes before broadcast. Worker recovery checks the same signature and can resend only those bytes. Temporary provider errors defer the claim for reconciliation without creating another payment or consuming additional quota. Retry scheduling is durable, allowing other eligible claims to progress while a failing claim waits. A transport error alone does not make a claim terminal. These retry behaviors have local regression coverage; remote validator restart evidence is described below. The faucet uses its own service address above. Its interfaces are documented here separately from the [published public API Reference](https://docs.grapl.io/developers/api/reference), which describes the optional grapl HTTP API. ## Use the SDK through RPC The existing [SDK Getting started guide](https://docs.grapl.io/developers/sdk) accepts an RPC URL, expected genesis hash, grapl program address and fee payer. Reuse that configuration flow with the verified sandbox values above and a disposable test account. The direct client prepares and simulates unsigned transactions; your application retains wallet signing, submission and confirmation. The [grapl HTTP API](https://docs.grapl.io/developers/api) remains optional for the direct RPC workflow. The public UI E2E completed its protocol transactions through RPC without an HTTP API key supplied by the test runner. Pool discovery is a separate service; an indexing delay does not change a finalized transaction's on-chain result. See the [transaction lifecycle](https://docs.grapl.io/developers/transactions) and [current protocol status](https://docs.grapl.io/start/status) for the implemented boundaries. ## Verification and remaining gates The public UI E2E ran at the former application address `https://sandbox.grapl.io` on release `171100677b13cb7eb9875252efce9e8ef1cf27d0`. It completed the end-to-end flow in **one run, with zero retries, in 4.1 minutes**. Publication, CI and deployment of that application release are complete. This verification preceded the backend service hostname migration, deployed with release `278aa46`, and the move to the main application at [grapl.io](https://grapl.io). The full E2E evidence remains tied to release `1711006` and its historical hostname; it does not establish a repeated E2E run on the new application origin. The public UI completed two faucet claims and seven actions: wrap SOL (Test), create a pool, open a position, add liquidity, swap, collect fees and remove liquidity. An ephemeral Wallet Standard test wallet signed the actions against the real VPS RPC and faucet over HTTPS. All nine receipts were independently rechecked at finalized commitment with `meta.err: null`, together with their exact balance changes. No HTTP API key was supplied by the test runner. Fee collection returned **2 atoms of USDG (Test)** to the wallet. An independent SDK/RPC read checked the position owner and full-range ticks `[-443584, 443584]`. After withdrawal, both the pool and position liquidity were zero, and both position fee balances were zero. The sandbox instance and genesis remained unchanged. This test covers a **direct CLMM pool with classic SPL tokens, no hook and a disposable test wallet**. It does not validate external wallet extensions, all token variants, hooked or curve pools, every SDK capability, Mainnet use or production readiness. Existing claim idempotency, quota and network identity checks remain applicable. The earlier local UI build had already completed the same lifecycle against the remote backend. Its pool was discovered and listed by separate indexer/API/proxy checks. Discovery of the new public-E2E pool was subsequently verified after the indexer correction on October 12, 2026. A public proxy read returned three pools at `observedSlot: 30294`, including `FiMJg1LvCRrQfet8jGStyB8oC3TX3DutRSFa3XfuQcPX`. Current pool snapshots now refresh independently of receipt replay. The journal retains an explicit gap where RPC history was pruned; its cursor was not advanced over that gap. Do not treat recovered discovery as complete transaction history or historical volume. Earlier remote deployment verification used a disposable recipient and confirmed both faucet allocations: 1 SOL (Test) and 1,000 USDG (Test), with finalized receipts, resulting balances and idempotent replay. It also checked a WSS slot notification, five rejected administrative RPC methods (`403`), and API pool discovery with and without authentication. Bootstrap verified the loaded executable bytes of the core and six optional hook examples. These infrastructure checks are separate from the public UI E2E above. A controlled restart of the persistent validator completed on October 12, 2026. The public check at `17:05:26 UTC`, after the validator's recorded start at `17:05:11 UTC`, confirmed unchanged configuration and genesis, the same 1 SOL (Test) and 1,000 USDG (Test) balances, and both original claims and finalized receipts. The indexer resumed and caught up automatically. This verifies the tested restart of this instance; the public UI lifecycle was verified separately. A separate, disposable Agave VPS probe preserved its genesis, one-SOL balance and finalized receipt across three stages: initial execution, validator restart and container recreation with a changed IP. That isolated probe used a different ledger and identity; its addresses must not be used to configure this sandbox. Earlier local evidence dated October 12, 2026 covers seven finalized successful transactions in a private Surfpool fixture: two faucet payments, token creation, pool creation, position creation, liquidity and a swap. Restarting the application services and PostgreSQL preserved network identity, balances, claims, quotas and observer state. These disposable fixture identities are historical evidence, not application configuration. Restarting that earlier Surfpool runtime changed its genesis while preserving test balances. The gateway and faucet then returned `503`, and the observer stopped adding to the old stream. This negative fixture verified rejection of a changed network identity. It is separate from the successful persistent Agave restart above. | Surface | Verified | Remaining work | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Configuration and faucet | Remote identity, allocations, finalized balances, idempotent replay and two claims in the public UI E2E; local retry regressions | External wallet extension compatibility | | RPC and WebSocket | Remote genesis, receipts, balances, WSS notifications and rejection of five administrative methods | Continued operational and capacity validation | | HTTP API | Authenticated discovery and public listing of all three observed pools, including the public-E2E pool; unauthenticated requests rejected | Receipt-history gap remains; historical volume and analytics are not complete | | Public browser application | Public E2E verified on release 1711006: two claims plus seven SDK actions; nine finalized successful receipts and final position state checked | External wallet extension compatibility and capabilities beyond the tested CLMM flow | | Validator persistence | Remote controlled restart preserved configuration, genesis, both test balances, claims and finalized receipts | Long-term retention policy and ongoing operational validation | | Application/database recovery | Remote indexer caught up automatically after validator restart; local service/database restart retained state | Separate restart/recovery validation for the deployed application services and database | | Ingress and operations | HTTPS/WSS access; current `connection-peer` quota identity | Individual client-IP trust configuration/testing, capacity and status/incident reporting | The sandbox is intended for testing integrations. These checks do not establish uptime guarantees, indefinite state retention, an audit or production readiness. ## Reset semantics Ordinary restarts retain the instance's identity, program artifacts, ledger and database together; the controlled validator restart above verified preservation of the tested state. The proposed intentional reset procedure stops the old instance, retains its state for diagnosis, and creates a new instance with separate volumes, a new `instanceId`, authority, test mint and genesis. Replacing only the ledger or reusing the old database with a new chain is not a supported reset. Applications must re-read configuration and verify the new genesis before reconnecting. Previous balances, transactions, pools, positions and Address Lookup Tables must not be treated as available in the replacement instance; re-fund and recreate the test resources needed for that instance. The intentional reset procedure remains subject to separate validation; the successful ordinary restart does not validate a reset. A public reset schedule, advance notice, maintenance/status reporting and verified recovery instructions have not been published. Do not assume that service restarts reset the ledger or that test state is retained indefinitely. # Getting started Prepare an unsigned transaction with the direct RPC client and keep wallet authorization in your application. Source: https://docs.grapl.io/developers/sdk The direct client uses RPC, an expected chain genesis hash, a program address, and a fee payer. It does not need a grapl API URL or key. Preparation reads accounts, assembles instructions, estimates costs, and simulates the final transaction. It does not sign or broadcast. ## Prerequisites - Node.js 24 and the `sdk/` source package in a compatible [grapl protocol checkout](https://github.com/grapl-io/grapl). - Access to a matching deployed program and funded disposable identities for local testing. No public deployment is advertised here. - Your wallet integration and a trusted RPC endpoint for that deployment. `@grapl/sdk` is currently a private source package implemented in the development checkout, not an announced npm release. Availability on a published branch must be checked separately. Build `sdk/`, then link its package directory from your application. The relative path below must point to your own checkout: ```sh # From the protocol checkout's sdk directory pnpm install --frozen-lockfile pnpm build # From your application's directory pnpm add @grapl/sdk@file:../grapl/sdk @solana/kit@8.4.0 ``` ## Configure the client This function accepts deployment values from your application's configuration. Verify the expected genesis hash independently; do not select a chain only by trusting a newly supplied RPC endpoint. ```typescript import { GraplClient, createKitRpcPort } from "@grapl/sdk"; import type { Address } from "@solana/kit"; export function connectGrapl( rpcUrl: string, programAddress: Address, expectedGenesisHash: string, feePayer: Address, ): GraplClient { return new GraplClient({ rpc: createKitRpcPort(rpcUrl), programAddress, genesisHash: expectedGenesisHash, feePayer, }); } ``` ## Prepare a first operation Create a six-decimal token with an initial supply of one token, expressed as `1_000_000n` atoms. `mintAuthority: null` requests no remaining mint authority. Token IDs must be unique for that creator. ```typescript import type { GraplClient, PreparedTransaction } from "@grapl/sdk"; export async function prepareFirstToken( client: GraplClient, tokenId: bigint, ): Promise { return client.prepareCreateToken({ tokenId, decimals: 6, initialSupply: 1_000_000n, mintAuthority: null, }); } ``` Inspect the result's `summary`, `addresses`, `creations`, `requiredSigners`, `simulation`, and cost estimates. A successful preparation means you have simulated unsigned v0 bytes, not a created token. Your application explicitly asks the wallet to sign, submits through RPC, and checks confirmation. See [transaction lifecycle](https://docs.grapl.io/developers/transactions). ## Choose your task | Task | Guide | Current integration surface | | ----------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | Swap | [ExactIn and ExactOut](https://docs.grapl.io/developers/swaps) | Prepare a chosen pool with explicit caller-provided limits | | Zap | [Compose a zap](https://docs.grapl.io/developers/zaps) | Generated builders and explicit instruction composition | | Pool Manager | [Create a market](https://docs.grapl.io/developers/create-market), [manage positions](https://docs.grapl.io/developers/liquidity) | Token/pool creation, liquidity, positions and fee claims | | Hooks | [Resolve policy accounts](https://docs.grapl.io/developers/hooks) | Explicit compatible resolver for hooked operations | | Transactions & Accounts | [Review and sign](https://docs.grapl.io/developers/transactions), [pool ALTs](https://docs.grapl.io/developers/pool-lookup-tables) | Unsigned preparation, simulation and lookup-table lifecycle | These groups describe integration tasks, not separate client classes. The client does not select the best pool, produce native executable quotes, sign, send or confirm. Curve and zap composition uses generated builders; native SOL wrapping remains explicit. Use bigint amounts and validate the program/chain identity. See [current limitations](https://docs.grapl.io/security/limitations). The isolated SDK verification covered 18 scenarios and 94 finalized transactions with disposable identities, including expected rejections. This is local verification of the tested source build; it does not establish a public deployment, npm publication or browser/WASM parity. # SDK quickstart has moved The SDK quickstart is now part of Getting started. Source: https://docs.grapl.io/developers/sdk-quickstart The installation, client setup and first unsigned operation are now in [Getting started](https://docs.grapl.io/developers/sdk). This compatibility URL redirects there in the browser. # Prepare ExactIn and ExactOut swaps Supply a selected pool and explicit execution bounds; keep quoting separate from transaction preparation. Source: https://docs.grapl.io/developers/swaps The direct SDK prepares swaps against a caller-selected pool. It does not currently discover the best route or produce an executable native quote. Obtain complete, policy-aware bounds from a compatible quote engine before building. Spot price alone does not predict a CLMM trade across ticks. ## Choose the mode | Method | Required amount bounds | | --------------------- | ---------------------------------- | | `prepareSwapExactIn` | `amountIn` budget and `minimumOut` | | `prepareSwapExactOut` | `amountOut` target and `maximumIn` | Both methods also need `pool`, `inputMint`, and `sqrtPriceLimitX64`. Amounts are `bigint` token atoms. The input mint determines direction; the price limit must be valid for that direction. ```typescript import type { GraplClient, PreparedTransaction, SwapExactInIntent, } from "@grapl/sdk"; export async function prepareBoundedSwap( client: GraplClient, reviewedIntent: SwapExactInIntent, ): Promise { return client.prepareSwapExactIn(reviewedIntent); } ``` Supply a complete reviewed intent; this wrapper does not calculate slippage or a quote. Read [swap semantics](https://docs.grapl.io/concepts/swaps) for net output, partial input consumption, and rounding. ## Account and policy resolution The client resolves the pool's token accounts and required tick pages. Initial selection conservatively includes initialized pages in the trade direction and rejects a plan requiring more than 16 pages. Specialized native account plans can use generated builders with explicit composition. A configured pool hook needs the matching resolver and dependencies. Missing eligibility, time, fee, or actor context must fail; do not substitute an unhooked quote. See [Hook integration](https://docs.grapl.io/developers/hooks). ## Native SOL and settlement The client expects funded SPL token accounts, including WSOL when the pair uses wrapped SOL. It does not wrap native SOL or close token accounts implicitly. Keep wrapping and cleanup explicit in your application. Preparation simulates but does not send. After explicit signing and submission, reconcile actual token debits, net credits, and hook effects. The optional [API quote service](https://docs.grapl.io/developers/api) is a distinct integration and is not called by these SDK methods. # Unsigned transaction lifecycle Review simulation, fees, signers, expiry, and confirmation before treating an operation as settled. Source: https://docs.grapl.io/developers/transactions Every direct `prepare…` method returns a `PreparedTransaction`. The SDK performs mandatory simulation but never opens a wallet, signs a message, broadcasts, or retries in the background. ## Inspect the prepared result | Field | What your application should do | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `transaction`, `wireBytes`, `wireBase64` | Pass the supported representation to your wallet adapter; wire encodings include zeroed signature slots | | `instructions` | Reuse the ordered instructions for explicit composition | | `requiredSigners`, `feePayer` | Ensure every required signature is supplied; payer and actor may differ | | `programAddress`, `genesisHash` | Confirm deployment identity | | `addresses`, `creations`, `summary` | Show recipients, limits, created accounts, and intended effects | | `lifetime` | Retain the blockhash and last valid block height for expiry handling | | `simulation`, `computeUnitLimit` | Review the final simulation and resource cap | | `estimatedNetworkFeeLamports` | Show network cost, including priority fees | | `estimatedRentLamports` | Show account deposits separately from network and token fees | | `serializedSize`, `lookupTables`, `minContextSlot` | Track message size, validated lookup tables, and RPC context | The priority-fee estimate is included in the network fee; do not add it twice. Rent estimates cover declared SDK creations or growth. Hook-specific deposits and arbitrary composed instructions may add costs. ## Sign, submit, confirm 1. Present the operation and its bounds to the user. 2. Request signatures from every required wallet or signer using its supported v0 interface. 3. Submit the signed bytes explicitly through your RPC integration. 4. Observe the signature at your chosen commitment and inspect execution errors. 5. Reconcile the actual state, token amounts, and fees before showing success. The wallet and RPC submission code belongs to your application. The SDK does not expose a `send` or `confirm` convenience method. For the underlying RPC contract, see [sendTransaction](https://solana.com/docs/rpc/http/sendtransaction) and [getSignatureStatuses](https://solana.com/docs/rpc/http/getsignaturestatuses). ## Refresh expired or changed plans Never replace the blockhash in a signed transaction. After expiry, read state again, prepare a new transaction, and collect new signatures. Monotonic RPC context slots are not a frozen multi-account snapshot, and simulation does not reserve state. Custom instruction composition requires another complete preparation and simulation. `prepareInstructions` accepts your ordered instructions, address map, creation inventory, and truthful review summary. The caller owns the semantics of those instructions. ## Atomicity and size Prepared v0 transactions must fit 1,232 serialized bytes, including signatures. Oversized plans fail explicitly; the SDK does not split them silently. If you implement a series of transactions, explain which accounts or deposits may remain when a later step fails and rebuild each dependent step after its prerequisites confirm. See [Address Lookup Tables](https://docs.grapl.io/developers/address-lookup-tables) for explicit compression and [Solana simulation](https://solana.com/docs/rpc/http/simulatetransaction) for RPC behavior. # Compose a zap Combine a swap and a liquidity operation using the SDK's generated instructions. Source: https://docs.grapl.io/developers/zaps The SDK exports `getZapInInstruction` and `getZapOutInstruction`. These are low-level builders; there is no `prepareZap` convenience method or `Zap` client class. Use your configured `GraplClient` to simulate the complete composition with `prepareInstructions` before wallet signing. ## Choose the operation | Operation | Builder | Protected result | | --------- | ---------------------- | -------------------------------------------------------------------------- | | Zap in | `getZapInInstruction` | Spend up to `maximumInput` and receive at least `minimumLiquidity` | | Zap out | `getZapOutInstruction` | Remove principal and receive at least `minimumOutput` in the desired asset | Both target a CLMM pool, including a graduated curve. Zap out does not collect accrued position fees. A full withdrawal may leave too little liquidity for the conversion leg; do not promise that every position can exit into one token. See [zap semantics](https://docs.grapl.io/guides/zaps) for budget isolation, rounding and rollback. ## Build with resolved accounts Resolve the actor, pool, ordered mints, vaults, actor token accounts, tick directory and owned position. Supply the hook program/authority and the exact remaining tick/hook accounts required by each leg. `swapAccountsLen` on zap in and `liquidityAccountsLen` on zap out delimit those account segments; they must agree with the instruction's account order. The example accepts already resolved accounts and arguments. It does not infer a profitable swap amount, construct hook proofs or choose a liquidity range. ```typescript import { getZapInInstruction } from "@grapl/sdk"; import type { GraplClient, PreparedTransaction, ZapInAccounts, ZapInInput, } from "@grapl/sdk"; import type { Address, AccountMeta } from "@solana/kit"; export async function prepareResolvedZapIn( client: GraplClient, program: Address, accounts: ZapInAccounts, input: ZapInInput, remaining: readonly AccountMeta[], ): Promise { const instruction = getZapInInstruction(program, accounts, input, remaining); return client.prepareInstructions({ instructions: [instruction], addresses: { pool: accounts.market.pool, position: accounts.position }, creations: [], summary: { operation: "zap-in", maximumInput: input.args.maximumInput }, }); } ``` The example assumes every required account already exists, hence `creations: []`. If the composition creates accounts, include their creation instructions and accurate deposit metadata. The supplied program must match your client's deployment. For zap out, use `ZapOutAccounts`, `ZapOutInput` and `getZapOutInstruction` with a negative liquidity delta and explicit aggregate output/price limits. ## Review the complete transaction Inspect all required signers, account privileges, simulation logs, compute and deposits. Supply hook descriptors independently for the swap and liquidity legs; a denied callback or failed bound rolls back the whole zap. Do not remove accounts to fit the message. Consider [ALTs](https://docs.grapl.io/developers/address-lookup-tables) when address overhead is the constraint. Continue with [wallet review and confirmation](https://docs.grapl.io/developers/transactions). Generated builders only assemble instructions; successful preparation is not a settled zap. # Find your way through the docs Choose the right guide for market design, application integration, or hook development. Source: https://docs.grapl.io/guides/docs-workflow The documentation follows the decisions involved in building with grapl. Start with the behavior you want, then use the reference pages to construct the corresponding operation. | You want to… | Read first | Continue with | | ------------------------------ | ---------------------------------------- | ---------------------------------------------------------- | | Understand pricing | [AMM math](https://docs.grapl.io/concepts/amm-math) | [Concentrated liquidity](https://docs.grapl.io/concepts/concentrated-liquidity) | | Create a market | [Build with grapl](https://docs.grapl.io/guides/overview) | [Direct and curve launches](https://docs.grapl.io/concepts/launches) | | Add custom behavior | [Hooks overview](https://docs.grapl.io/hooks/overview) | [Design your first hook](https://docs.grapl.io/hooks/first-hook) | | Connect a trading interface | [First integration](https://docs.grapl.io/start/quickstart) | [Client amounts](https://docs.grapl.io/integrations/sdk) | | Support a router | [Metis integration](https://docs.grapl.io/integrations/metis) | [Wallet context](https://docs.grapl.io/integrations/wallet-context) | | Inspect an account or argument | [Instructions](https://docs.grapl.io/reference/instructions) | [Accounts](https://docs.grapl.io/reference/accounts) | | Understand trust boundaries | [Security model](https://docs.grapl.io/security/model) | [Limitations](https://docs.grapl.io/security/limitations) | ## Concepts, guides, and reference Concept pages explain why a primitive works and how it affects users. Guides connect those primitives into a task. Reference pages describe fields, account roles, errors, and compatibility requirements. Examples are optional policy implementations. Their rules are not default restrictions on every pool. The [capability overview](https://docs.grapl.io/start/status) separates core operations from unsupported features. ## Search and page navigation Use the search control in the header to find concepts and symbols. The section tabs select a subject area; the sidebar lists its pages. Inside an article, the contents list links to individual headings. # Validate an integration Establish acceptance criteria for swaps, liquidity changes, and custom pool behavior. Source: https://docs.grapl.io/guides/local-tests An integration is complete when the result shown to the user matches the authorized operation and its settled state. Use the criteria below for each market and hook configuration your application supports. ## Swaps - Parse amounts using the actual mint decimals and integer atoms. - Match ExactIn or ExactOut to the user's intent, including partial-fill behavior. - Enforce minimum net output or maximum input and the chosen price limit. - Reconcile actual input, output, input fees, and optional output fees after success. - Reject missing tick pages, unavailable hook evaluators, and unsupported token programs. Use [swap modes](https://docs.grapl.io/concepts/swaps) and [quote consistency](https://docs.grapl.io/integrations/router-verification) for the detailed contract. ## Liquidity Open a position with aligned endpoints, add liquidity within token maxima, remove principal within minima, collect fees, and close only an empty position. Cover both an active range and a one-sided range outside the current price. A zap changes more than one balance. Attribute only operation-funded amounts, preserve user limits, and confirm that a failed conversion reverses the entire operation. See [LP lifecycle](https://docs.grapl.io/concepts/lp-lifecycle) and [zaps](https://docs.grapl.io/guides/zaps). ## Curves and graduation Keep virtual reserves separate from spendable vault balances. Verify the real-inventory bound and net quote target. Graduation is a separate atomic instruction: confirm CLMM mode, the creator-owned position, unchanged pool identity and hook, and the recorded remainder. The [bonding-curve guide](https://docs.grapl.io/concepts/bonding-curve) describes the transition. ## Hooks and failure behavior Validate identity, policy state, phase order, permissions, and reply bounds. Reject every enabled phase in a controlled scenario and compare all affected state before and after. Do not infer settlement from logs emitted by a transaction that failed. Follow [hook validation](https://docs.grapl.io/hooks/testing) and [security invariants](https://docs.grapl.io/security/invariants). Record the exact program and interface versions associated with each supported flow. # Build with grapl Choose a market, follow its liquidity lifecycle, and connect your application. Source: https://docs.grapl.io/guides/overview grapl provides the on-chain primitives for tokens, pools, positions, and swaps. Start with the market you need, then add a hook if your application needs custom behavior. Each path links the product behavior to its account and instruction contract. ## Choose your market | Your goal | Start here | Result | | ------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------- | | Create a classic SPL token | [Token factory](https://docs.grapl.io/guides/token-factory) | A mint and initial supply; pool creation is a separate operation | | Open trading at a chosen price | [Direct pools](https://docs.grapl.io/concepts/launches) | A CLMM pool with an initial square-root price and funded positions | | Bootstrap trading along a curve | [Bonding curve](https://docs.grapl.io/concepts/bonding-curve) | Virtual-reserve pricing backed by real inventory, then explicit CLMM graduation | | Apply custom pool rules | [First hook](https://docs.grapl.io/hooks/first-hook) | A pool rule implemented through authenticated callbacks | Direct pools and graduated pools use the same CLMM core. The curve path preserves the pool and hook at graduation; it does not create a second AMM program. ## Follow the liquidity lifecycle 1. **Configure the pool.** Select ordered mints, initial price, tick spacing, fee bounds, and an optional hook. Read [accounts](https://docs.grapl.io/reference/accounts) and [instructions](https://docs.grapl.io/reference/instructions) for the current interface. 2. **Open a position.** Choose aligned lower and upper ticks. [Positions](https://docs.grapl.io/concepts/positions) explains ownership and range selection. 3. **Fund it.** Add liquidity with token limits. An active range requires both assets; a range entirely outside the current price can take one asset. Follow [LP lifecycle](https://docs.grapl.io/concepts/lp-lifecycle). 4. **Trade or rebalance.** Use [ExactIn or ExactOut](https://docs.grapl.io/concepts/swaps). A [zap](https://docs.grapl.io/guides/zaps) can combine a swap with a liquidity change on an existing position; the client chooses the split. 5. **Remove and collect.** Withdraw principal and claim position fees as separate actions. [Fee accounting](https://docs.grapl.io/concepts/fees) explains what belongs to the position. ## Connect your application The [client-input example](https://docs.grapl.io/integrations/sdk) handles exact token parsing and formatting. It is not a complete transaction SDK. Build instructions from the current IDL and use the [source map](https://docs.grapl.io/reference/source-map) to find the implementation of each operation. For routed swaps, read [Metis integration](https://docs.grapl.io/integrations/metis) before selecting a hook. Native execution and a usable route quote have different requirements. ## Confirm the result Begin with [your first integration](https://docs.grapl.io/start/quickstart), then [validate each operation](https://docs.grapl.io/guides/local-tests). Reconcile balances, position state, and hook effects against the user’s intent. # Create a token independently Issue a classic SPL token with explicit supply and authority decisions before choosing a market. Source: https://docs.grapl.io/guides/token-factory The token factory creates a mint and its initial supply. It does **not** automatically create a pool, bonding curve, hook configuration, or launch campaign. Bring the resulting token to either a direct CLMM or a virtual-reserve curve in a separate operation. ## Choose the issuance parameters `create_token(CreateTokenArgs)` accepts: | Field | Type | Decision | | ---------------- | ---------------- | ------------------------------------------------------------- | | `token_id` | `u64` | Creator-scoped identifier in the mint PDA seeds | | `decimals` | `u8` | Display precision; supply is still specified in integer atoms | | `initial_supply` | `u64` | Positive number of atoms issued immediately | | `mint_authority` | `Option` | Explicit final authority, or `None` to revoke minting | For a token with six decimals, one million whole tokens is `1_000_000_000_000` atoms. Use an exact decimal parser or integer arithmetic; do not pass a JavaScript floating-point result as a financial amount. ## Understand the resulting accounts ```text mint PDA: ["mint", creator, token_id.to_le_bytes()] issuance PDA: ["issuance", mint] ``` Both accounts are owned by the classic SPL Token program after initialization. The issuance token account holds the initial supply and names the creator as token authority; it is not an associated token account. The creator funds the rent and signs issuance. The mint is initialized with no freeze authority. After minting the initial supply, the instruction sets the requested final mint authority. Passing `None` makes that decision irreversible under this token program: future pool creation does not restore minting rights. ## Construct the issuance instruction Construct the instruction from the matching IDL, binding the creator signer and derived accounts. After execution, verify the mint's decimals and supply, absence of freeze authority, final mint authority, and exact balance of the issuance account. Reusing the same creator and token ID must not create another mint at the same address. Use [integration validation](https://docs.grapl.io/guides/local-tests) to define acceptance criteria for the resulting mint and account balances. ## Choose the next operation - [Direct CLMM](https://docs.grapl.io/concepts/lp-lifecycle): choose initial price, fee configuration, hook, and position range. - [Bonding curve](https://docs.grapl.io/concepts/bonding-curve): deposit inventory and configure virtual reserves, bootstrap floor, and quote target. Names, symbols, images, metadata accounts, Token-2022 extensions, and Confidential features are outside this factory's current interface. Token creation alone does not make a market tradable or discoverable by Jupiter. # Troubleshoot an integration Trace rejected operations to snapshot, amount, account, and policy mismatches. Source: https://docs.grapl.io/guides/troubleshooting ## A quote succeeds but execution rejects Check the snapshot slot, current price, liquidity coverage, price limit, token balances, and slippage limits. For a hooked pool, also compare Clock, policy state, version, actor, and proof context. Execution validates current state; a quote does not reserve it. See [quote consistency](https://docs.grapl.io/integrations/router-verification) and [wallet context](https://docs.grapl.io/integrations/wallet-context). ## The token amount is off by a power of ten Read decimals from the mint, parse the user's decimal string once, and keep integer atoms through the transaction. Avoid converting financial amounts through JavaScript `Number`. Do not apply decimals a second time to an amount already returned in atoms. Use [client amounts](https://docs.grapl.io/integrations/sdk). ## A tick-page account is missing Load the canonical directory and the pages required by directional traversal. Preserve page order, ownership, writability, and duplicate checks. A price limit can still require the next initialized page to establish the traversal boundary. Read [tick arrays](https://docs.grapl.io/concepts/tick-arrays). ## A hook or permission check rejects Match the pool's hook program, ABI version, profile, enabled phases, and immutable fee caps. Supply every extra account with the corresponding descriptor in exact order. A writable outer account cannot exceed the pool's per-phase access ceiling. Use [hook troubleshooting](https://docs.grapl.io/hooks/troubleshooting). ## A liquidity deposit needs both assets A position containing the current price needs both reserve assets. A range entirely to one side can accept one asset. If you want a conversion plus deposit, use a [zap](https://docs.grapl.io/guides/zaps) with an explicit split and limits; a liquidity change does not silently perform a swap. ## A reward was logged but no balance changed Check transaction success before interpreting callback logs. A later failure can roll back reward issuance and reserve settlement. Then inspect the expected reward mint, recipient token account, issuance counter, and sponsor funding. See [reward credits](https://docs.grapl.io/hooks/rewards). ## An operation is absent from the interface Consult [protocol capabilities](https://docs.grapl.io/start/status), [instructions](https://docs.grapl.io/reference/instructions), and [limitations](https://docs.grapl.io/security/limitations). Unsupported behavior must reject explicitly; do not infer a callable instruction from a concept heading. # Enter and exit liquidity with one asset Compose a swap with a position change atomically, while keeping budgets, fees, and leftovers explicit. Source: https://docs.grapl.io/guides/zaps A zap combines an ordinary swap with an ordinary liquidity operation. It provides a single transaction outcome while retaining both operations' fees, price impact, account validation, and lifecycle hooks. It is useful when an in-range or full-range position requires two assets but the user starts with one. ## Zap in The caller opens or selects an owned CLMM position and supplies `ZapInArgs`: | Field | Purpose | | ------------------- | -------------------------------------------------------------------------------------- | | `swap` | ExactIn swap parameters, including the chosen conversion amount and minimum net output | | `maximum_input` | Total single-asset budget for conversion plus deposit | | `minimum_liquidity` | Positive lower bound on liquidity received | | `liquidity_hook` | Separate payload/account descriptors for add-liquidity callbacks | | `swap_accounts_len` | Boundary between swap remaining accounts and liquidity remaining accounts | The core first swaps the caller-selected portion. It then computes affordable liquidity at the **post-swap price**, using the remaining input budget and only the actual net output produced by this operation. For a 1,000 A budget, a caller might request a 480 A swap. The deposit can use at most the remaining A and the B just obtained. Preexisting B in the wallet does not silently increase the zap budget. Unused amounts and rounding leftovers remain in the wallet. The correct conversion amount depends on price, range, liquidity, fees, and price impact. **There is no optimal-split planner in this instruction.** Simulate and quote the composed operation rather than assuming that swapping 50% is optimal. ## Zap out `zap_out` removes a negative liquidity delta, then swaps the withdrawn principal of one asset into the other. With `a_to_b = true`, the final desired asset is B: ```text final B = B principal withdrawn + net B received from swapping withdrawn A ``` `minimum_output` protects this aggregate output. Removal limits and the swap price limit are also enforced. If the conversion cannot consume all withdrawn input, the zap fails instead of claiming a one-asset exit with hidden leftovers. The zap does not spend preexisting wallet balances or collect unclaimed LP fees. [Collect position fees](https://docs.grapl.io/concepts/lp-lifecycle) separately. Removing the pool's last usable liquidity can leave no liquidity for the conversion, so a full exit through the same market is not universally possible. ## All relevant callbacks still run ```mermaid flowchart LR BeforeSwap --> Swap --> AfterSwap AfterSwap --> BeforeAddLiquidity --> AddLiquidity --> AfterAddLiquidity ``` A zap in runs swap callbacks when a nonzero conversion is requested, then add-liquidity callbacks. A zap out runs remove-liquidity callbacks and then swap callbacks when there is input to convert. Supply account descriptors separately for each leg; the hook does not gain arbitrary access to the wallet because the operations were combined. A denied callback, failed minimum, token error, or CU exhaustion rolls back every leg atomically. A successful first leg in a simulation is not enough to claim success for the whole zap. ## Current boundaries Zaps target CLMM mode, including graduated curves. They are local program composition; they are not automatic compounding, a strategy vault, or a multi-venue route optimizer. Full-range liquidity still economically needs both assets at an interior price—the zap obtains the second asset through a swap. **Source:** `programs/grapl-amm/src/instructions/zaps/`. See [status](https://docs.grapl.io/start/status) for verification and [instructions](https://docs.grapl.io/reference/instructions) for the entrypoint surface. # Gate swaps with a Merkle proof Bind membership to the authenticated actor, exact pool domain, and expiry. Source: https://docs.grapl.io/hooks/allowlist Prerequisites: [callback authentication](https://docs.grapl.io/hooks/authentication), [policy setup](https://docs.grapl.io/hooks/attach), and [the callback contract](https://docs.grapl.io/hooks/first-hook). Use `examples/hooks/allowlist`; pure hashing and verification live in `examples/hooks/shared/src/{allowlist,hashing}.rs`. **Example hook: allowlist.** This is an optional program built for this repository, not native or default policy in the protocol/library. The core does not depend on Merkle membership or `grapl-hook-policies`. A pool or launch does not require an allowlist. ## 1. Choose the domain and root The immutable `AllowlistPolicy` stores `chain_domain`, one pool mint, `root`, and `root_version`. Setup checks that the mint belongs to the pair. The example requires PolicyOnly and exactly BeforeSwap, so it gates both swap directions and does not lock LP exits. Membership binds chain namespace, core protocol, pool, mint, root version, trader, and expiry. Changing any field changes the leaf. The runtime gets `trader` from authenticated `HookInvocation.actor`; it never accepts an identity override in the proof payload. The chain namespace is a creator-selected 32-byte value. Choosing the cluster genesis hash is a convention; the program does not query or verify the actual cluster genesis hash. Document and consistently apply the selected domain. The exact SHA-256 tags, field order, little-endian integers, and sorted-node rule are maintained in [policy mechanics](https://docs.grapl.io/reference/policies). Do not hash JSON, decimal strings, base58 text, or an extra array-length prefix in their place. ## 2. Construct the leaf and proof Encode the configured domain, root version, authenticated trader, and expiry in the exact byte order specified by [policy mechanics](https://docs.grapl.io/reference/policies). Hash the domain-separated leaf, then combine it with each sibling using the sorted-node rule. The proof contains sibling hashes, not a list of wallet addresses. Each sibling adds 32 bytes. Validate your client encoding against the canonical `examples/hooks/shared/fixtures/merkle-v1.json` vector before using it to authorize a swap. ## 3. Encode membership for a swap The runtime helper `membership_payload(expires_at, &proof)` in `examples/hooks/shared/src/runtime/evaluate.rs` encodes only version, expiry, sibling count, and siblings. The payload contains no trader field. The maximum is 12 siblings and 394 bytes, within the core's 512-byte bound. An empty proof is valid for a single-leaf tree. Pass the bytes as `SwapArgs.hook_payload` and the configured policy as the first read-only remaining account. Derive the root using the intended pool and authenticated actor domain. At execution, the hook reconstructs the leaf with the authenticated actor and configured domain, folds the proof from leaf to root, checks the root, and rejects when `now >= expires_at`. Extra bytes, an unknown payload version, or an inconsistent sibling count reject. ## Membership is reusable authorization The proof can be reused by the same actor in the same domain before expiry. There is no consumed nonce, one-buy limit, quota, or per-wallet spend tracking. This example declares its policy read-only and does not request participant state. The generic framework can support stateful rules, but this example does not implement them. Replay tests reject changes of domain/actor; they do not turn valid membership into a single-use ticket. There is no root-rotation or individual revocation instruction in the example. Expiration is the implemented time boundary. A copied proof also does not confer the actor's signing authority, and the policy applies only to this pool. ## Validate identity and expiry Reject an altered domain, another actor, a bad root, an expired grant, or an inconsistent proof length. A copied proof cannot supply the actor’s signing privilege. Continue with [hook validation](https://docs.grapl.io/hooks/testing) and [router identity](https://docs.grapl.io/hooks/metis). # Bind and configure a hook Connect the pool to one program and create the policy at the correct point in its lifecycle. Source: https://docs.grapl.io/hooks/attach Start with [your first hook](https://docs.grapl.io/hooks/first-hook) and choose its [permissions](https://docs.grapl.io/hooks/permissions). The binding and setup sequence determines when the policy can safely run. ## 1. Select the program and capabilities The `InitializePool` account list includes `hook_program` and the core-derived `hook_authority`. `InitializePoolArgs.configuration.hook_capabilities` selects callbacks, profile, and bounded account permissions. The target is stored in `Pool.hook_program`; it is not a field embedded in a token mint. You can create a pool without any hook: use bitmap `0`, `PolicyOnly`, and the default public-key/System Program sentinel. Token-account ownership, user limits, and accounting checks still apply. Those core safety checks are separate from optional example policies such as allowlists. | Example hook | Capabilities | Configuration account | | ------------------- | -------------------------------------------------------------------------- | ------------------------------------------- | | Dynamic fee | BeforeSwap, DynamicFee | Required | | Trading window | BeforeSwap, PolicyOnly | Required | | Allowlist | BeforeSwap, PolicyOnly | Required | | Lifecycle probe | All ten, PolicyOnly | None | | Decaying launch fee | BeforeSwap, DynamicFee | Required | | Reward credits | BeforeSwap and AfterSwap, PolicyOnly with explicit writes/external program | Required, with issuer and participant state | Resolve the program ID from your deployment configuration. These docs do not publish hook deployment addresses. For the lifecycle probe, the setup fragment is: ```rust use grapl_amm::hooks::abi::{HookCapabilities, HookProfile, CALLBACK_MASK}; let capabilities = HookCapabilities { bits: CALLBACK_MASK, profile: HookProfile::PolicyOnly, ..HookCapabilities::default() }; assert!(capabilities.is_supported()); ``` Use the matching IDL to construct `InitializePoolArgs`, derive PDAs, and order initialization accounts. The lifecycle example needs no policy extras. ## 2. Solve initialization bootstrap explicitly The three economic policies enable only BeforeSwap. Pool initialization therefore does not call them. Once the pool exists, its creator can configure the policy before the first swap. A swap attempted without the required valid policy fails; it does not fall back to a permissive path. The lifecycle probe can run during initialization because it needs no policy account. A custom hook that demands an already configured external policy during BeforeInitialize cannot use the existing post-pool setup sequence unchanged. Design that bootstrap before enabling the initialization bit. Both initialization callbacks currently see the assigned candidate pool with `entered = true`; the setup instruction instead requires an existing pool with `entered = false`. See [the exact timing](https://docs.grapl.io/hooks/lifecycle). ## 3. Initialize an economic policy once The creator sends `configure_instruction(&HookPolicy)` to the selected example program. That function encodes instruction bytes; the caller still constructs the Solana instruction and signs it. | Setup account order | Required role | | ------------------- | ------------------------------------------------------------- | | Creator | Pool creator, signer, writable rent payer | | Pool | Read-only core pool bound to this hook | | Policy | Writable PDA `[b"policy", pool, &[0]]` under the hook program | | System Program | Allocation, funding, and assignment | Read `examples/hooks/shared/src/runtime/initialize.rs` and the fixture's `configuration_instruction` builder in `tests/svm_support/transactions.rs`. Setup validates the program binding and policy shape. A prefunded system PDA is supported; an already initialized policy rejects. There is no example policy-update, root-rotation, or policy-close instruction. Do not document a second configure call as an update. ## 4. Supply descriptors with the operation accounts For the fee/window/allowlist examples, the configured policy is the first remaining account of the **core** instruction. Supply a matching `HookAccountAccess` with `class: HookAccountClass::PolicyPda { index: 0 }` and `writable_phases: 0` in `SwapArgs.hook_accounts`. The core validates it and forwards it after the callback authority and pool. Every remaining account needs a descriptor in exact order, including when a selected callback is disabled. The no-hook and lifecycle-probe paths use an empty vector and no extras. A stateful hook explicitly requests HookState or ExternalState writes within its configured permission ceiling; setup writability never implicitly grants callback access. Prepare required hook state before swapping. The reward example supports either participant preparation in advance or opt-in atomic preparation funded by the hook's sponsor PDA. The callback receives no actor signing privilege. See [reward credits](https://docs.grapl.io/hooks/rewards) for the provisioning flows; these accounts are not core pool defaults. Keep policy extras out of the lifecycle probe, which expects only two callback accounts. Fee collection also rejects unexpected remaining accounts and does not dispatch a hook. ## Binding and upgrade governance are different The pool has no instruction to replace its hook binding. That fixed address does not make the code immutable: a program deployed with an upgrade authority may change at the same address. Pool/core upgrades and hook upgrades are separate governance decisions. Document their authorities and consequences before deployment. Removal hooks can reject withdrawals. There is no implemented emergency escape or policy-bypass control. The current three BeforeSwap examples do not enable removal callbacks, but a broader hook can. Make that distinction visible to LPs. Unlike [Uniswap's address-encoded hook flags](https://developers.uniswap.org/docs/protocols/v4/guides/hooks/hook-deployment), grapl stores capabilities in pool configuration. No CREATE2 address mining applies. Continue with a concrete [fee policy](https://docs.grapl.io/hooks/swap-fees). # Authenticate callbacks and actors Trust the core-signed context without forwarding user or vault signing privileges. Source: https://docs.grapl.io/hooks/authentication Prerequisite: understand [permissions](https://docs.grapl.io/hooks/permissions). Use this page before implementing an allowlist or any policy that depends on who is trading. ## Follow the trust chain ```mermaid sequenceDiagram participant A as Actor authority participant C as grapl core participant H as Hook program A->>C: Signed request C->>C: Validate actor
and accounts C->>C: Mark entered
and serialize pool C->>H: CPI + core PDA signer H->>H: Verify origin
and pool binding H->>H: Evaluate policy H-->>C: Tagged HookReply C->>C: Validate reply Note over C,H: Explicit state permissions
No vault or actor signer ``` For market operations, `MarketAccounts.actor` is a required signer. Both actor token accounts must have the expected mint and that actor as their token authority. Liquidity changes also require the position's owner to equal the actor. Initialization uses the creator signer instead. The core copies this identity into `HookInvocation.actor`. A hook trusts that field only after authenticating the callback. Proof bytes, a supplied public key, the transaction fee payer, or a router's name do not replace this chain. ## Inspect the actual CPI accounts | Order | Account | Privileges inside the callback | | -------- | --------------------------------------------------------------- | -------------------------------------------------------- | | 0 | Core PDA `[b"hook", pool]` | Read-only signer | | 1 | Core-owned pool | Read-only, not a signer | | 2 onward | Declared policy, hook state, external state or program accounts | Per-phase read/write permissions; never upstream signers | The callback PDA is derived under the **core program**, separately from the pool's vault authority. The runtime grants its signer privilege through `invoke_signed`. See [Solana CPI with PDA signers](https://solana.com/docs/core/cpi/cpi-with-pda). Every extra has a matching `HookAccountAccess` descriptor in the core instruction. The core validates count, class, owner, program declarations, duplicates, protected aliases and per-phase privileges. It rebuilds CPI metas rather than forwarding transaction writability. Default permissions allow four read-only hook-owned extras; explicitly expanded permissions remain bounded at 16 extras and eight writes per callback. For `PolicyPda { index }`, index zero is the byte `[0]`, not ASCII `"0"`. Hook-owned state still needs hook-side PDA/discriminator/relationship validation. External program ownership alone does not bind a token account to the intended reward mint or actor. The fee/window/allowlist examples expect one policy; the lifecycle probe expects no extras. Core-owned accounts and protected pool, actor, vault, mint, position, tick-book, authority and settlement-token identities cannot be supplied as extras. A hook may sign its own PDA in a nested CPI, but that is separate from borrowing the actor or core vault authority. ## Reuse the authenticated BaseHook entrypoint The [custom hook template](https://docs.grapl.io/hooks/first-hook) calls `grapl_amm::hooks::base::process_hook` from its entrypoint. The dispatcher authenticates the pool and core signer before constructing `HookContext` and calling your implementation. Generic authentication lives in `programs/grapl-amm/src/hooks/base/authentication.rs`. Existing example policy adapters delegate to those helpers from `examples/hooks/shared/src/runtime/authentication.rs`; policy-specific PDA checks and their existing error-code mapping remain in the example layer. The base dispatcher additionally checks operation sequence, mint pair and the exact capability contract before dispatch. The lifecycle probe also uses `BaseHook`, so its rollback tests exercise the reusable entrypoint. See [BaseHook reference](https://docs.grapl.io/hooks/base-hook) for the checks and [hook validation](https://docs.grapl.io/hooks/testing) for the executable test harness. ## Coming from Uniswap v4 In v4, `msg.sender` inside a hook is the PoolManager, and the callback's `sender` is commonly a router. Its [identity guide](https://developers.uniswap.org/docs/protocols/v4/guides/hooks/accessing-msg.sender) discusses trusted routers. grapl uses a runtime-validated account signer plus the core PDA callback. There is no EVM `msg.sender` or `tx.origin` to read. An upstream program might sign with its own PDA. That does not establish the identity of a human behind that PDA. A future routed identity model must define the end user, token authority, destination, and proof forwarding together; the current adapter rejects a distinct user and token-transfer authority. ## Validate the trust boundary Check direct calls, wrong authority, pool substitution, invalid privileges, and wrong policy state. Validate per-phase access and protected-account rejection. [Hook validation](https://docs.grapl.io/hooks/testing) connects these checks with reserve settlement and rollback. # BaseHook reference The Rust interface, authenticated context, dispatch rules and safe defaults for custom grapl hooks. Source: https://docs.grapl.io/hooks/base-hook `grapl_amm::hooks::base` is the reusable authoring layer for executable hook programs. It uses the core lifecycle ABI and contains no application policy. The [template tutorial](https://docs.grapl.io/hooks/first-hook) shows how to use it. ## Public interface | Item | Contract | | -------------------------- | -------------------------------------------------------------------------------------- | | `BaseHook` | Trait with capabilities, custom account validation and ten callback methods | | `HookContext` | Authenticated invocation, decoded pool and extra account views | | `process_hook` | Validate, dispatch, validate the reply and write canonical return data | | `prepare_reply` | Run the same validation and return encoded reply bytes without the return-data syscall | | `authenticated_pool` | Check core ownership, pool decoding/version and configured hook binding | | `authenticated_invocation` | Additionally check callback authority, privileges, pool address and entered state | Use `process_hook` for callback entrypoints. The lower-level authentication helpers do not perform every dispatch check, including capability equality, sequence and mint matching. They are provided for compatibility with existing adapters. ## BaseHook methods | Method | Default behavior | | ---------------------------------- | --------------------------------------------------------------- | | `capabilities()` | Required; return the exact callback/profile/permission contract | | `validate_accounts(context)` | Reject extra accounts and payload bytes | | `before_initialize(context)` | Reject unless implemented | | `after_initialize(context)` | Reject unless implemented | | `before_add_liquidity(context)` | Reject unless implemented | | `after_add_liquidity(context)` | Reject unless implemented | | `before_remove_liquidity(context)` | Reject unless implemented | | `after_remove_liquidity(context)` | Reject unless implemented | | `before_swap(context)` | Reject unless implemented | | `after_swap(context)` | Reject unless implemented | | `before_donate(context)` | Reject unless implemented | | `after_donate(context)` | Reject unless implemented | A callback returns `Result`. `context.acknowledge()` returns the current ABI version and phase, `allow = true`, and no trading-fee or output-fee override. It does not execute transfers or emit return data itself. A `DynamicFee` BeforeSwap callback must set its required fee before returning. The copyable template deliberately implements all methods as acknowledgements. The trait's default rejection prevents an enabled but forgotten callback from silently allowing an operation. ## HookContext fields | Field | Meaning | | ---------------- | --------------------------------------------------------------------------------------------------------- | | `invocation` | Phase, pool, mints, actor, sequence, clock, snapshots, operation details, account descriptors and payload | | `pool` | Decoded core-owned pool authenticated against this hook program | | `extra_accounts` | Declared callback accounts after the authority and pool | The dispatcher constructs this context after authentication. Its non-exhaustive structure prevents consumers from constructing one through the public Rust API. These are borrowed views; persist any required state in validated accounts owned by your program. `invocation.actor` is attested by the core, not an independently forwarded wallet signature. A router PDA may itself be the actor. Read [identity and authentication](https://docs.grapl.io/hooks/authentication) before implementing actor-dependent policy. ## Dispatch checks Before running custom code, the base checks: 1. The executing program ID equals the expected hook ID. 2. The invocation's tag, ABI version, encoding and phase-specific context are valid. 3. The authority is the core-derived `[b"hook", pool]` PDA and is a read-only signer; the pool is read-only and not a signer. 4. The pool is a non-executable account owned by the core, with a valid discriminator/version, bound to this hook, and marked as entered. 5. The pool address, mint pair and operation sequence match the authenticated state. 6. Extra account count matches the descriptors, the phase is enabled, and capabilities exactly match the pool's bitmap, profile and permissions. 7. Your `validate_accounts` implementation accepts the custom state and payload. After dispatch, the base validates the reply against the phase, profile, fee cap and output-fee permissions, then encodes it. `process_hook` writes it to Solana return data. The core independently validates the response and its originating program. ## Responsibilities that remain with the author Validate policy-specific PDAs, account relationships, external program identities and all arithmetic. The core's generic account classification does not prove that an account is the reward recipient or configuration your application intended. Configure setup and upgrade authorities deliberately. This interface does not deploy your program, create policy state, implement administrative instructions, or establish a quote adapter. It also does not make pool hooks run on arbitrary token transfers outside the pool. The lifecycle probe uses the same base and rejects selected phases to test rollback. Test custom side effects and economic invariants in addition to that generic harness. See [validation](https://docs.grapl.io/hooks/testing) and [ABI reference](https://docs.grapl.io/reference/hook-abi). # Compose your own policy Combine timing, membership, fees, and rewards inside one hook without adding rules to the AMM. Source: https://docs.grapl.io/hooks/compose-policy The core supplies authenticated facts and bounded execution tools. A hook author decides what those facts mean. A pool can remain permissionless, require a proof, apply a time-dependent fee, or award a separate reward asset. None of those policies is a default pool restriction. ## One hook, several decisions Suppose a market should open at a given time, accept members during an early window, decay its input fee, and issue credits after purchases. Implement those decisions in one hook program with its own configuration. | Phase | Read from the core | Developer-defined decision | | ------------------ | -------------------------------------------------------------- | -------------------------------------------------------- | | BeforeInitialize | Pool, mints, creator, configuration snapshot | Accept the intended market and policy configuration | | BeforeSwap | Actor, direction, mode, request limits, frozen Clock | Enforce the time window, verify membership, choose a fee | | AfterSwap | Actual consumed input, net output, output fee, resulting state | Award credits using the amount actually settled | | BeforeAddLiquidity | Position and signed liquidity intent, pool mode | Decide whether this liquidity operation is permitted | | AfterAddLiquidity | Resulting liquidity and principal amounts | Update permitted hook-owned accounting | This is a policy design, not a new built-in composite instruction. Use the separate [examples](https://docs.grapl.io/hooks/overview) as building blocks and write tests for the combined behavior. BeforeSwap authenticates context, checks eligibility and chooses permitted fees. Core settlement prices the trade and enforces limits. AfterSwap reads settled amounts and applies declared effects. Hook permissions are bounded by the pool, operation and phase. ## Order the checks deliberately Reject invalid context and accounts before running an expensive proof or external CPI. Check cheap eligibility rules before preparing sponsored accounts. Calculate bounded replies only after the operation is eligible. Mint rewards only from authenticated settled amounts. ```text authenticate core callback and pool binding → validate hook state and declared external accounts → check time and membership rules → choose an allowed fee → let the core settle → calculate reward from actual output ``` `AfterSwap` remains inside the transaction. If reward issuance is mandatory and fails, the trade rolls back. If the product intends optional rewards, define and test an explicit policy for when it may return success without issuance; never silently change that promise in an adapter. ## Use the right identity The core authenticates the swap actor and token-account relationships. The hook receives that context without receiving the actor's wallet signature as a general-purpose CPI capability. [Actor identity](https://docs.grapl.io/hooks/authentication) is not proof that an account is the original human at the beginning of every routed transaction. An allowlist proof proves membership against the configured root. The whole address list is not stored in each proof. Bind the leaf to the intended actor and policy domain; do not accept a caller-selected address merely because its proof is valid. ## Scope each permission Declare only the accounts and writable phases the policy needs. A reward issuer may need writable mint and recipient state after settlement; a timing check can remain read-only. Account descriptors select roles within the pool's immutable permission ceiling. They do not grant reserve custody or arbitrary signing power. See [permissions](https://docs.grapl.io/hooks/permissions) for `ActorReadOnly`, `CreatableHookState`, and `PoolMintReadOnly`; [rewards](https://docs.grapl.io/hooks/rewards) demonstrates sponsored preparation and external token-program calls. ## Quote the same policy For a public Metis quote, the hook adapter needs enough public state to produce the same financial result as execution. A Clock-based decay can use a coherent snapshot; a buyer-specific proof cannot be guessed before the buyer is known. Keep quote logic alongside the example policy, inject it into the generic adapter, and validate the policy again on-chain. A supported quote adapter cannot bypass an on-chain denial. Read [Metis compatibility](https://docs.grapl.io/hooks/metis) before promising a custom hook can participate in every route. ## Test the combination Cover opening/closing timestamps, invalid proofs, both swap directions and modes, fee caps, insufficient sponsorship, capped rewards, account substitution, and rollback after a later callback fails. For a curve, include graduation with `pool_mode` changing from 1 to 0. For zaps, verify both callback sequences and the final user limit. Use [hook validation](https://docs.grapl.io/hooks/testing) to check combined effects and rollback. Each standalone rule must also remain correct when composed. # Read the callback context Use authenticated identity, a consistent Clock, typed amounts, and bounded account access. Source: https://docs.grapl.io/hooks/context Every callback receives a versioned `HookInvocation`. Decode it with the current ABI and [authenticate the core caller](https://docs.grapl.io/hooks/authentication) before using its fields. The SDK's [hook resolver](https://docs.grapl.io/developers/hooks) prepares accounts and payload; it does not establish identity or replace on-chain checks. ## Identity and execution context | Field | Meaning | Integration rule | | ------------------------------------ | ----------------------------------------- | --------------------------------------------------------------------- | | `pool`, `mint_a`, `mint_b` | Core-selected pool and reserve pair | Validate the bound pool, never a caller's substitute | | `actor` | Authenticated operation authority | It can be a router PDA; do not assume an end-user wallet | | `phase` | One of the ten lifecycle callbacks | Match the reply to the current phase | | `operation_sequence` | Pool operation counter | It is not randomness, a signature, or a cross-transaction reservation | | `clock.slot`, `clock.unix_timestamp` | Runtime Clock captured for this operation | Use the same captured time for before/after policy evaluation | | `before`, `after` | Typed pool snapshots; `after` is optional | Use the snapshot appropriate to the phase | | `payload` | Caller-supplied bytes, at most 512 | Validate shape and proofs; bytes alone grant no authority | | `account_access` | Descriptors for declared extras | Validate exact roles as well as ownership | The Clock is read once for the operation and shared by its callback pair. A failed transaction rolls back the sequence and other state changes. The core serializes the entered pool before CPI; authentication checks this invocation state. ## Requested amounts and actual settlement `HookOperationDetails` distinguishes initialization, swaps, liquidity changes, and donations. All financial quantities are integers in native units. Pool snapshots include signed ticks, Q64 square-root price, active liquidity, and fee-growth values. For a swap, `HookSwapDetails` includes direction, mode, reserve mints, input/output accounts, input authority, output owner, request bounds, and price limit. A before callback evaluates the request. An after callback can inspect `settled`: | Settled field | Meaning | | ----------------------- | -------------------------------------------- | | `consumed_input` | Actual gross input consumed | | `net_input` | Input used for pricing after the trading fee | | `actual_output` | Net output delivered after any output fee | | `output_fee_amount` | Separately charged output fee | | `unspent_input` | Request input left unspent | | `fee_amount`, `fee_ppm` | Actual trading fee and effective rate | Rewards based on received output must use `actual_output`. A requested amount is not a receipt. Read [lifecycle timing](https://docs.grapl.io/hooks/lifecycle) for ExactOut field mapping, partial fills, liquidity deltas, and donation context. Protocol retention partitions the existing trading fee; it does not increase `consumed_input` or introduce another hook callback. ## Extra accounts and nested CPI The core passes its hook-authority PDA as a read-only signer, the pool read-only, and validated extras with phase-specific privileges. The actor's signature and the vault authority are not forwarded. Pool permissions and each operation's descriptors must both permit a write. Declare external programs and state explicitly. Check exact PDAs, owners, discriminators, mints, authorities, and pool/actor relationships inside the hook. A hook may sign for its own PDA when calling another program; this cannot authorize spending the AMM vaults. Calling the core back from the hook is not a supported settlement path. Continue with [account permissions](https://docs.grapl.io/hooks/permissions), [policy composition](https://docs.grapl.io/hooks/compose-policy), and the exact [Hook ABI](https://docs.grapl.io/reference/hook-abi). # Decay a launch fee from 99% Configure an optional linear fee schedule with an explicit start, duration, and ending rate. Source: https://docs.grapl.io/hooks/decaying-fee **Example hook: decaying fee.** `examples/hooks/decaying-fee` presents a 99%-to-floor preset of the [existing linear dynamic-fee curve](https://docs.grapl.io/hooks/swap-fees). It starts at a configured rate, decreases it with elapsed time, and keeps the selected ending rate afterward. Its convenience constructor starts at `990000` ppm, or **99%**. The schedule lives in the external hook; the core supplies authenticated Clock and enforces the pool's fee cap. Prerequisites: [dynamic-fee replies](https://docs.grapl.io/hooks/swap-fees), [pool binding](https://docs.grapl.io/hooks/attach), and [callback permissions](https://docs.grapl.io/hooks/permissions). ## 1. Choose the schedule and pool cap `DecayingFeeSchedule` contains: | Field | Type | Meaning | | ------------------ | ----- | ------------------------------------------------ | | `start_fee_ppm` | `u32` | Initial swap fee, at most 990,000 ppm | | `end_fee_ppm` | `u32` | Permanent floor, no greater than the initial fee | | `starts_at` | `i64` | Unix timestamp at which decay begins | | `duration_seconds` | `u64` | Positive duration of the decay | For example, start at 99% and reach 0.30% after 100 seconds: ```rust use grapl_hook_decaying_fee::DecayingFeeSchedule; let schedule = DecayingFeeSchedule::from_99_percent(3_000, 100, 100) .expect("valid positive duration and ending fee"); assert_eq!(schedule.fee_at(100, 990_000).unwrap(), 990_000); assert_eq!(schedule.fee_at(150, 990_000).unwrap(), 496_500); assert_eq!(schedule.fee_at(200, 990_000).unwrap(), 3_000); ``` The pool must have a cap at least as high as the initial fee. The core's maximum permitted configured cap is 990,000 ppm. Choosing this example does not raise a lower pool cap or change any default fee. A zero duration, rising schedule, initial rate above the supported ceiling, or schedule above the actual pool cap rejects. A 99% fee is charged on consumed input under the AMM's fee calculation; it is not a guarantee about received output or price impact. Integer token rounding and user limits still apply. See [fee accounting](https://docs.grapl.io/concepts/fees). ## 2. Follow exact time and rounding semantics The evaluator uses `call.clock.unix_timestamp`, supplied by the authenticated core. Its integer calculation is: ```text elapsed = clamp(now − starts_at, 0, duration_seconds) fee_ppm = start_fee_ppm − floor((start_fee_ppm − end_fee_ppm) × elapsed / duration_seconds) ``` Subtracting a rounded-down discount keeps the result at or just above the ideal fractional fee, with less than one ppm of rounding difference. The result is monotone, stays within the endpoints, and equals the ending rate when the duration has elapsed. Arithmetic uses checked integer widths. For the example above: | Runtime timestamp | Fee | | ----------------- | --------------------- | | Before or at 100 | 990,000 ppm = 99% | | 125 | 743,250 ppm = 74.325% | | 150 | 496,500 ppm = 49.65% | | 175 | 249,750 ppm = 24.975% | | At or after 200 | 3,000 ppm = 0.30% | Before the start, the hook allows an otherwise valid swap at the initial fee. There is **no implicit trading-window gate**. A launch that must prohibit early trading needs that additional rule composed explicitly by its hook author. Elapsed time determines this schedule; trade count and volume do not accelerate it. ## 3. Bind and configure the optional program Select exactly BeforeSwap and the DynamicFee profile. The example uses one read-only `PolicyPda { index: 0 }`, derived as `[b"policy", pool, &[0]]` under its own program. Pass the matching descriptor with the core instruction; an empty payload is required. `configure_decaying_fee(schedule)` produces the immutable setup bytes. The creator configures the policy after the pool exists. Setup accounts are creator (writable signer), pool (read-only), policy PDA (writable), and System Program (read-only), in that order. The state stores its version, pool binding, and schedule; decoding rejects wrong tags/versions, wrong length, and trailing bytes. Consult `src/runtime/` and the fixture builders for executable setup and callback account checks. The callback authenticates core origin, pool binding, enabled profile/phase, operation sequence, mints, and account descriptors before returning `Some(fee_ppm)`. It never transfers tokens or changes core accounting itself. Subsequent core settlement still enforces output and price limits. The two examples expose the same linear calculation and upward fee-rate rounding. Their configuration and program contracts differ: | Example | Configuration | Program contract | | ------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | [Dynamic fee](https://docs.grapl.io/hooks/swap-fees) | `peak_ppm`, `floor_ppm`, `starts_at`, `ends_at` | General schedule in the shared example policy format | | Decaying launch fee | `start_fee_ppm`, `end_fee_ppm`, `starts_at`, `duration_seconds`; 99% convenience constructor | Dedicated immutable state, program binding, and adapter account plan | The pure fee calculation belongs in `examples/hooks/shared/src/fee.rs`; the preset's wrapper and account contract belong in `examples/hooks/decaying-fee`. Sharing that calculation prevents numerical drift. These are alternative external programs, and the core does not stack them automatically. ## 4. Preserve the schedule in quotes A compatible Metis adapter must reuse the exact schedule and rounding at the same account/Clock snapshot. It must preserve the returned fee in native execution and reject unsupported phase/profile combinations. Public routing remains a separate integration step. ## Related Solana precedent Time-based launch fee decay already exists in Solana products. Meteora's [Fee Scheduler](https://launch.meteora.ag/#:~:text=Fee%20Scheduler) describes initial fees up to 99%, linear or exponential decay, and an ending base fee. The grapl example above implements its own linear external-hook contract; it does not implement Meteora's scheduler or an exponential curve. # Hook examples Choose a source-backed starting point for fees, eligibility, rewards, or a composed policy. Source: https://docs.grapl.io/hooks/examples These programs live under `examples/hooks/` in the protocol repository. They are optional, independently deployed examples. No allowlist, window, reward system, or launch fee is a built-in restriction on an unhooked pool. | Start with | Source directory | What it demonstrates | Next guide | | --------------- | ---------------- | -------------------------------------------------------------------- | ---------------------------------------- | | Lifecycle probe | `lifecycle/` | Authenticate and acknowledge all ten phases; reject a chosen phase | [First hook](https://docs.grapl.io/hooks/first-hook) | | Dynamic fee | `dynamic-fee/` | Choose a Clock-based trading fee within the pool cap | [Dynamic fees](https://docs.grapl.io/hooks/swap-fees) | | Launch decay | `decaying-fee/` | Reduce a configured 99% opening fee toward a floor over a duration | [Decaying fees](https://docs.grapl.io/hooks/decaying-fee) | | Trading window | `timelock/` | Include the opening instant and exclude the closing instant | [Trading windows](https://docs.grapl.io/hooks/trading-window) | | Membership | `allowlist/` | Verify a Merkle proof bound to the authenticated actor | [Allowlist](https://docs.grapl.io/hooks/allowlist) | | Rewards | `rewards/` | Issue bounded, non-transferable credits from eligible settled output | [Rewards](https://docs.grapl.io/hooks/rewards) | | Shared support | `shared/` | Authentication, policy account setup, and client encoding | [Authentication](https://docs.grapl.io/hooks/authentication) | ## Pick the smallest suitable program Use the lifecycle probe to learn the callback contract. Add one policy at a time, with explicit success and rejection cases. For fees, configure both a compatible hook profile and the pool's immutable fee cap. The 99% ceiling does not change another pool's fee or override a lower configured cap. For membership and windows, evaluate the authenticated actor and runtime Clock. For rewards, wait for AfterSwap and use settled output. Any external mint or state write needs the declared program and account permissions; rewards do not borrow the trader's signature or vault authority. ## Combine policies deliberately A pool binds one hook program. Selecting several example directories does not stack them. A combined hook implements a single account contract and ordered evaluation: authenticate, check eligibility, choose permitted fees, settle through core, then apply bounded after-effects. Follow [compose a policy](https://docs.grapl.io/hooks/compose-policy) for a window plus membership, fee schedule, and rewards. [Liquidity and donation policies](https://docs.grapl.io/hooks/liquidity-donate) describe other callback choices; they are not a separate default program. ## Verify the complete integration Test boundary values, substituted accounts, missing quote context, and rollback after every enabled phase. A locally successful callback is not proof of quote/execution parity. Pair [validation](https://docs.grapl.io/hooks/testing) with [Metis integration](https://docs.grapl.io/hooks/metis), and use [troubleshooting](https://docs.grapl.io/hooks/troubleshooting) for common account and reply failures. # Build your first hook Start from the Rust BaseHook template, implement a callback, and verify authenticated CPI execution. Source: https://docs.grapl.io/hooks/first-hook Use the custom hook template to build a program that participates in a grapl pool's lifecycle. The starter exposes all ten callbacks and authenticates calls before your logic runs. It does not impose an allowlist, trading window, fee policy, or rewards policy. ## Prerequisites You need a protocol checkout containing `examples/hooks/template`, the repository's pinned Rust toolchain, and the Agave SBF build tools. The program uses Rust 2021 and Anchor 1.1.2. You do not need a wallet, API key, or public RPC for the included tests. `BaseHook` is currently a module of the `grapl-amm` crate. There is no separate published `grapl-base-hook` package to install. ## Start with the template Open these files in the protocol checkout: | File | Purpose | | ------------------------------------------- | ------------------------------------------------ | | `examples/hooks/template/src/lib.rs` | Program identity and authenticated entrypoint | | `examples/hooks/template/src/callbacks.rs` | Capability contract and callback implementations | | `examples/hooks/template/tests/host_cases/` | Authentication and dispatch tests | | `examples/hooks/template/tests/svm_cases/` | Executable SBF lifecycle tests | Keep the entrypoint routed through `process_hook`: ```rust use grapl_amm::hooks::base::process_hook; // Inside process_instruction, with your program's ID and CustomHook in scope: process_hook(program_id, &ID, accounts, encoded, &CustomHook) ``` This calls your implementation only after checking the invocation and pool binding. Rust uses a trait implementation here, rather than Solidity inheritance. ## Implement the callbacks you need The template's `CustomHook` implements `BaseHook`. Each callback receives a `HookContext` containing the authenticated invocation, decoded pool and declared extra accounts. For example, inside your `impl BaseHook for CustomHook`, replace `before_swap` with your rule: ```rust fn before_swap(&self, context: &HookContext<'_, '_>) -> Result { let swap = match &context.invocation.details { HookOperationDetails::Swap(swap) => swap, _ => return err!(CoreError::InvalidHookContext), }; require!(swap.requested_input <= 1_000_000, CoreError::InvalidHookContext); Ok(context.acknowledge()) } ``` Import `HookOperationDetails` from `grapl_amm::hooks::abi` and `CoreError` from `grapl_amm::errors`. This illustrative rule limits requested input to one million **base units**, not one million human-readable tokens. Choose bounds and error types appropriate to your own policy, and test both sides of the limit. Use `AfterSwap` and its settled amounts when calculating rewards. Requested input can differ from consumed input or the amount delivered to the recipient. See [callback context](https://docs.grapl.io/hooks/context). ## Declare the capability contract The starter's `template_capabilities()` enables all ten callbacks with the `PolicyOnly` profile and default account permissions. Each callback explicitly acknowledges the invocation. Disable the bits you do not use; a method inherited without an implementation rejects. When creating a pool, set its `hook_program` account to your executable program and its `configuration.hook_capabilities` to the exact contract returned by your hook's `capabilities()`. Bitmap, profile and permissions must match. The `BaseHook` dispatcher rejects mismatches. For dynamic trading fees, use the `DynamicFee` profile, enable `BeforeSwap`, and return `fee_ppm` within the pool's cap. Changing the reply alone does not enable that capability. See [dynamic fees](https://docs.grapl.io/hooks/swap-fees) and [attach a hook](https://docs.grapl.io/hooks/attach). ## Add state deliberately The default `validate_accounts` accepts only an empty payload and no extra accounts. Override it when your hook needs state or input. Validate each account's owner, seeds, discriminator, pool relationship and expected privileges; validate the payload's encoding and bounds. Declare extras through `HookAccountAccess` and the pool permission contract. The core validates and narrows their privileges before CPI. Your validation binds those accounts to your own policy. Neither the actor's signing authority nor core vault authority is forwarded to your hook. A single program can serve many pools with separate configuration PDAs. Callback handling does not create those PDAs for you. If you add setup instructions, authenticate those instructions separately and keep them distinct from the callback ABI. See [authentication](https://docs.grapl.io/hooks/authentication) and [permissions](https://docs.grapl.io/hooks/permissions). ## Run the validation gate From the protocol repository root: ```bash make test-hook-template ``` The gate checks formatting and Clippy, builds executable SBF programs, and runs: - Host tests for every callback, invalid account identities and privileges, context mismatches, missing implementations and invalid replies. - LiteSVM tests that invoke all ten template callbacks through the real core program. - A direct-call rejection test without a core PDA signature. - The lifecycle probe's deliberate before/after failures, including rollback after settlement. Expect every suite to report zero failures. Host tests can construct signer flags; the SBF tests exercise actual runtime CPI signing. Both are needed. ## Move into your own program The template's program ID is a fixture, not a deployed custom hook. Replace it with your program identity before deployment. If you move the source outside this repository, replace the relative `grapl-amm` dependency with a reviewed, pinned source dependency and port the tests' shared fixture imports. Adding a hook program does not automatically provide a quote adapter. SDK and Metis integrations must evaluate your policy with matching state, time and identity inputs. Start with [BaseHook reference](https://docs.grapl.io/hooks/base-hook), then [validate hook behavior](https://docs.grapl.io/hooks/testing) and [hook quote adapters](https://docs.grapl.io/hooks/metis). # Callback timing and context Use typed before/after state, authenticated identities, and actual settled amounts. Source: https://docs.grapl.io/hooks/lifecycle Use this catalog to choose where a rule belongs. Prerequisites: [hook model](https://docs.grapl.io/hooks/overview) and [position lifecycle](https://docs.grapl.io/concepts/lp-lifecycle). Types and serialization are listed in the [ABI reference](https://docs.grapl.io/reference/hook-abi). ## Follow one authenticated operation `HookInvocation` identifies phase, pool, mints, and actor. It carries `operation_sequence`, a frozen `HookClock`, `before` and optional `after` pool snapshots, typed `details`, ordered account-access descriptors, and application payload. The payload is copied between the phase pair; it is not trusted identity. The core reads Clock once. Both phases share that slot/timestamp and sequence. Sequence increments once per initialize, modify-liquidity, swap, or donate even when callbacks are disabled, and rolls back with a failed transaction. It is not random, a transaction signature, or a client reservation. Prepare state using the hook's stable identities rather than predicting the next pool sequence. Each snapshot contains price, signed current tick, active pool liquidity, and global fee growth for both assets. A before callback has `after: None`; an after callback preserves `before` and adds the resulting state. The serialized pool is read-only inside CPI and has `entered = true`. Position/tick/vault/actor settlement accounts remain protected; a key in the context does not grant access to its account bytes. ## Initialization — phases 0 and 1 | Phase | Context | Response | | ---------------------- | ------------------------------------------------------------------------------ | ------------------------------ | | `BeforeInitialize` (0) | Validated provisional pool; creator actor; configured price and zero liquidity | Allow without a fee, or reject | | `AfterInitialize` (1) | Same candidate after initialization work, with an after snapshot | Allow without a fee, or reject | Details are `HookOperationDetails::Initialize`. Anchor creates accounts before the instruction body, and the candidate is assigned before the callbacks. An after snapshot is still provisional until transaction success. Rejection rolls back pool, tick book, and vault creation. A policy configured only after the pool exists cannot already be required by its initialization callback; see [bootstrap](https://docs.grapl.io/hooks/attach). ## Liquidity — phases 2 through 5 | Phase | Snapshot timing | `HookLiquidityDetails` | | --------------------------- | ---------------------------- | --------------------------------------------------------- | | `BeforeAddLiquidity` (2) | Before accounting/deposit | Position, positive signed delta, calculated A/B principal | | `AfterAddLiquidity` (3) | After accounting/deposit | Same position, delta and amounts | | `BeforeRemoveLiquidity` (4) | Before accounting/withdrawal | Position, negative signed delta, calculated A/B principal | | `AfterRemoveLiquidity` (5) | After accounting/withdrawal | Same position, delta and amounts | The actor owns the position. Deposits round up; withdrawals round down. Amounts and user limits are checked before the callback, so an invalid request may fail without invoking the hook. The context's active pool liquidity is not position liquidity: an out-of-range position can change without changing active `L`. The position key does not include its range ticks or the user's `limit_a/b`. All four phases require a fee-free acknowledgement. [Liquidity and donations](https://docs.grapl.io/hooks/liquidity-donate) follows the accounting sequence. ## Swaps — phases 6 and 7 `HookSwapDetails` explicitly identifies A-to-B or B-to-A, input/output mints and token accounts, input authority, output owner, requested input, minimum output, and price limit. Do not infer direction by guessing which amount is positive. | Phase | Timing | `settled` | | ---------------- | ------------------------------------------------ | ---------------------------------------------------- | | `BeforeSwap` (6) | Before traversal and transfers | `None` | | `AfterSwap` (7) | After traversal and exact classic SPL settlement | Actual input, output, unspent input, fee amount/rate | The settled result distinguishes `consumed_input` including fees from `net_input` excluding fees. `actual_output` is the amount transferred to the output account. `requested_input = consumed_input + unspent_input`; partial execution can leave part of the maximum input unused. All token amounts are unsigned base units, while liquidity deltas are signed. An output-based reward uses `settled.actual_output` and `output_mint` in AfterSwap. Requested input is not an output receipt. A later callback, nested CPI, or transaction failure rolls back both settlement and hook effects. Actor is the authenticated local authority and may be a router PDA; `output_owner` is the local output-account owner, not proof of a route's final beneficiary. Only DynamicFee BeforeSwap returns `Some(fee_ppm)`. Every AfterSwap reply requires `None`, including a hook that writes reward state. See [dynamic fees](https://docs.grapl.io/hooks/swap-fees) and [reward credits](https://docs.grapl.io/hooks/rewards). ## Donations — phases 8 and 9 | Phase | Timing | `HookDonationDetails` | | ------------------ | ----------------------------------------- | ------------------------ | | `BeforeDonate` (8) | Before LP growth credit and token deposit | A/B contribution amounts | | `AfterDonate` (9) | After LP growth credit and token deposit | Same amounts | Donation requires active liquidity for a nonzero credit, updates LP fee growth, and leaves price and active `L` unchanged. Before/after snapshots expose the growth difference. Neither phase may override the fee. The authenticated actor is the donor. ## Operations without callbacks Opening/closing positions and collecting LP, beneficiary, or protocol fees have no dedicated phases. Collection does not synthesize a zero-liquidity callback, and there are no dedicated graduation phases. `graduate_curve` uses BeforeAddLiquidity with curve mode and AfterAddLiquidity with CLMM mode. Zaps compose the existing swap and liquidity callbacks for their respective legs. External reward claims or consumption belong to their own hook program; they are not new core instructions. ## Build against the callback contract The dispatch code lives in `programs/grapl-amm/src/hooks/`; operation orchestration lives under `instructions/{pool,positions,trading,fees}`. Match the typed context to the operation and phase before evaluating a rule. Use [hook validation](https://docs.grapl.io/hooks/testing) to check sequence, Clock, partial fills, and settlement. # Liquidity, donations, and fee claims Place policies around principal changes and donations without inventing collection callbacks. Source: https://docs.grapl.io/hooks/liquidity-donate Prerequisites: [LP lifecycle](https://docs.grapl.io/concepts/lp-lifecycle), [callback context](https://docs.grapl.io/hooks/lifecycle), and [the callback contract](https://docs.grapl.io/hooks/first-hook). Use the lifecycle probe to explore these phases; the fee, window, and allowlist examples enable only BeforeSwap. ## Follow a liquidity change `programs/grapl-amm/src/instructions/positions/liquidity.rs` follows this order: 1. Calculate token obligations from the position range, current price, and signed liquidity delta. 2. Check deposit maxima or withdrawal minima. 3. Mark the pool entered and call the enabled before phase. 4. Crystallize prior position fees, update position/ticks/active liquidity, and settle tokens. 5. Serialize the pool for the enabled after phase, then clear the entered flag. The operation's A/B amounts are computed before the hook and reused afterward. They are deposit or withdrawal principal, not a change to fee claims. Hooks do not replace the range calculation, rounding rule, or ownership check. The liquidity instruction uses this argument shape: ```rust let args = ModifyLiquidityArgs { liquidity_delta: delta, limit_a: if delta > 0 { u64::MAX } else { 0 }, limit_b: if delta > 0 { u64::MAX } else { 0 }, hook_payload: payload, hook_accounts: Vec::new(), }; ``` This excerpt shows the argument shape. Replace permissive example limits with meaningful maxima/minima derived from the user’s amounts and slippage. ## Know which policies this context can express A hook can inspect authenticated actor, position key, delta sign/magnitude, token amounts, active pool liquidity, and pool configuration. It cannot read the position's tick range from the key alone: the position and tick book are not forwarded. A hook may maintain its own reward accounting or counters through explicitly permitted writable state. Its account declarations and lifecycle ordering must match the operation; a callback bit alone does not grant writes. Core position/tick/vault accounts remain protected, and custom settlement deltas are unsupported. Compare the account model before adapting [Uniswap's liquidity-hook example](https://developers.uniswap.org/docs/protocols/v4/guides/hooks/liquidity-hooks). A removal policy can reject an LP's exit. The core has no emergency escape instruction that bypasses it. A fixed hook binding also does not establish immutable code if the program has an upgrade authority. ## Follow a donation `instructions/fees/donate.rs` validates a nonzero A/B contribution, runs BeforeDonate, credits active LP fee growth, deposits the tokens, and runs AfterDonate. Donation does not change price, active `L`, or the beneficiary split. It credits LP fee growth directly and requires active liquidity for a nonzero credit. The donation argument shape is: ```rust let args = DonateArgs { amount_a: 10, amount_b: 10, hook_payload: payload, hook_accounts: Vec::new(), }; ``` The same `HookDonationDetails` amounts reach both phases. Before/after snapshots include global fee growth, so AfterDonate can compare the credit while price and active liquidity remain unchanged. A rejection restores the contribution, growth, and provisional hook effects. ## Fee claims are a different operation `collect_position_fees` crystallizes and pays fees without changing liquidity. `collect_beneficiary_fees` pays the configured recipient's debt. Neither dispatches hooks; neither should be described as a liquidity-change or donation callback. Opening and closing the position record also have no callbacks. For reporting, a donation may increase LP entitlement, but it is not organic swap volume. A collection moves an existing claim and must not count as another earning event. See [fee accounting](https://docs.grapl.io/concepts/fees) and [LP performance](https://docs.grapl.io/concepts/lp-performance). ## Preserve atomic settlement A rejection in either callback must restore the position, ticks, pool, and affected token balances. Check after-phase failures as well as early policy rejection. Use [hook validation](https://docs.grapl.io/hooks/testing) when combining liquidity rules with external effects. # Hooks and Metis compatibility Implement an external evaluator with contextual accounts and explicit simulated effects. Source: https://docs.grapl.io/hooks/metis Prerequisites: [callback authentication](https://docs.grapl.io/hooks/authentication), [swap context](https://docs.grapl.io/hooks/lifecycle), and [state permissions](https://docs.grapl.io/hooks/permissions). `GraplAmm` supports hook pools through an injected `HookQuoteAdapter`. The generic integration owns snapshots, core traversal, reply validation, and native construction. Hook implementations stay beside their external programs. A hooked pool without a matching adapter rejects; a no-hook pool needs no adapter. ## Keep policy implementations outside the library The contract lives in `integrations/metis/hooks.rs`, exposed as `grapl_amm::metis::hooks`. The fee, window, and membership examples implement it through `ExampleHookQuoteAdapter` in `examples/hooks/shared/src/metis.rs`, reusing the same source evaluator as their SBF programs. The generic library contains no allowlist, time schedule, or reward economics. | Example | Ordinary `Amm::quote` | Required inputs | | ----------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Dynamic fee | Supported with its injected adapter | Validated schedule and frozen Clock | | Trading window | Supported during the configured window | Validated window and frozen Clock | | Allowlist | Rejects without actor context | Contextual actor, proof payload, policy domain, and Clock | | Prepared rewards | Requires a compatible effect evaluator | Actor, complete swap intent, exact accounts, and validated reward state | | Sponsored rewards | Supported with `SponsoredRewardQuoteAdapter` | Public configuration/issuer/mint snapshot; real wallet bound later; funded sponsor at execution | | Lifecycle probe | No supplied example adapter | An explicit implementation covering its enabled swap callbacks | `SingleRangeQuoter` is a separate, older one-segment snapshot contract that rejects hooks. Its restrictions do not describe `GraplAmm`. ## 1. Declare exactly what the adapter supports | Trait method | Responsibility | | ----------------------------- | ------------------------------------------------------------------------------------------------ | | `program_id`, `program_label` | Bind one program address and name its local dependency | | `requirements` | Reject unsupported profiles/phases; declare policy count, actor need, and Clock need | | `validate_snapshot` | Validate the supplied accounts' encoding, relationships, and invariants | | `account_plan` | Return ordered `HookAccountRequest { key, access }` entries | | `requires_swap_details` | Require complete execution intent when missing fields could change evaluation | | `evaluate` | Produce a canonical reply for a read-only evaluator | | `evaluate_effects` | Produce a reply plus explicit account-byte updates for a stateful evaluator | | `execution_account_plan` | Opt into late wallet-bound account assembly; default `None` preserves prepared snapshot behavior | The default account plan uses at most four canonical read-only policy PDAs. Override it for hook state, external state, or external programs. `requires_swap_details()` defaults to false. The default effect evaluator delegates to `evaluate` only for read-only phases; requested writes require an explicit effect implementation. Every enabled swap callback must be reproduced. Both callback views have `entered = true` and share a simulated next operation sequence and frozen Clock. BeforeSwap receives the initial snapshot and unfilled intent. AfterSwap receives final pool state and the settled traversal result. The generic adapter checks the reply's phase, allow decision, profile, and configured fee cap through the core ABI. ## 2. Discover and prepare an isolated snapshot Create `GraplAmm` through `Amm::from_keyed_account`, then bind an adapter with `with_hook_adapter(Arc::new(adapter))`. Binding invalidates quote readiness. For a contextual account plan: 1. Build the request's `HookSwapContext`. 2. Call `get_accounts_to_update_with_context(&context)`. 3. Fetch the six native pool/tick/mint/vault dependencies, executable hook, and ordered extra accounts. 4. Call `prepare_context(provider, clock, &context)` to obtain a separately refreshed clone. 5. Quote and construct the swap using that clone and the same context. `prepare_context` preserves the original AMM instance. It does not put a buyer or proof into shared mutable request state. `HookAccountSnapshot` contains key, owner, and bytes; the loader also validates executable status and the core's permissions, class/owner declarations, aliases, and account bounds. The external adapter validates each role's exact address and contents. Read-only public policies can use `get_accounts_to_update` and `update_with_clock`. Standard `Amm::update` freezes the injected `AmmContext.clock_ref`. The host must obtain coherent accounts and time: separate atomic ClockRef reads do not establish one bank snapshot. A failed in-place refresh clears readiness; failed `prepare_context` leaves the original adapter unchanged. Quotes fetch no accounts and read no mutable Clock. ## 3. Supply the execution fields the policy needs The current context is: ```rust pub struct HookSwapContext { pub actor: Option, pub payload: Vec, pub details: Option, pub accounts: Vec, } ``` For an actor/proof-only policy, a context fragment can leave the other fields at their defaults: ```rust let context = HookSwapContext { actor: Some(actor), payload: membership_payload, ..Default::default() }; ``` An adapter requiring complete intent must receive `details`: direction, mints, settlement account addresses, authority/owner, requested input, minimum output, and price limit, with `settled: None`. When supplying details for a quote, set `actor` explicitly: `input_authority` and `output_owner` must equal that non-default actor, and the settlement accounts must be distinct and non-default. These are claimed bindings until execution verifies them. Supply an ordered account plan when the adapter requires one. A generic quote must reject if it cannot determine execution-dependent eligibility. `HookEvaluation` borrows `pool`, `invocation`, `accounts`, and optional full `clock`; `intent_complete` identifies whether complete execution details were supplied. The invocation is the canonical ABI 4 type, including its account descriptors. Quote identity is claimed context, not a signature or a reservation of the next pool sequence. Ordinary `Amm::quote` uses empty context and rejects an actor-dependent or complete-intent-dependent adapter. The upstream `QuoteParams` has no wallet, signature, arbitrary proof, or timestamp field. Contextual methods supplement the local adapter API without modifying Jupiter's public request format. ## 4. Simulate state effects separately from swap output `evaluate_effects` returns `HookEvaluationResult { reply, updates }`. Each `HookAccountUpdate { key, bytes }` replaces an existing account's bytes under the current phase's declared write permission. Duplicate updates, undeclared keys, read-only accounts, executable accounts, and byte-length changes reject. This interface cannot create accounts, resize them, change their owner/lamports, or bypass protected aliases. Before-phase changes feed the later phase in the same simulation. `quote_effects_with_context` returns `HookQuoteResult { quote, updates }`, containing final changed account bytes separately from the AMM quote. The original snapshot stays unchanged; this is hypothetical execution, not committed state. For prepared rewards, compare mint supply, destination balance, and issuance counters through these effects. Prepare those accounts before quoting/swapping. Never add reward credits to `Quote.out_amount`: that field counts the selected AMM output token. The opt-in sponsored adapter quotes public eligibility and the AMM output without predicting wallet reward bytes. `execution_account_plan` derives recipient accounts only after `SwapParams.user` is available. The hook creates missing accounts from its own rent reserve and applies participant caps during execution. This is an explicit public quote mode, not simulation of account creation. It rejects identity-dependent allowlists, and a quote cannot guarantee sponsor funding or recipient-account validity. ## 5. Preserve context in native construction Use `swap_with_context(&swap_params, &context)` with the quoted actor, payload, details, and account plan. The builder binds an explicit actor to `SwapParams.user`; an omitted actor is filled from that user for construction. It requires `user == token_transfer_authority`. Explicit settlement addresses must match the native swap accounts. The transaction and core still authenticate signer privilege and token-account ownership. The builder returns `Swap::Placeholder` plus native account metas. Hook extras follow the twelve native accounts and visited tick pages, never request signer privilege, and request transaction writability only for enabled write phases. The core reconstructs the narrower callback privileges. Use `encode_native_swap` to preserve the supported native decision and matching `SwapArgs` descriptors rather than assembling placeholder bytes yourself. The current native decision uses the `GRAPLM03` envelope and rejects legacy decisions. Encoding must preserve the requested input; when complete context bound a minimum output, a different minimum rejects. The price limit, payload, and account descriptors travel in the decision as well. A routed CPI signer can be a PDA owning intermediate token accounts. That authenticated local authority does not universally identify the original human wallet or final beneficiary. Public routed identity needs actual route/CPI evidence; the local placeholder is not a registered Jupiter destination. ## 6. Compare quotes with actual settlement Compare quote amounts and supported simulated effects with settled reserve and hook-account changes. Cover both directions, each advertised swap mode, policy boundaries, rejection, and rollback. See [quote consistency](https://docs.grapl.io/integrations/router-verification). Refresh for changed account/time state. A window can close, a proof can expire, or another transaction can consume a reward budget before execution. Program upgrades also require revalidating evaluator compatibility; a matching program address does not prove matching bytecode. Never make a pool quotable by ignoring a callback, omitting state, or substituting a static fee. Implement the missing behavior or reject explicitly. Public routing additionally requires destination/encoder support, account/CU checks, and the separate [Jupiter integration process](https://developers.jup.ag/docs/swap/routing/amm/integration). Continue with the [general Metis guide](https://docs.grapl.io/integrations/metis), [reward example](https://docs.grapl.io/hooks/rewards), and [hook testing](https://docs.grapl.io/hooks/testing). `integrations/metis/HOOK_API.md` is the source-level adapter contract. ## Sale-funded output fees The adapter applies the same bounded output-fee calculation as core. `Quote.out_amount` is net, and `HookQuoteResult.output_fee_amount` exposes the separate output charge. Input-fee metadata remains denominated in the input mint. Both vault sufficiency and net minOut are checked. The [rewards example](https://docs.grapl.io/hooks/rewards#fund-sponsorship-from-sales) uses this to replenish its rent sponsor with native SOL. # Hooks Learn the callback model and compose bounded pool policies. Source: https://docs.grapl.io/hooks/overview A hook is a Solana program that adds rules to a pool's lifecycle. It can check eligibility before a trade, choose a fee within the pool's cap, or issue rewards after settlement. The core still owns pricing, liquidity accounting, and token transfers. **A pool works without a hook.** Attach one only when you need custom behavior. Allowlist, trading-window, fee-decay, and rewards policies live in `examples/hooks/`; none is a default restriction on every pool. ## Choose your next step [Build your first hook](https://docs.grapl.io/hooks/first-hook) Build the lifecycle example locally, acknowledge callbacks, and test rejection. [Understand callback context](https://docs.grapl.io/hooks/context) Read actor identity, Clock, requested amounts, settlement, and extra accounts. [Explore hook examples](https://docs.grapl.io/hooks/examples) Choose fees, launch decay, windows, membership, rewards, or a composed policy. [Test and integrate](https://docs.grapl.io/hooks/metis) Define the read-only state and context required by a Metis adapter. ## Before and after a swap `BeforeSwap` sees the proposed operation and can reject it or request a permitted fee. After the core settles, `AfterSwap` receives actual amounts. An after callback can still reject: all transaction state changes roll back together. Transaction fees can still be charged. The callback authority is a **core PDA**, not the wallet or vault signer. The authenticated `actor` may be a router PDA. Use [authentication](https://docs.grapl.io/hooks/authentication) before interpreting an actor as the end user. ## Ten callbacks, five operations | Operation | Before phase | After phase | | ----------------- | ----------------------- | ---------------------- | | Initialize a pool | `BeforeInitialize` | `AfterInitialize` | | Add liquidity | `BeforeAddLiquidity` | `AfterAddLiquidity` | | Remove liquidity | `BeforeRemoveLiquidity` | `AfterRemoveLiquidity` | | Swap | `BeforeSwap` | `AfterSwap` | | Donate | `BeforeDonate` | `AfterDonate` | The pool enables specific phases through its callback bitmap. Read [lifecycle timing](https://docs.grapl.io/hooks/lifecycle) for the exact snapshot and fields available at each phase. Graduation uses the add-liquidity pair; it does not introduce a new callback. Zaps compose the corresponding swap and liquidity phases. ## One hook, several rules One hook program binds to a pool, and that program can serve many pools with separate policy accounts. To combine a trading window, membership proof, fee schedule, and rewards, implement their order and state in one composed hook. Independent examples are not automatically stacked. Follow [Compose your own policy](https://docs.grapl.io/hooks/compose-policy) to choose callback timing, identity requirements, state access, and quote behavior. ## Optional example programs | Example path under `examples/hooks/` | What you learn | Enabled phases | | ------------------------------------ | ------------------------------------------------------------------------------ | --------------------------------------- | | `lifecycle/` | Authentication, typed replies, phase logs, rollback | All ten | | `dynamic-fee/` | Runtime-clock fee schedule bounded by the pool cap | BeforeSwap | | `decaying-fee/` | [99%-to-floor launch schedule](https://docs.grapl.io/hooks/decaying-fee) with configurable duration | BeforeSwap | | `timelock/` | Inclusive opening and exclusive closing timestamps | BeforeSwap | | `allowlist/` | Membership for the core-authenticated actor | BeforeSwap | | `rewards/` | Optional non-transferable credits from actual settled output | Composed eligibility and reward effects | | `shared/` | Shared policies, account setup, and client encoding | Shared support | ## Know the current boundary The ABI provides typed before/after context, bounded policy replies, dynamic fees, and explicit state permissions. It rejects custom accounting and return deltas. Hooks may update permitted external or hook-owned state while the core pool and settlement accounts remain protected. The lifecycle probe logs phases without a counter; [reward credits](https://docs.grapl.io/hooks/rewards) illustrate a separate stateful example. A pool hook is not a universal restriction on a token. It does not control transfers or trading in other pools. [Token-2022 transfer hooks](https://docs.grapl.io/integrations/token-programs) have a different caller and scope. # Choose phases, profiles, and account permissions Request only the callbacks and state access your hook implements. Source: https://docs.grapl.io/hooks/permissions Prerequisite: identify your phase in the [callback catalog](https://docs.grapl.io/hooks/lifecycle). The public contract is `programs/grapl-amm/HOOK_FRAMEWORK.md`; implementation types live in `src/hooks/abi/`, with permission and account checks in `src/hooks/{permissions,accounts}.rs`. ## Select callbacks and reply behavior Each phase has a bit from `HookPhase::bit()`. The complete mask is `1023`; BeforeSwap alone is `64`, and both swap phases are `192`. These are grapl values, not Uniswap address flags. This configuration fragment enables policy-only swap callbacks with the default read-only account policy: ```rust use grapl_amm::hooks::abi::{HookCapabilities, HookPhase, HookProfile}; let capabilities = HookCapabilities { bits: HookPhase::BeforeSwap.bit() | HookPhase::AfterSwap.bit(), profile: HookProfile::PolicyOnly, ..HookCapabilities::default() }; assert!(capabilities.is_supported()); assert_eq!(capabilities.bits, 192); ``` `is_supported` checks callback/profile compatibility; it is not the complete validation of account permissions. The fee, window, and allowlist examples require exactly BeforeSwap. A composed stateful example can select a broader set, but each selected phase needs a valid implementation. | Profile | Requirement | Successful reply | | ------------------ | ----------------------------- | ---------------------------------------------------------- | | `PolicyOnly` | Any supported callback subset | `allow: true`, `fee_ppm: None` | | `DynamicFee` | Must include BeforeSwap | BeforeSwap returns `Some(fee)`; other phases return `None` | | `CustomAccounting` | Unsupported | Rejected | The core accepts a configured pool cap up to `990000` ppm (99%). That protocol ceiling does not raise an existing pool's immutable cap or default fee. A hook cannot exceed the cap chosen for its pool. An error, `allow: false`, missing return data, wrong producer/version/phase, or incompatible fee rejects the operation. ## Declare the pool's access ceiling `HookCapabilities.permissions` bounds what callers may request. Its default allows at most four read-only hook-owned extras, zero writable extras, and no external programs. A callback bit alone grants no write access. | Permission field | Grants within enabled phases | | ---------------------------------------------- | --------------------------------------------------------------------------------- | | `max_output_fee_ppm` | Independent output-fee ceiling, default 0; at most 990000 and requires BeforeSwap | | `hook_write_phases` | Writes to hook-owned policy/state accounts | | `external_read_phases` | Read-only external state | | `external_write_phases` | Writable external state | | `external_program_phases` | Access to declared executable programs | | `max_extra_accounts` | Configured total, at most 16 | | `max_writable_accounts` | Configured writable total per callback, at most 8 | | `external_program_count` / `external_programs` | Declared prefix of at most four program IDs | Masks must be subsets of enabled lifecycle phases. The program-ID prefix must be unique; unused entries must be zero. Core and attached-hook IDs cannot appear in that prefix. The System Program's zero ID is valid only as an explicitly included prefix entry. An external ID authorizes a program/owner class, not arbitrary role-specific addresses. ## Request exact accounts for each operation The hookable instruction's `hook_accounts` vector must describe every remaining account in order. Each descriptor selects a class and `writable_phases`. An empty vector supplies no accounts; it does not request a permissive default. | Class | Core requirement | Additional hook responsibility | | --------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `PolicyPda { index }` | Hook-owned, non-executable, canonical `[b"policy", pool, index]`, index 0–3 | Decode policy and validate its pool/configuration | | `HookState` | Hook-owned, non-executable | Bind its PDA, discriminator, actor and other relationships | | `ExternalState` | Non-executable; owner is a declared external program | Bind the exact mint/account/authority and supported state | | `ExternalProgram` | Executable; key is a declared program | Use only the intended CPI interface | | `CreatableHookState` | Hook-owned state, or empty System-owned account with System explicitly authorized | Bind the canonical PDA before allocating or assigning; writes need hook-write permission | | `PoolMintReadOnly` | Exact pool reserve mint; zero write mask and no signer | Use only as a readonly mint view for token CPI | | `ActorReadOnly` | Exact authenticated actor; zero write mask; no CPI signing privilege | Use as an identity/ATA-owner view, never as a payer or signer | | `InstructionsReadOnly` | Canonical Instructions sysvar key/owner; readonly, nonsigner, non-executable; explicit external-read phase | Verify the signed top-level envelope instead of trusting CPI signer flags | Writes need both pool permission and the descriptor's phase bit. An external state account also needs read permission in any enabled phase where it is passed read-only. External programs are always read-only. Transaction-level writability is an upper bound; the core rebuilds CPI metas separately for each phase and can downgrade an account to read-only. All enabled phases of the operation are validated before the first callback. A missing AfterSwap permission therefore cannot let a BeforeSwap side effect execute first. Duplicate extras, wrong owners/programs, excess counts, and protected-account aliases reject. Signer extras reject except for `ActorReadOnly`: it may refer to the outer actor signer, but the callback receives it strictly read-only and without signing privilege. ## Preserve the authority boundary The core hook PDA remains a read-only signer, and the pool remains read-only. Actor signer privileges and vault authority are never forwarded. State permissions do not grant arbitrary settlement. `PoolMintReadOnly` explicitly permits a reserve mint view. A nonzero output-fee ceiling allows a bounded BeforeSwap request; core performs the token transfer and checks net slippage without forwarding vault authority. A hook may sign for its own PDA in a nested CPI using its own program seeds. For example, a reward hook may control a separate Token-2022 mint. Its program/state accounts must be declared and validated; that authority cannot spend the AMM's vaults. See [authentication](https://docs.grapl.io/hooks/authentication) and [reward credits](https://docs.grapl.io/hooks/rewards). ## Apply the permissions Check per-phase privilege downgrades and rollback for every stateful operation. Serialized permission masks alone do not establish runtime behavior. Continue with [pool binding](https://docs.grapl.io/hooks/attach) and [hook validation](https://docs.grapl.io/hooks/testing). # Award non-transferable reward credits Compose optional eligibility rules and issue RWD from actual settled output. Source: https://docs.grapl.io/hooks/rewards **Example hook: RewardCredits (RWD).** `examples/hooks/rewards` demonstrates an external composed hook. It selects BeforeSwap and AfterSwap, evaluates optional eligibility gates, and calculates credits from the output actually delivered by a swap. The core supplies context and account permissions; reward economics are not native pool rules. Pools without this hook earn no RWD through it. Prerequisites: [callback context](https://docs.grapl.io/hooks/lifecycle), [state permissions](https://docs.grapl.io/hooks/permissions), and [your first integration](https://docs.grapl.io/start/quickstart). Conceptual reward allocation. The rule below defines the actual amount and recipient. ## Configure the reward unit and budgets `RewardRules` selects one `eligible_output_mint`, a `PurchaseRewardRate`, a lifetime issuance cap, and a per-participant cap. Its public constructor leaves `window` and `allowlist` disabled. Optional gates reuse the earlier example policies; they do not become requirements of every pool. For actual output in base units: ```text calculated = floor(actual_output × numerator × 10^reward_decimals / (denominator × 10^purchased_decimals)) available = min(lifetime_cap − lifetime_issued, participant_cap − participant_issued) granted = min(calculated, available) ``` The rate uses checked integer/wide arithmetic, positive numerator/denominator, and decimal counts from 0 through 18. Caps and issued totals use reward base units. Corrupted totals above a cap reject; a valid exhausted budget grants zero and preserves an otherwise valid swap. Credits rounded away are not accumulated as fractional entitlement. Illustrative rewards: 1.25 tokens of settled output at 2 RWD per token imply 2.5 RWD. Remaining lifetime budget 1 RWD and participant budget 0.5 RWD limit issuance to 0.5 RWD. Credits are separate from swap output. Issuance totals are cumulative. Burning or consuming a token balance does not reopen lifetime or participant issuance budgets. The reward example does not promise a monetary peg, redemption reserve, or AMM liquidity for these credits. ## Compose gates and effects inside one hook The example authenticates the core callback, validates its account/context bindings, checks an optional trading window, and then checks optional Merkle membership. BeforeSwap may prepare sponsored accounts, but never issues credits. AfterSwap repeats the deterministic eligibility evaluation over the same operation Clock and uses `settled.actual_output` for issuance. The rate applies only when `output_mint` matches the configured eligible mint. The opposite direction grants zero. Enabled gates still apply to the operation; an unrewarded direction is not an automatic bypass. With allowlist disabled, the payload must contain no membership proof. With it enabled, the proof binds the actor, pool, protocol, eligible mint, root version, namespace, and expiry. This is composition within one selected program. The core does not attach an ordered list of independent hook programs or supply a built-in rule registry. A composed program owns its ordering, failure semantics, and state. The reward example uses PolicyOnly and returns no dynamic fee override. ## Provision the exact reward accounts `RewardAddresses::derive(hook, pool, actor)` uses stable pool/actor identities. It does not predict the next pool operation sequence. Choose one immutable preparation mode: | Mode | Pool capabilities and adapter | Who prepares accounts? | | ------------------ | -------------------------------------------------------------- | ------------------------------------------------------ | | Prepared (default) | `reward_capabilities()`, `RewardHookQuoteAdapter` | Participant, before contextual quote/swap | | Sponsored (opt-in) | `reward_capabilities_for(true)`, `SponsoredRewardQuoteAdapter` | Hook, inside the first swap, funded by its sponsor PDA | The following six-account table describes prepared mode. | Extra, in callback order | Identity | Requested access | | ------------------------ | --------------------------------------------------------- | -------------------------------------- | | Configuration | Hook PDA `[b"policy", pool, &[0]]` | Read-only PolicyPda | | Issuer | Hook PDA `[b"reward-issuer", pool]` | HookState, writable only AfterSwap | | Participant | Hook PDA `[b"reward-user", pool, actor]` | HookState, writable only AfterSwap | | RWD mint | Hook PDA `[b"reward-mint", pool]`, owned by Token-2022 | ExternalState, writable only AfterSwap | | RWD destination | Actor's Token-2022 associated account for the reward mint | ExternalState, writable only AfterSwap | | Token-2022 program | Declared executable ID | Read-only ExternalProgram | Use `reward_capabilities()` and `reward_account_access()` together. They declare six extras, at most four writable in AfterSwap, hook writes only after settlement, external reads before settlement, external writes afterward, and the Token-2022 program in both swap phases. The core hook PDA and read-only pool precede these extras. Provisioning has two explicit instructions: | Instruction | Ordered accounts and privileges | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `configure_rewards(&rules)` | Creator (writable signer), pool (read-only), configuration/issuer/mint (writable), eligible purchased mint, Token-2022 program, System Program (read-only) | | `REWARD_PREPARE_TAG` | Actor (writable signer), pool/configuration (read-only), participant (writable), reward mint (read-only), destination (writable), Token-2022/Associated Token/System programs (read-only) | Setup requires the pool creator and the exact reward capabilities. It validates the eligible pool mint and purchased decimals, creates immutable configuration and issuer state, then creates a NonTransferable reward mint controlled by the issuer PDA. There is no freeze authority. Participant preparation creates actor-bound state and the canonical Token-2022 ATA. Its ATA creation is idempotent; the full participant-creation instruction is not advertised as repeatable. Use the executable builders in `examples/hooks/rewards/tests/svm_support/instructions.rs` for the precise account order and instruction encoding. `SwapArgs.hook_accounts` must equal `reward_account_access()` and the supplied callback extras must follow that same order. Prepare the reward accounts before swapping. Callback CPI receives neither the payer nor the actor's signing privilege. The hook's issuer PDA is its separate mint authority. Core vaults, actor settlement accounts, pool mints, and other protected accounts cannot be substituted as reward extras. ### Sponsor account creation for public quotes Set `rules.sponsored_preparation = true` before configuration. Fund `reward_rent_address(pool)` using a normal SOL transfer. This System-owned PDA has seeds `[b"reward-rent", pool]` under the hook; funding is separate from pool creation. The current example has no sponsor-withdrawal instruction. `reward_account_access_for(true)` and `sponsored_reward_keys(pool, actor)` describe ten extras. Participant state becomes `CreatableHookState`, writable before and after; the ATA also becomes writable before and after. After the six accounts above, add the sponsor (writable BeforeSwap), System Program, Associated Token Program, and `ActorReadOnly` view. Pool capabilities explicitly authorize all three external programs. The actor view carries no signature or writing power. BeforeSwap validates identity and eligibility before using the sponsor's own PDA signature to create missing accounts. AfterSwap issues RWD. Repeated swaps preserve counters and do not repay rent. Both swap directions can trigger preparation, including a sale that earns zero credits. Insufficient sponsor funds or a substituted account rolls back the entire transaction. Rent sponsorship is per new address; small trades from many fresh wallets can consume the budget. Issuance caps do not cap sponsored account creation. The sponsor must keep funds available; the ordinary quote does not check that balance. Normal transaction fees still belong to the transaction fee payer. ## Preserve actual settlement and issuance identity BeforeSwap has no settled output. AfterSwap carries the input/output mints, actual output, actual consumed input, unspent input, fee amount/rate, and before/after pool snapshots. Issuance uses output rather than the requested maximum input, a quote estimate, or the transaction fee. The prepared and ordinary sponsored profiles require an authenticated nondefault actor matching the local input authority and output owner. Their participant state is bound to that actor and pool. The routed profile below separates the execution actor from the signed beneficiary. Issuer and participant state track the last processed operation sequence; stale/replayed context rejects. The sequence is a core operation identifier, not randomness or a preallocated reward-request address. RWD uses the Token-2022 NonTransferable mechanism. Its destination ownership must remain immutable. The standard prevents transfers while allowing the owner to burn and close eligible accounts; that does not implement an application-specific redemption or claim flow. See the [Token-2022 extension guide](https://www.solana-program.com/docs/token-2022/extensions#non-transferable-tokens). The traded pool assets still use classic SPL Token. A separate reward CPI does not make Token-2022 assets valid pool reserves. Minting and hook-state updates remain provisional: a later failure must roll them back with the swap. ## Keep rewards with the wallet through a router A router may spend from its own PDA and intermediate token accounts. In the prepared and ordinary sponsored profiles, that PDA is the local actor and would receive the credits. Non-transferable rewards cannot be forwarded from it later. Choose `routed_reward_capabilities(rules.sell_fee_ppm)` at pool creation and inject `RoutedRewardQuoteAdapter` to bind rewards to a separate wallet. This explicit profile uses sponsored preparation and public eligibility; allowlists remain unsupported by this profile. Existing pools retain their immutable profile. The wallet signs a transaction containing `RoutedRewardIntent`: beneficiary, pool, top-level router, next operation sequence, deadline slot and exact swap accounts, mints, amounts and price bounds. Both callbacks authenticate the core invocation and inspect the canonical Instructions sysvar. The beneficiary must be a signer of the original transaction, and the exact intent must occur in the current signed top-level instruction. A PDA's CPI signature cannot satisfy that proof. The participant PDA and reward ATA belong to the verified beneficiary. The router downgrades the beneficiary to readonly/nonsigner for CPI, so the hook receives no wallet signing power. Rent comes from the sponsor; issuance uses settled output and the existing caps. Missing, changed, expired or replayed consent rejects. A later transaction failure rolls back minting and rent with the swap. | Integration | Contextual execution entrypoint | Verified scope | | --- | --- | --- | | Metis | `swap_with_context` | Native builder and a local custody probe | | Titan | `generate_swap_instruction_with_context`, `build_leg_with_context` | Local Titan template with PDA custody | | DFlow | `build_native_instruction_with_context` | Native builder and local pass-through CPI | In the local custody fixture, 10,000 input atoms buy 9,871 output atoms. The wallet receives 19,742,000 reward atoms and the custody PDA receives zero. These results use a disposable test reward mint, not a public GEM deployment. Public router adoption remains separate. DFlow's pinned interface still requires a Grapl execution mapping, and no local test establishes listing or discovery in an aggregator's live service. This contract authorizes rewards for a particular Grapl leg; the router remains responsible for its final output delivery. A pending intent must be rebuilt after another pool operation changes its sequence. ## Preserve quote compatibility `RewardHookQuoteAdapter` in `examples/hooks/rewards/src/metis/` implements the external effect interface. It requires an actor and complete swap details, derives the six-account plan, reuses reward evaluation and token validation, and predicts issuer/participant bytes plus mint/destination balances. Reward credits are separate effects; they must never be added to `Quote.out_amount` for the AMM output asset. Missing actor/state or unsupported effect evaluation must reject explicitly. See [Metis compatibility](https://docs.grapl.io/hooks/metis) for the generic integration boundary. `SponsoredRewardQuoteAdapter` supports standard wallet-free refresh/quotes using configuration, issuer and mint snapshots. It validates public rules, then binds ten execution accounts to the real `SwapParams.user`. The hook creates the missing recipient accounts during execution. This adapter returns no wallet reward-byte predictions; actual issuance and participant caps are enforced on-chain. It rejects allowlist-enabled configurations and nonempty proof payloads. A public trading window remains supported. ## Probabilistic rewards remain a separate design The deterministic example has no lottery. A probabilistic extension would first commit eligibility, then obtain verified randomness asynchronously, authenticate fulfillment, and consume the request once. No provider has been selected or integrated for that flow. Clock, blockhash, caller-selected seeds, and a same-transaction path that can abort after a loss are not acceptable randomness sources for the proposed flow. Sequence numbers can bind requests but do not supply entropy. Random selection, consumption, claims, or a reference reserve would remain responsibilities of the external hook/application, with their own verification. Next, review [authentication](https://docs.grapl.io/hooks/authentication), [permissions](https://docs.grapl.io/hooks/permissions), and the [ABI reference](https://docs.grapl.io/reference/hook-abi) before extending the example. ## Fund sponsorship from sales Sale funding is optional and defaults to `sell_fee_ppm = 0`. This example supports a classic `TOKEN/WSOL` pool: the eligible reward token is TOKEN, purchases earn RWD, and sales may contribute part of their WSOL output to the sponsor. Other quote assets such as USDC reject this funding mode; they require a separate conversion mechanism. ```rust rules.sponsored_preparation = true; rules.sell_fee_ppm = 5_000; // 0.5% of gross WSOL output. let capabilities = reward_rule_capabilities(&rules); ``` Set these immutable capabilities at pool creation, then configure the hook with matching rules. The standard `SponsoredRewardQuoteAdapter` selects the same fee rule and builds the required account plan. Manual clients use `funded_reward_access(true)` and `funded_reward_keys(pool, actor, true)`. 1. BeforeSwap prepares missing reward accounts and a temporary WSOL PDA `[b"reward-fee", pool]`. The sponsor pays its rent; the actor stays readonly and nonsigner inside the hook. 2. The hook requests `floor(gross_output * sell_fee_ppm / 1_000_000)`. Core transfers net output to the seller and the contribution to the temporary vault, under the independent immutable `max_output_fee_ppm` ceiling. 3. AfterSwap closes the WSOL vault to `[b"reward-rent", pool]`. This unwraps the contribution into native SOL and returns the temporary rent. A failure anywhere rolls all account and token changes back. `Quote.out_amount` and `settled.actual_output` are net. `minimum_out` protects that net amount. `HookQuoteResult.output_fee_amount` reports the contribution separately in output-token units; Jupiter's ordinary `fee_amount`/`fee_mint` continue describing the input-denominated AMM fee. LP accounting is unchanged, and the seller pays both configured charges. The fee-enabled plan has 13 extras. The sponsor is writable in both swap phases; the extra roles are the temporary WSOL account, classic Token Program, and a `PoolMintReadOnly` view of the native mint. Purchases do not create the temporary fee vault or pay this contribution. The sponsor still needs initial SOL: BeforeSwap pays rent before the sale settles. Small floor-rounded fees may be zero. Sales replenish the budget, but wallet creation demand can exceed revenue; a quote does not reserve or verify the sponsor balance. Normal transaction fees remain the transaction fee payer's responsibility. There is no sponsor-withdrawal instruction in this example. ## Rewards on direct Prism markets Prism can fill a pair directly, such as `WSOL → TOKEN`, without an intermediate CLMM pool. It can also participate in a larger route. Maker inventory remains in maker-owned token accounts until settlement; executable output is limited by balance, delegated allowance, strategy budget and price range. `RoutedRewardQuoteAdapter` supports both deposited CLMM and Prism markets. Create the market with `routed_reward_capabilities(rules.sell_fee_ppm)` and use the same signed-beneficiary intent described above. The Prism builder resolves the beneficiary's execution accounts after quoting and preserves signed price and minimum-output bounds. The reward ATA belongs to the wallet, including when the execution authority is a router PDA. For a TOKEN/WSOL market, a configured sale contribution can fund the sponsor in native SOL. Local SBF tests reconciled 4,945,104 lamports collected from a sale and 3,605,280 lamports spent preparing a different buyer's reward accounts. That buyer received 19,838,000 test reward atoms; the custody PDA received zero. A second buy reused those accounts without another rent charge. These are fixture quantities, not a promised emission rate or economic return. Validation covers the Metis native builder, a local Titan custody template, and DFlow native/pass-through CPI builds. It does not establish adoption by public routers. The sponsor still needs bootstrap SOL, and stablecoin fees need a separate conversion before they can pay rent. Tests reject forged/replayed consent and revoked allowance, and roll back rewards, fees and rent when a later instruction fails. Rent is an account-storage deposit, not a fee per swap. Preparation only funds missing accounts (or a rent shortfall); repeated swaps reuse existing accounts. Closing and recreating a token account can require a deposit again. The reward participation PDA has no close instruction in this example. Actual rent amounts come from the execution environment's rent configuration, so the local figures above are not a fixed quote for another cluster. # Swap hooks and dynamic fees Use a clock-based fee schedule without replacing core pricing or settlement. Source: https://docs.grapl.io/hooks/swap-fees Prerequisites: [the callback contract](https://docs.grapl.io/hooks/first-hook), understand [BeforeSwap/AfterSwap](https://docs.grapl.io/hooks/lifecycle), and use the [policy setup order](https://docs.grapl.io/hooks/attach). The example is `examples/hooks/dynamic-fee`; its pure calculation is `examples/hooks/shared/src/fee.rs`. **Example hook: dynamic fee.** The core provides the bounded fee-reply capability; this particular time-decay schedule is optional example code. Pools do not adopt it automatically. ## 1. Configure the fee schedule Consider a schedule that decreases from 1,000 ppm to 100 ppm: ```rust HookPolicy::DynamicFee(FeeSchedule { peak_ppm: 1_000, floor_ppm: 100, starts_at: 100, ends_at: 200, }) ``` Pass this policy value to the configuration instruction. The timestamps are illustrative Unix seconds. Production schedules would need explicitly chosen timestamps and clock-aware quotes; changing a browser clock cannot change execution time. Use the `DynamicFee` profile with exactly `HookPhase::BeforeSwap.bit()` for this example. The authenticated pool cap must allow the peak. Empty/reversed intervals, floor above peak, or peak above cap reject configuration. ## 2. Calculate the rate with integer rounding For time clamped into the configured interval: ```text remaining = ends_at - clamped_time duration = ends_at - starts_at fee_ppm = floor_ppm + ceil((peak_ppm - floor_ppm) × remaining / duration) ``` | Runtime time | Rate for the example | | ---------------- | -------------------- | | Before or at 100 | 1000 ppm = 0.10% | | 150 | 550 ppm = 0.055% | | At or after 200 | 100 ppm = 0.01% | The shared runtime passes `call.clock.unix_timestamp`, captured and authenticated once by the core, to the policy evaluator. It no longer reads a second Clock sysvar. Callers cannot replace that timestamp with a browser timestamp or payload value. The client must pass an empty hook payload; it cannot override the schedule by submitting a fee value. Ending the decay leaves the floor fee active. It does not close trading. ## 3. Apply the reply in the core BeforeSwap returns `HookReply` with `Some(rate)`. The core validates the phase, profile, responder, and cap, then uses the rate for traversal. This overrides the rate for that swap; it does not rewrite the pool's static fee configuration or fee-share beneficiaries. The core computes input fees and performs transfers. AfterSwap sees actual consumed input/output and the resulting price, and must return no fee even if the pool uses `DynamicFee`. An after hook cannot retroactively replace the fee or return custom deltas. This differs from the signed deltas and return types discussed in [Uniswap's swap-hook guide](https://developers.uniswap.org/docs/protocols/v4/guides/hooks/swap-hooks). Use grapl's [typed swap context](https://docs.grapl.io/hooks/lifecycle) when interpreting requested input, consumed input, actual output, and unspent input. ## Follow one amount through the schedule For 10,000 input atoms, the example charges 10 atoms at time 100, 6 at time 150, and 1 at time 200. The midpoint rounds the token fee up from 5.5 to 6. Keep rate rounding and token-amount rounding distinct. ## Next: preserve quote/execution agreement A compatible quote adapter needs the same schedule, configuration, timestamp semantics, cap, rounding, and execution eligibility. Quoting the static fee or using process wall-clock time would not reproduce this policy. Continue with [Metis compatibility](https://docs.grapl.io/hooks/metis). For a 99%-to-floor preset of this same linear curve, continue with [Decaying launch fee](https://docs.grapl.io/hooks/decaying-fee). That demonstration accepts `starts_at` plus `duration_seconds`, has its own program/configuration format, and defaults its convenience constructor to 990,000 ppm. It preserves the same upward rate rounding and endpoint clamps. The initial rate still requires a matching pool cap; the temporal rule remains external to the core. # Validate hook behavior Check identity, permissions, policy boundaries, and atomic effects for your hook. Source: https://docs.grapl.io/hooks/testing Validate the behavior your application promises to users. A successful callback is only one part of an operation: the quote, account permissions, reserve settlement, and any external effects must agree. ## Run the custom-hook baseline The [BaseHook template](https://docs.grapl.io/hooks/first-hook) provides a single authoring gate from the protocol checkout: ```bash make test-hook-template ``` It runs host authentication and reply tests, builds the core and hook SBF programs, executes all ten callbacks through core CPI in LiteSVM, and checks deliberate before/after failures with the lifecycle probe. Expect zero failures from each suite. It uses disposable in-process fixtures and does not send transactions to a public network. Host tests cover forged signer flags, wrong authority and pool owners, hook binding, inactive pools, sequence and mint mismatches, malformed bytes, capability mismatch, missing callbacks and invalid replies. A separate SBF test calls the hook directly without the core PDA signature and verifies rejection before any callback executes. The broader `make test-router` gate repeats the template through the local router CPI probe; that probe is not Jupiter admission. The starter has no application policy. Extend its tests with your own state transitions and quote adapters rather than treating the generic baseline as validation of custom economics. ## Check the policy's boundaries | Rule | Accept | Reject | | -------------- | -------------------------------------------------------- | ------------------------------------------------------- | | Trading window | Opening timestamp through the instant before closing | Before opening and at closing | | Fee schedule | Correct integer fee within the pool cap | Over-cap fee or payload override | | Membership | Valid actor-bound proof for the configured pool and root | Wrong actor, grant, pool, root, or proof shape | | Rewards | Credits derived from eligible settled net output | Missing required funding or substituted recipient state | Combine boundary cases when rules share a callback. A valid proof after a window closes must still reject. See [compose a policy](https://docs.grapl.io/hooks/compose-policy). ## Authenticate the entire account contract Validate the callback PDA, pool owner and version, hook binding, invocation state, policy PDA, and exact account ordering. Exercise direct calls to the hook and substituted accounts: neither should impersonate a core callback. Check signer and writable privileges at each phase. An outer writable account does not automatically become writable in every callback. The actor's signing privilege and vault authority are not forwarded. ## Reconcile before and after For each supported operation, compare pool state, positions, tick pages, reserve balances, fee accrual, and hook-owned or external state. Check actual partial-fill amounts and both swap directions. Requested input is not evidence of settled output. Deliberately reject each enabled phase. Confirm the expected error and the restoration of all operation state, including failures after transfers or reward issuance. Transaction fees remain outside that rollback claim. ## Compare quoting with execution Use a coherent pool and policy snapshot, the same Clock, and the same identity context. Compare quote amounts with actual token deltas, including output fees. Verify that unavailable policy state, stale pages, and unsupported modes reject explicitly. An ordinary public quote cannot evaluate a wallet-bound rule without the required context. Follow [hook adapters](https://docs.grapl.io/hooks/metis) and [quote consistency](https://docs.grapl.io/integrations/router-verification). ## Measure the intended composition Account count, tick traversal, proof bytes, external CPIs, and account creation all affect the transaction budget. Simulate the exact account list and transaction format you intend to submit. Measure both acceptance and rejection paths; a small successful example does not bound the largest supported operation. The lifecycle example under `examples/hooks/lifecycle/` demonstrates phase acknowledgement and deliberate rejection. Use it to understand the contract, then validate your own economics and state transitions. See [hook troubleshooting](https://docs.grapl.io/hooks/troubleshooting) for common failures. # Restrict trading to a time window Configure a half-open runtime-clock window and verify its boundary behavior. Source: https://docs.grapl.io/hooks/trading-window Prerequisites: [the callback contract](https://docs.grapl.io/hooks/first-hook) and understand [configuration](https://docs.grapl.io/hooks/attach). The program is `examples/hooks/timelock`; its pure policy is `TradingWindow` in `examples/hooks/shared/src/window.rs`. **Example hook: trading window.** This optional example gates swaps in a configured pool. Trading windows are not a built-in or default protocol requirement. ## 1. Define the interval The example policy value is: ```rust HookPolicy::Timelock(TradingWindow { opens_at: 100, closes_at: 200, }) ``` These timestamps are illustrative Unix seconds. Setup requires `opens_at < closes_at`, the `PolicyOnly` profile, and exactly BeforeSwap. Use the creator-owned setup path; there is no policy-update instruction. ## 2. Apply execution time, not client time The callback reads the runtime Clock and evaluates: ```text opens_at <= now < closes_at ``` | Time | Result for `[100, 200)` | | ---- | ------------------------ | | 99 | Reject: not open | | 100 | Allow: opening included | | 199 | Allow | | 200 | Reject: closing excluded | The hook requires an empty payload and returns `fee_ppm: None`. It does not let the caller submit an alternative timestamp. A transaction prepared before closing can still reject when executed after closing. ## 3. Explain what is gated The policy applies to swaps in either direction in its bound pool. It is not a buy-only rule, a token transfer restriction, an LP vesting schedule, or a liquidity withdrawal lock. The example enables no removal callbacks and leaves fee claims outside hook dispatch. The name `timelock` refers to this trading window only. Combining it with dynamic fees would require a deliberately composed policy program and its own tests; a pool cannot attach these two example programs independently. ## Next: quote only executable policy paths A compatible quote adapter must evaluate the same runtime-time snapshot and account for expiration between quote and execution. A successful quote at opening is not an unconditional promise of execution. Continue to [Metis compatibility](https://docs.grapl.io/hooks/metis) or [allowlist membership](https://docs.grapl.io/hooks/allowlist). # Troubleshoot a hook Locate the failing layer before changing policy or weakening validation. Source: https://docs.grapl.io/hooks/troubleshooting Identify the failing operation, program, phase, and logs using [the callback contract](https://docs.grapl.io/hooks/first-hook). Start by finding whether failure occurred in core validation, CPI authentication, policy evaluation, reply validation, or settlement. ## Diagnose by symptom | Symptom or error | Likely cause | Fix and verify | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `UnsupportedHook` during setup | Unknown/delta bits, CustomAccounting, or DynamicFee without BeforeSwap | Select supported capabilities and validate the profile | | `InvalidHookTarget` | Wrong sentinel, nonexecutable program, or core used as its own hook | Correct target binding and program loading | | `InvalidHookAccounts` | Descriptor/account mismatch, wrong role/address/owner, duplicates, protected alias, signer extra, or missing transaction writability | Rebuild the exact account list and descriptors from [authentication](https://docs.grapl.io/hooks/authentication) | | `InvalidHookPermission` | Requested phase access or count exceeds pool configuration; invalid external-program declarations | Check [per-phase permissions](https://docs.grapl.io/hooks/permissions), including both callbacks | | `InvalidHookContext` | Inconsistent operation details, sequence, snapshots, direction, or settled result | Use the current typed ABI and matching core build; never fabricate callback context | | `MissingCoreSignature` | Hook was called directly or wrong callback authority supplied | Send the operation through core; retain PDA validation | | `InvalidPoolState` | Wrong pool version or callback outside entered state | Use the matching core and account contract; do not forge the lifecycle flag | | `InvalidPolicyKind` | Economic example configured for more than BeforeSwap or wrong profile | Use its exact phase/profile pair | | `AlreadyConfigured` | Repeated setup against existing policy | Use the existing policy; setup is not an update operation | | `PolicyRejected` | Closed time, expired/mismatched proof, or fee bounds | Read the structured `hook_policy_rejected` reason and reproduce the exact boundary | | `InvalidPayload` | Nonempty fee/window payload or malformed proof/probe payload | Use the policy's documented encoder | | `MissingHookResponse` | No return data or a different program supplied it | Set the final canonical reply from the configured hook | | `InvalidReplyPhase` / `InvalidFeeResponse` | Reply mismatches phase, profile, or required fee presence | Validate `HookReply` against configured capabilities | | `FeeCapExceeded` | Dynamic reply exceeds immutable pool cap | Correct the schedule/reply; do not silently clamp unauthorized policy | | Custom error `7000..7009` | Lifecycle probe intentionally rejects phase `error - 7000` | Use empty payload for success; keep phase bytes for rollback tests | Core errors and example-program errors are separate namespaces. Interpret names and logs in the context of the program that failed; the same numeric Anchor offset is not a global error identity. See [core errors](https://docs.grapl.io/reference/errors). ## A callback never appears Confirm its bit is enabled and the operation actually has that callback. Opening/closing a position and collecting fees have none. Before hooks also do not run if account or argument validation fails earlier. A zero liquidity delta rejects rather than becoming a fee-claim callback. For initialization, do not expect an empty/unassigned pool snapshot. Both initialize callbacks see the serialized candidate pool. A policy that can be configured only after creation is not available during that bootstrap. ## A valid proof works for one account but fails for another Recompute the full domain and authenticated actor. An allowlist leaf is bound to actor, expiry, pool, mint, core protocol, root version, and configured chain namespace. Matching the sibling list alone is insufficient. Use [leaf and proof construction](https://docs.grapl.io/hooks/allowlist#2-construct-the-leaf-and-proof) to isolate byte encoding from runtime identity. ## An after hook fails after token transfers This still rejects the transaction. Verify committed state and token balances rather than treating an intermediate log as settlement. The payer fee can remain charged. Compare all affected accounts and identify the exact rejecting phase. Preserve authentication and reply checks while correcting the underlying policy or account mismatch. ## A working native swap is rejected by Metis Check that a matching `HookQuoteAdapter` is bound, all declared policy accounts and a coherent Clock snapshot were refreshed, and required actor/payload context is present. The external evaluator may also be rejecting the policy itself, such as a closed window or expired proof. Continue with [Metis compatibility](https://docs.grapl.io/hooks/metis); do not suppress rejection or quote a different policy. # Confidential Balances research Current official network status, visibility boundaries, and future integration requirements. Source: https://docs.grapl.io/integrations/confidential-balances Confidential settlement is explicitly deferred. The current expansion adds factory, curve, swap modes, and zaps using classic SPL reserves. This page preserves research and does not advertise confidential AMM support. **Research checked 2026-10-08. Unsupported by grapl.** The current program accepts classic SPL Token accounts only. A Token-2022 Cargo feature used by Anchor macros does not enable Token-2022 settlement, confidential transfers, or hidden AMM state. ## Current official status The current Solana technical guide explicitly says the ZK ElGamal Proof Program is enabled on mainnet and devnet. It also distinguishes local runtimes: a stock `solana-test-validator` does not enable it. The Token-2022 status page says the latest program, including confidential transfers, is deployed on all clusters and requires that proof program. These are current technical documentation statements, rather than an inference from an older launch announcement. [Solana runtime requirements](https://solana.com/docs/tokens/extensions/confidential-transfer/apply-pending-balance), [Token-2022 project status](https://www.solana-program.com/docs/token-2022/status). This is a dated documentation finding. No confidential transaction, mainnet deployment, live feature-gate inspection, or grapl compatibility test was performed for this page. Before implementing against a cluster, verify its runtime/program state and execute the exact proof/transfer flow on an authorized isolated environment. ## Names and responsibilities **Confidential Balances** is the feature name in current documentation; `ConfidentialTransferMint` and `ConfidentialTransferAccount` remain extension/state names. The Token-2022 program applies token state transitions. The native ZK ElGamal Proof Program verifies proofs concerning encrypted values and commitments; it is not a generic private AMM executor. [Solana overview](https://solana.com/docs/tokens/extensions/confidential-transfer), [Anza proof-program reference](https://docs.anza.xyz/runtime/zk-elgamal-proof). | Layer | What it establishes | What it does not establish | | --------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------- | | Confidential transfer | Correct encrypted token movement with the required proofs | Anonymous participants or hidden transaction participation | | Token account | Public, encrypted pending, and encrypted available balances | Immediate spendability of pending credits | | Optional mint auditor | Ability to decrypt transfer amounts under its key | Spending authority or full account-balance disclosure | | grapl CLMM | Public price, ticks, liquidity, positions, and ordinary token settlement | Encrypted reserves, private ranges, or a confidential quote path | Token account addresses, mints, owners, and participation remain public. Encrypting token amounts does not automatically hide the application's other state. [Solana overview](https://solana.com/docs/tokens/extensions/confidential-transfer). ## The token flow The ordinary confidential flow configures the mint and accounts, deposits public tokens into pending balance, applies pending credits, transfers from available balance, applies the recipient's credits, and optionally withdraws to public balance. Pending credits cannot be spent directly. [Token-2022 usage](https://www.solana-program.com/docs/confidential-balances). Proof generation requires client-side secret material. Verification may use inline proof instructions or verified context-state accounts; the exact transaction plan depends on supported clients, format, and budgets. Deposits and withdrawals expose amounts. Confidential mint/burn changes the boundary further by disabling the public deposit/withdraw path. [Integration guide](https://solana.com/docs/tokens/extensions/confidential-transfer/integration-guide). ## Compatibility with the current pool All Token-2022 accounts are outside the present settlement contract, including their public balances. Withdrawing a confidential balance to a public Token-2022 balance does **not** make that account a classic SPL account. An extension-enabled mint cannot be treated as a different token program by changing a client flag. Even a future public Token-2022 settlement path would expose the traded amounts and CLMM state at the pool boundary. Hiding reserves, positions, or price transitions would require a different state/verification design and an explicit quote-availability model. That conclusion is an architectural inference from the current public state and the extension's documented scope, not an advertised grapl feature. ## Future adapter boundaries — design only The proposed separation is between custody/balance handling and transfer settlement. No adapter API or implementation is claimed here. | Responsibility | Required design | | -------------------- | ----------------------------------------------------------------------------------------------------- | | Custody and balances | Separate public, pending, available, withheld, and fee-debt amounts; define spendability | | Key control | Define who can decrypt and produce proofs; a PDA signature alone is not a confidential decryption key | | Transfer adapter | Bind mint/program, source, destination, amount/commitment, proofs, and authorization | | Proof lifecycle | Validate context ownership and operation binding, freshness, cleanup, replay behavior, and budgets | | Public AMM boundary | Explicitly state what becomes visible and require compatible token settlement | | Quote adapter | Preserve deterministic executable pricing; reject unknown balances/policies instead of guessing | | Accounting | Reconcile actual settlement with LP principal, fees, beneficiaries, and failed-transaction rollback | Custody providers and wallet clients need an explicit key recovery and authorization model. Do not store decryption keys or derivation signatures in logs, fixtures, or documentation. The issuer's auditor policy is a separate trust decision. [Issuer guide](https://solana.com/docs/tokens/extensions/confidential-transfer/issuer-guide). ## Verification gates Any future support needs isolated proof lifecycle tests, public/confidential boundary tests, token-extension composition tests, solvency and rollback checks, and quote/execution parity for every advertised path. Network availability and an upstream audit do not establish any of those properties for grapl. # DEX discovery and indexing Build a grapl pool and trade catalog for independent analytics platforms, including DEX Screener and GMGN integrations. Source: https://docs.grapl.io/integrations/discovery An analytics platform needs a decoder for the grapl program and its deployment. Solana provides account scans, logs and transaction subscriptions; it does not define one shared pool layout for every AMM. Discovering a token mint, indexing its grapl pool, and offering an executable trade route are separate capabilities. ## Bootstrap an independent integration 1. Pin the deployment's genesis hash and program ID. The current implementation and fixtures support development/Sandbox verification; this guide does not announce a mainnet deployment. 2. Scan finalized Pool6 accounts using [SDK discovery](https://docs.grapl.io/developers/discovery). Verify the owner, discriminator, PDA and bump rather than trusting a pool address supplied by a third party. 3. Store pool identity, mint/vault bindings, mode, configuration and hook program. Fetch SPL mint decimals and token metadata separately; metadata is not proof of authenticity. 4. Observe program activity, fetch successful finalized receipts and decode [core events](https://docs.grapl.io/reference/events). Include CPI invocations; a router transaction is not necessarily a top-level grapl instruction. 5. Recover missed history after reconnects, deduplicate by signature/log index, and rescan current pool accounts periodically. Mark history gaps explicitly. ## Indexer storage and recovery The grapl indexer runs separately from the API. `logsSubscribe` wakes it up, while polling and paginated `getSignaturesForAddress` recover retained history. Each receipt, checkpoint, transaction-status observation and protocol-event batch is persisted atomically. | Storage | Purpose | | ------------------------ | ---------------------------------------------------------------------------------------------- | | `indexer_pool` | Latest validated account snapshot, scoped by genesis/program and address | | `indexer_transaction` | Retained raw receipt, including failed transactions | | `indexer_event_batch` | Decode status and reason for each receipt; gaps remain visible | | `indexer_protocol_event` | Typed events keyed by stream, signature and original log index; indexed by pool and event name | Wide values in JSON storage are decimal strings. Pools can be discovered even when historical RPC retention is insufficient. That current catalog does not imply complete volume, price candles, user activity or APY history. The indexer's event replay utility reprocesses retained receipts with the pinned decoder without moving the chain cursor. It is idempotent. Replay cannot recover logs absent from the retained receipt, nor manufacture events that an older program never emitted. A provider retaining the missing receipts is needed to repair that coverage. The [public SSE API](https://docs.grapl.io/developers/api/events) currently streams transaction status. It does not expose the internal typed swap/liquidity tables as a public pool-event stream. ## Prepare the integration handoff For DEX Screener, GMGN or another indexer, provide the deployment identity, program source and pinned IDL; Pool6 filters and PDA seeds; event schema and settled-amount semantics; mint/vault ordering; curve graduation behavior; and representative successful/failed direct and CPI receipts. Include the decoder's truncation and replay behavior. Executable routing additionally requires hook-aware quotes and execution support; discovery alone does not bypass hook rules. [DEX Screener describes automatic token listing](https://docs.dexscreener.com/token-listing) once its platform tracks a liquidity pool and a transaction. This is not a promise to decode every new Solana AMM. [GMGN's contact page](https://docs.gmgn.ai/index/contact-us) is the coordination entry point for discussing a new integration. Each platform must confirm support for the grapl deployment and format. These local implementation tests do not establish indexing, listing, or trading support on either platform. See [Metis integration](https://docs.grapl.io/integrations/metis) for the independent quote/execution path. # Jupiter and Metis Use native CLMM quotes and external hook adapters without assuming public router admission. Source: https://docs.grapl.io/integrations/metis The Metis adapter connects pool state to deterministic quotes and instruction construction. The optional `grapl_amm::metis` module exports `GraplAmm`, an implementation of the official `Amm` trait with native account decoding, core traversal, and native swap encoding. Hook support uses an injected external `HookQuoteAdapter`; policy implementations remain outside the generic library. No public Jupiter route, indexed pool, submission, or admission is claimed. ## Update before quoting `from_keyed_account` validates a native pool account. For a hooked pool, bind a compatible adapter with `with_hook_adapter` before collecting dependencies. `get_accounts_to_update` identifies pool, tick book, both mints, both vaults, and the configured hook's program and default account plan when applicable. The host supplies them through `AccountProvider`; a failed refresh invalidates previous quote state. `update_with_clock` accepts an explicit coherent Clock snapshot. Standard `update` captures the injected Jupiter ClockRef, whose independent atomic reads do not themselves guarantee bank coherence. The host is responsible for obtaining matching account and time snapshots. `quote` performs no account fetching or live-clock reads. It evaluates supported BeforeSwap logic, runs the core's CLMM traversal or virtual-reserve curve calculation, then evaluates supported AfterSwap logic on the resulting state. Replies enforce the configured profile and fee cap. ExactIn and ExactOut with `FeeMode::Normal` have native quote/encoding paths; ExactOut targets net output, and ExactIn requires a complete fill; partial consumption, output shortage, input-vault overflow, and invalid dependencies reject. For actor-specific accounts, call `get_accounts_to_update_with_context`, fetch the returned dependencies, then use `prepare_context(provider, clock, &context)` to obtain an isolated refreshed clone. Failed preparation leaves the original instance unchanged. The external adapter supplies the exact `HookAccountRequest` plan; the generic loader validates it through core permissions. Account bounds are 16 extras and eight writes per callback, with the default limited to four read-only policy PDAs. ## Choose the required context | Pool | Adapter/context | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | No hook | No adapter; zero callback bits, PolicyOnly, System Program hook sentinel | | Public time/fee example | Matching external adapter plus validated policy and Clock snapshot | | Actor-dependent example | Matching adapter plus `quote_with_context` with claimed actor and opaque payload | | Stateful hook | Matching effect evaluator, complete intent when required, contextual account plan, and coherent state | | Unsupported hook/profile/callback combination | Explicit rejection until a compatible implementation is supplied | Ordinary `Amm::quote` supplies empty context. It rejects an adapter requiring an actor or complete swap details. `HookSwapContext` contains actor, payload, optional `HookSwapDetails`, and ordered `HookAccountRequest` entries. The three economic examples share their actual policy evaluator with their off-chain adapter in `examples/hooks/shared/src/metis.rs`; they are optional examples, not native protocol policies. See [Hooks and Metis compatibility](https://docs.grapl.io/hooks/metis) for the complete authoring and construction flow. Both mints and vaults must use supported classic SPL Token semantics. Token-2022 pool reserves, referral context, distinct user/transfer authority, and incomplete dynamic-account discovery remain unsupported. `SingleRangeQuoter` is a separate, narrower caller-supplied snapshot contract; its one-segment/no-hook limit does not describe `GraplAmm`. `quote_effects_with_context` returns the AMM quote and simulated changes to existing account bytes. Each effect needs write permission in that callback; account creation, resizing, owner/lamport changes, executable writes, and protected aliases reject. Before-phase changes feed AfterSwap. The original snapshot is unchanged. Reward balances and counters belong in these separate effects, never in the AMM output amount. ## Construct the native swap `get_swap_and_account_metas` builds with the swap user and an empty payload. For contextual policies, use `swap_with_context` and preserve the quoted actor, payload, execution details, and account plan. The builder returns `Swap::Placeholder` and ordered native account metas, with exact non-signer hook extras and their requested write privileges appended. For ExactIn, `encode_native_swap` preserves explicit input, minimum output, and hook payload. ExactOut uses `encode_native_swap_exact_out` with the matching decision, target net output, and maximum input. The builder requires `user == token_transfer_authority` and binds any explicit context actor to that user. Runtime execution still requires signer privilege and ownership of the actor token accounts. A claimed quote actor, fee payer, or AccountMeta flag is not authenticated wallet authorization. Routed PDA authority needs its own identity contract. A quote is a snapshot result, not a reservation. Refresh for new policy/time state, and allow execution to reject if a time window closes or proof expires. The adapter does not silently ignore a policy to preserve an old quote. ## Compare quote and settlement Use coherent snapshots and compare actual input/output deltas with the quote in both directions and swap modes. Include policy rejection, output fees, stale state, and insufficient account coverage. [Quote consistency](https://docs.grapl.io/integrations/router-verification) describes the acceptance criteria. ## Upstream contract and remaining work The compiled interface is pinned to [Jupiter AMM Interface revision `95bd18485e580625a39eb92a0c0ad2061579daad`](https://github.com/jup-ag/jupiter-amm-interface/tree/95bd18485e580625a39eb92a0c0ad2061579daad). `QuoteParams` has no wallet, signature, arbitrary proof payload, or timestamp field. grapl's contextual methods supplement that local API; they do not modify Jupiter's public quote request or register a router destination. Public routing needs an accepted destination/encoder, actual route/CPI tests, account and compute-budget validation, and the separate [Jupiter integration review](https://developers.jup.ag/docs/swap/routing/amm/integration). Local trait implementation and SBF parity do not complete those stages. The source-level external adapter contract is `integrations/metis/HOOK_API.md`. # Quote and execution consistency Keep amounts, policy context, and account construction aligned across routing. Source: https://docs.grapl.io/integrations/router-verification A router quote and its eventual transaction must describe the same operation. Shared pricing math helps, but consistency also depends on snapshot freshness, policy evaluation, identity, account order, and token settlement. ## Freeze the inputs to the quote Use one coherent pool, tick, and policy snapshot. Bind the direction, swap mode, amount, Clock, limits, and any explicit actor/proof context. When refreshing the pool, refresh the dependent accounts required by its hook and traversal. A quoted actor is not signature evidence. The actual operation authority is established during execution and may be a router PDA. See [wallet context](https://docs.grapl.io/integrations/wallet-context). ## Compare the right amounts | Mode | Quote obligation | Execution protection | | -------- | ------------------------------------------------ | ---------------------------------- | | ExactIn | Actual consumed input and net received output | Minimum net output and price limit | | ExactOut | Input needed to deliver the requested net output | Maximum input and price limit | Include both the input trade fee and any configured output fee. Treat partial fills explicitly. Compare actual token debits and credits, rather than only an internal pricing return value. ## Preserve hook behavior The injected evaluator must reproduce the supported hook's decisions. Missing accounts, incompatible policy versions, unavailable identity context, and unsupported effects reject. The core library must never substitute a permissive no-hook quote for a hooked pool. Sponsored reward quotes can evaluate public pricing without predicting wallet-specific reward bytes. Execution still binds recipient accounts and requires sponsor funding. Prepared or membership-bound policies need their required context. Read [hook compatibility](https://docs.grapl.io/hooks/metis). ## Construct the same account contract Supply canonical visited tick pages and hook extras in the expected order, with exact signer and writable privileges. A router cannot manufacture a wallet signature or escalate a read-only outer account. The core rebuilds callback privileges within the pool's ceiling. Measure the full transaction: tick pages, proof siblings, router accounts, external programs, and optional account creation all consume the available envelope. Address lookup tables can reduce address overhead; they do not compress proof bytes. ## Observe confirmed settlement Check transaction success and reconcile reserve deltas plus required hook effects. A failed after callback rolls back settlement. The core currently has no typed Anchor event schema, so callback logs alone are not a stable settlement feed. The adapter contract does not establish public Jupiter routing availability. Public routing requires the provider's integration, discovery, account-packing, and admission process. See [Metis integration](https://docs.grapl.io/integrations/metis). # Handle token amounts Use exact token amounts and distinguish utility functions from transaction builders. Source: https://docs.grapl.io/integrations/sdk The source SDK `@grapl/sdk` exports exact amount conversion utilities alongside its direct transaction client. The package is not an announced npm release. Follow the [Getting started](https://docs.grapl.io/developers/sdk) to link the source package. ## Amount API | Export | Signature and behavior | | ------------------- | ---------------------------------------------------------------------------- | | `TOKEN_AMOUNT_MAX` | `bigint`, equal to `2^64 - 1` | | `assertTokenAmount` | `(amount: bigint): void`; requires `0..u64::MAX` | | `parseTokenAmount` | `(text: string, decimals: number): bigint`; parses unsigned decimal text | | `parseSwapAmount` | Same arguments; additionally rejects zero | | `formatTokenAmount` | `(amount: bigint, decimals: number): string`; exact canonical decimal output | The `number` parameter is mint-decimal metadata, not a financial amount. Decimal metadata must be an integer in `0..255`. ## Convert a user amount The SDK exposes exact parsing and formatting: ```typescript import { parseTokenAmount, formatTokenAmount } from "@grapl/sdk"; const atoms: bigint = parseTokenAmount("1.25", 6); const display: string = formatTokenAmount(atoms, 6); ``` Here `atoms` is `1250000n` and `display` is `"1.25"`. The import resolves to the source package linked in your application. The parser rejects negative signs, exponent notation, leading-zero whole numbers other than zero, excessive fractional precision, and values above `u64::MAX`. It does not round invalid user input into a valid amount. ## Construct instructions separately These utilities only parse amounts. Use the separate [direct client methods](https://docs.grapl.io/developers/sdk) to prepare transactions, the [IDL and instruction contract](https://docs.grapl.io/reference/instructions) for account order, and [wallet context](https://docs.grapl.io/integrations/wallet-context) for authorization. # Token programs and extensions Compatibility is a policy for both mints, not a consequence of being an SPL token. Source: https://docs.grapl.io/integrations/token-programs **Design.** Validate both mint accounts and their owning token programs. A token launched by the same platform is not automatically compatible or liquid. Check the current implementation before enabling Token-2022 extensions. ## SOL is represented by a token account The architecture uses WSOL in the core. Wrapping and unwrapping native SOL belong to a client or router flow. No native-lamport swap entry point is implied. See [Solana's Sync Native guide](https://solana.com/docs/tokens/basics/sync-native). ## Separate reward tokens from pool assets Pool settlement currently accepts classic SPL Token accounts. An external hook may separately declare Token-2022 state and program access for a reward mint it controls. The [RewardCredits example](https://docs.grapl.io/hooks/rewards) uses that distinct CPI boundary; its mint is not an additional AMM reserve or evidence that Token-2022 swaps, LP deposits, or fee claims are supported. The core's protected settlement accounts cannot be supplied as reward extras. ## Issuer freeze authority Classic SPL mints may retain an issuer freeze authority. Pool creation does not require the issuer to revoke it and does not alter the mint. The SDK and Metis adapter use the same eligibility policy. A freeze authority on the mint is separate from a frozen token account: SPL Token still rejects transfers involving frozen source or destination accounts. A failed instruction rolls back the transaction's token movements and grapl accounting; network fees remain payable. An issuer can freeze a pool vault as well as a user's token account. grapl cannot bypass that decision. Metis rejects a refreshed snapshot containing a frozen vault; a freeze after quotation can still cause execution to fail. ### USDC, USDG and WSOL A read-only mainnet snapshot at slot `456394876` recorded these profiles: | Asset | Mint program | Current core status | | --- | --- | --- | | USDC | Classic SPL Token, six decimals, issuer freeze authority | Unchanged official mint profile tested locally through pool creation, liquidity, swap and fees with WSOL | | WSOL | Classic SPL native mint, nine decimals | Real local wrapping, backing lamports and token conservation verified; wrapping remains explicit | | USDG | Token-2022, six decimals, issuer-controlled extensions | Unsupported by the classic CLMM/SDK; the separate experimental settlement passed local replay with its observed extension profile | USDG's observed mint includes a permanent delegate, mint close authority, zero transfer fees, a disabled transfer hook, confidential transfer configuration and metadata. Removing the freeze-authority check alone does not support this profile. Extensions must be validated at execution, since their authorities may change them. This does not enable confidential settlement or modify the original tokens. Sources: [Circle USDC](https://www.circle.com/multi-chain-usdc/solana), [Paxos USDG](https://docs.paxos.com/guides/stablecoin/usdg/quickstart), and [Solana payment mint addresses](https://solana.com/docs/payments/how-payments-work). ## Extension checklist | Capability | Required integration work | | --------------------------------- | -------------------------------------------------------------------------- | | Transfer fees | Distinguish gross debit, received input, net output, and withheld balances | | Transfer hooks | Resolve extra accounts for both directions and evaluate CPI composition | | Frozen or default-frozen accounts | Define operational eligibility and explicit failure behavior | | Confidential balances | Define a supported accounting path before acceptance | | Other or unknown extensions | Reject unless a validated policy allows them | This is a requirements table, not a supported-extension list. Extension support must be tested for swaps, liquidity, fee collection, claims, and graduation, not only pool creation. ## Transfer hooks are mint policy Token-2022 calls the mint's hook through its transfer interface. That callback observes a transfer context, not a grapl swap lifecycle context. The token program restricts the privileges of transfer accounts passed into the hook; do not expect the user's signer to propagate unchanged. [Transfer Hook guide](https://solana.com/docs/tokens/extensions/transfer-hook). Pool and token hooks may coexist only when their required accounts, invocation graph, and budgets are supported. Neither hook may silently bypass the other's policy. ## Balances must be spendable Do not count withheld transfer fees as usable reserves. A quote must account for supported transfer costs at both ends. The amount credited to a beneficiary and the amount arriving in their wallet can differ when a mint charges transfer fees; document that distinction explicitly. # Wallet identity and authorization A trader, transfer authority, and final recipient are different identities. Source: https://docs.grapl.io/integrations/wallet-context **Design contract.** The core must pass authenticated operation context to a pool hook. Identity in a frontend payload is not sufficient. Actual core enforcement is described in the [instruction reference](https://docs.grapl.io/reference/instructions). ## Identify each role | Role | Meaning | | ------------------ | ------------------------------------------------------------- | | Trader | Wallet whose authorization is verified for this operation | | Transfer authority | Owner, delegate, or PDA permitted to spend the input account | | Source owner | Owner of the local hop's input token account | | Destination owner | Owner of the local hop's output account | | Final beneficiary | End recipient, only known if the route explicitly binds it | | Fee payer | Account paying transaction fees; not automatically the trader | ## Direct execution The simple path requires the trader as a signer, validates authority over the actual source, and binds the market, mints, destination, amount, and user limits. The runtime enforces signer privileges. The core authenticates its callback separately so a policy cannot be invoked with fabricated core context. ## Routed execution A router may hold intermediate tokens or invoke the pool through a PDA. The input owner is then not necessarily the original wallet. The router must preserve the expected user signer or support a separately verified authorization with nonce, expiry, assets, market, limits, and beneficiary. An authenticated signer at one hop does not prove who receives the final route output. Holder-based or recipient-based policies require the appropriate route binding. ## Quote is not eligibility An anonymous quote can describe pool math while the eventual wallet fails an allowlist. A wallet-specific fee changes the quote itself and needs contextual quote support. Do not treat `SwapParams.user`, hook bytes, transaction fee payer, or a router address as proof of authorization. The [Solana CPI documentation](https://solana.com/docs/core/cpi) explains how signer and writable privileges pass between programs. grapl still needs explicit validation of what those privileges mean for each operation. # Fees and revenue Follow the mandatory protocol share, LP and beneficiary allocations, and authorized collection in the assets actually earned. Source: https://docs.grapl.io/protocol/fees-and-revenue The current source increment reserves **10% of the input trading fee for the protocol**, then distributes the remainder between LPs and configured beneficiaries. This is part of the fee already charged to the trader, not an additional 10% charge on the trade amount. **Validation status:** the Pool 6 revenue implementation passed isolated local integration verification, including authenticated treasury initialization, claims in both pool assets, repeated collection without double payment, and rollback. These results do not establish a public deployment or a completed audit. ## Follow the fee Let `F` be the actual input trading fee, in token atoms. The protocol rate is fixed at `1,000 / 10,000` basis points. Fractional carry is kept separately for each pool and input mint: ```text numerator = F × 1,000 + previous carry protocol retention P = floor(numerator / 10,000) new carry = numerator mod 10,000 remainder R = F − P R = LP allocation + sum of beneficiary allocations ``` The carry prevents splitting a fee across small events from avoiding the protocol share. Ten one-atom fees retain one protocol atom in total, just as one ten-atom fee does. Collection does not clear that fractional carry. The existing LP and beneficiary shares sum to 10,000 and apply to `R`. Integer split residue goes to the first nonzero configured share, checking LPs first. Total liabilities must conserve the fee: protocol retention plus every remainder allocation equals `F`. A dynamic-fee hook changes `F` within the pool's cap. It does not change the fixed protocol percentage or the configured remainder split. Zero trading fee produces no additional protocol atoms. ## A six-decimal USDC example For a 1,000 USDC swap with a total 0.3% trading fee, `F` is 3 USDC. Protocol retention is 0.30 USDC, leaving 2.70 USDC. With an 80% LP, 15% creator, and 5% partner remainder split: | Allocation | USDC | Atoms at six decimals | | ------------------ | -------- | --------------------- | | Total trading fee | 3.000000 | 3000000 | | Protocol retention | 0.300000 | 300000 | | LP allocation | 2.160000 | 2160000 | | Creator allocation | 0.405000 | 405000 | | Partner allocation | 0.135000 | 135000 | The four allocations sum to 3 USDC. All configured recipients, including LPs, participate in the proportional reduction. The 0.3% trading fee and 80/15/5 split are illustrative; the source increment's protocol retention is fixed at 10%. ## Configure the remainder when creating a pool `PoolConfiguration` contains `lp_share_bps` and up to four beneficiaries with a `recipient` and `share_bps`. Active recipients must be unique and nonzero; empty slots have zero share. The total is 10,000 basis points of the amount remaining after protocol retention. These weights and recipient addresses are immutable under the current instruction set. The SDK's `defaultPoolConfiguration()` uses `lpShareBps: 10000` and four empty slots: LPs receive all of the remainder, while mandatory protocol retention still applies. The protocol share is enforced in core fee accounting. Calling the core directly, omitting a hook, or choosing another SDK configuration does not opt a pool out. The protocol does not consume one of the four beneficiary slots. ## Keep claims separate | Balance or cost | Treatment | | ------------------------- | ----------------------------------------------------------------------------------- | | `protocol_owed_a/b` | Protocol claim for the corresponding pool asset | | LP allocation | Position fee growth on traded intervals, or retained LP fees during curve mode | | `beneficiary_owed_a/b` | Claims belonging to the pool's configured recipients | | Optional hook output fee | Separate capped deduction in the output asset; outside this protocol-retention base | | Network and priority fees | SOL costs for execution; outside pool trading revenue | | Rent-exempt deposit | SOL backing account storage; outside trading revenue | Donations accrue directly to active LP fee growth. They do not pass through the protocol trading-fee retention or beneficiary split. During curve mode, protocol and beneficiary claims remain reserved and are excluded from graduation funding. ## Initialize the treasury recipient `ProtocolTreasury` is a core PDA derived with `b"protocol_treasury"`. `initialize_protocol_treasury(recipient)` creates it once, rejects a zero recipient, and requires the deployed program's upgrade authority authenticated through its ProgramData account. The initializer also funds this account's deposit. The current instruction set has no recipient setter and no runtime setter for the fixed 10% rate. The initializer establishes the authorized claim recipient; it does not turn fee retention on or off. Swaps accrue liabilities in their own pool and do not need a shared writable treasury account. No production recipient address is published here. Applications must derive and read the treasury for their verified deployment. Treasury initialization and collection are explicit on-chain operations with their own authorization. ## Collect protocol or beneficiary revenue `collect_protocol_fees` operates on one pool and requires the market actor's signature to match the configured treasury recipient. Destination token accounts must belong to that actor and match the pool mints. It clears that pool's protocol owed amounts and transfers both assets atomically. The fractional carry survives collection. Beneficiary collection is separate. `collect_beneficiary_fees` checks the selected slot and requires the signed actor to equal its recipient. It clears and transfers only that recipient's debt. Collecting a beneficiary does not claim protocol revenue or distribute every other recipient's balance. A failed transfer rolls back claim clearing and transfers together. After confirmation, reconcile the receipt and actual token credits before reporting collected revenue. An owed amount is not evidence that a wallet has already received funds. The treasury increment emits `ProtocolTreasuryInitialized` and `ProtocolFeesCollected` events. These specific events do not establish a complete normalized observer API for every protocol operation. ## Revenue stays in the collected assets Trading with input A produces its trading fee in A; input B produces it in B. A recipient can therefore accrue both assets across pools. There is no automatic conversion into USDC, SOL, or a reporting currency. Keep accrued token amounts, claimed token amounts, estimated valuations, and proceeds from a later conversion distinct. Conversion is another trade with its own fees, price impact, limits, and authorization. An estimated USDC value is not a USDC balance. ## Referral and future products A fixed partner beneficiary is not a dynamic referral recipient selected per swap. The current increment does not implement referral deductions from the protocol share. DCA, native limit orders, and managed market-making products remain separate research directions. [Meteora's revenue guide](https://docs.meteora.ag/protocol/protocol-revenues) informs the distinction between the trading-fee base, protocol revenue, and collection in traded assets. Its product-specific rates and referral mechanisms are not grapl behavior. **Implementation sources:** `instructions/fees/{protocol,accounting,treasury,donate}.rs`, `state/treasury.rs`, `instructions/curve/validation.rs`, and `settlement/accounts.rs` under `programs/grapl-amm/src/`. For user-facing costs, see [Fees and account deposits](https://docs.grapl.io/user-guides/fees). # Accounts and PDA seeds Current Anchor account identities and the limits of the initial storage model. Source: https://docs.grapl.io/reference/accounts **Source inspected:** `programs/grapl-amm/src/state/{mod,ticks}.rs`, `instructions/pool/initialize.rs`, `instructions/positions/lifecycle.rs`, and `hooks/abi/wire.rs`. These are implementation details under development, not a stable deployed ABI. ## Current addresses Seeds are listed in order. Numeric identifiers use little-endian bytes; pubkeys use their raw 32 bytes. Derive the bump with the program ID from your deployment configuration. | Account | Seeds | Deriving program | | -------------------------------- | ------------------------------------------------- | ---------------- | | Factory mint | `b"mint"`, creator, `token_id: u64 LE` | Core | | Initial issuance token account | `b"issuance"`, mint | Core | | Graduation position | `b"graduation"`, pool | Core | | ProtocolTreasury | `b"protocol_treasury"` | Core | | Pool | `b"pool"`, creator, `pool_id: u64 LE` | Core | | Vault A/B | `b"vault"`, pool, mint | Core | | TickDirectory | `b"ticks"`, pool | Core | | Tick array | `b"tick_array"`, pool, `array_index: i32 LE` | Core | | Position | `b"position"`, pool, owner, `position_id: u64 LE` | Core | | Hook callback authority | `b"hook"`, pool | Core | | Hook policy account at index `i` | `b"policy"`, pool, one-byte index | Configured hook | The current pool seed does not include the token pair. The implementation identifies it by creator and pool ID; the architectural pair/salt identity is not the current seed scheme. ## State responsibilities | Account | Selected fields | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Pool` | Mints, vaults, creator, configuration, hook program, Q64.64 price, tick, active liquidity, fee growth, beneficiary debts, protocol owed amounts and per-mint fractional carry, sequence, entry flag, curve state; Pool version 6 reserves 1160 bytes | | `ProtocolTreasury` | Bump and immutable claim recipient; 41 bytes including discriminator | | `Position` | Pool, owner, ID, ticks, liquidity, inside-growth checkpoints, fees owed | | `TickDirectory` | Pool, spacing, bump, two-level bitmap; 1816 bytes, borrowed through AccountLoader | | Tick array | Canonical page header plus fixed or compact boundary records | | `TickBoundary` | Signed index, gross/net liquidity, outside fee growth for A/B | Each page represents **64 aligned ticks**. Only initialized boundaries occupy compact records; fixed pages reserve all slots. The directory covers the entire supported price domain. A swap accepts at most 16 page accounts, and transaction bytes/CUs can impose a smaller practical budget. See [tick arrays and full range](https://docs.grapl.io/concepts/tick-arrays). ## Authority and token accounts The treasury initializer authenticates the deployed program's upgrade authority through ProgramData. Protocol collection requires the configured recipient's signature and recipient-owned token accounts. Fees accrue per pool; the treasury record does not become a shared writable account on each swap. See [Fees and revenue](https://docs.grapl.io/protocol/fees-and-revenue). Vaults use the pool PDA as token authority. The callback PDA owns no vault. It is the only core signer forwarded to the hook; the pool is read-only. Extra accounts need explicit `HookAccountAccess` descriptors and configured per-phase permissions. Defaults allow four read-only hook-owned extras; configured maxima are 16 extras and eight writable extras per callback. `PolicyPda` indices 0–3 use the table above. `HookState` has hook-defined address/discriminator relationships. ExternalState owners and ExternalProgram IDs must be declared; external programs remain read-only. Signer extras, duplicates, core-owned extras, and aliases of protected market/actor/settlement accounts reject. See [permissions](https://docs.grapl.io/hooks/permissions). The initial core uses classic SPL Token accounts. Initialization permits issuer freeze authority; frozen token accounts cannot settle transfers. See [token compatibility](https://docs.grapl.io/integrations/token-programs). ## Program identity Obtain the core program ID from the deployment configuration used by your application and verify the account owner against it. These docs do not publish a mainnet address registry. # Fee analytics API Optional host-only valuation, observed APR, and projected APY functions. Source: https://docs.grapl.io/reference/analytics **Implemented:** `integrations/analytics/{apr,projection,valuation,errors}.rs`, exported by `grapl_amm::analytics` under Cargo feature `analytics`. These are pure off-chain calculations. Do not enable this feature for the deployable program. ## Units and rounding Values use microdollars (`usd_e6`); rates use parts per billion (`ppb`). `100000000` ppb means 10%, not 100%. Annualization uses 31,536,000 seconds. Integer divisions round down, and width errors reject. ## Value tokens ```rust usd_value_e6(amount: u64, decimals: u8, price_usd_e6: u128) -> AnalyticsResult ``` `price_usd_e6` is the external USD price per whole token. The function scales base units and accepts decimals through 38 because its decimal scale must fit `u128`. The caller provides price provenance, time consistency, freshness, and manipulation checks. This function is not an oracle. ## Annualize earned LP fees ```rust observed_fee_apr(observation: FeeObservation<'_>) -> AnalyticsResult ``` `FeeObservation` contains `start`, `end`, `earned_fees_usd_e6`, and a borrowed slice of `CapitalInterval`. Each interval has `start`, `end`, and `capital_usd_e6`. Intervals must be increasing, contiguous, and fully cover the nonempty observation window. Integrated capital must be positive. The result is `fees × year_seconds × RATE_SCALE / sum(capital × duration)`, rounded down. Deposits and withdrawals alter capital exposure. Collections move an earned claim and must not be counted again as income. Fee APR uses the LP's share, excluding protocol and creator revenue. ## Project reinvestment ```rust projected_fee_apy(apr_ppb: u128, periods: u16, cost_per_period_ppb: u128) -> AnalyticsResult ``` Supported periods are `1..=365`. Cost is proportional to then-current capital per period. The result is signed and may be negative. A cost above the gross period capital factor rejects. Compounding applies the implementation's fixed-point downward rounding; a high-precision illustration can differ by small integer residues. For example, 10% APR with two reinvestments and zero cost produces 10.25% projected APY. This excludes price changes, inventory P&L, divergence from holding, rewards, changing range activity, and changing liquidity share. No automatic reinvestment exists in the core. ## Present the result Label supplied observations, valuation currency, coverage, and compounding assumptions. A calculation over synthetic inputs is not a historical return. See [LP performance](https://docs.grapl.io/concepts/lp-performance) before displaying a rate. # Errors and rejection behavior Treat unsupported capabilities as explicit failures and inspect the offending input. Source: https://docs.grapl.io/reference/errors Use symbolic errors from the matching build. Numeric Anchor error assignments are not presented as a stable deployed API. ## Core validation | Error family | Check | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `InvalidMintOrder` | Distinct mint keys in canonical byte order | | `InvalidTickSpacing`, `InvalidRange`, `InvalidPrice` | Domain, alignment, endpoint order, fixed-point units | | `InvalidFee`, `InvalidFeeSplit` | Cap, denominators, unique beneficiaries, exact share sum | | `UnsupportedPoolMode`, `UnsupportedHook` | Whether the requested mode/profile exists | | `InvalidHookTarget`, `InvalidHookAccounts` | Configured executable, exact extra-account classes, owners, keys, and protected aliases | | `InvalidHookPermission`, `InvalidHookContext` | Per-phase privileges, declared programs, bounded accounts, and phase/intent/settlement consistency | | `MissingHookResponse` | Return data and actual producer program | | `PoolEntered` | Whether the operation attempts nested entry | | `PositionNotEmpty` | Remaining liquidity or fee claims before close | | `SlippageExceeded` | User limits against actual token amounts | | `InvalidTickDirectory`, `InvalidTickArray` | Canonical owner/PDA, spacing, layout, and ordered unique pages | | `MissingTickArray`, `TickMissing` | Required initialized page or boundary absent | | `TickArrayCapacity`, `TickArrayLimit`, `TickTraversalLimit` | Reserved record capacity, 16-page budget, and bounded traversal | | `ArithmeticFailure`, `MathFailure` | Width, overflow, underflow, and math-domain errors | Source: `programs/grapl-amm/src/errors.rs`. ## Hook replies Malformed tags, unsupported versions, wrong phases, denied permission, invalid fee shape, and cap violations are errors. A failure must not become an implicit acknowledgement or default fee. Source: `programs/grapl-amm/src/hooks/abi/wire.rs` and `reply.rs`. ## Client errors Amount utilities throw `RangeError` with the supplied value and expected shape. Validate decimal precision before serialization; do not cast a negative value into an unsigned representation. If a quote is possible but a wallet is ineligible, that is an execution-policy failure, not proof that the route is safe. See [troubleshooting](https://docs.grapl.io/guides/troubleshooting). ## Issuance, curves, and composition `InvalidTokenIssuance` rejects invalid factory supply or final authority. `InvalidCurve` rejects invalid reserve plans, insufficient real backing, premature or repeated graduation, and unauthorized remainder collection. Swap and zap limits use `SlippageExceeded`; ExactOut does not turn a failed exact target into a partial success. Account substitutions and hook denials still use the generic validation families above. # On-chain events Decode authenticated pool initialization, swaps, liquidity changes and fee collection from grapl transaction logs. Source: https://docs.grapl.io/reference/events The core emits Anchor `Program data:` logs after the operation's settlement and relevant after-hook have succeeded. The pinned IDL defines each event discriminator and Borsh payload. An event can still appear in a transaction that later fails: require a successful finalized receipt before applying it. ## Event catalog | Event | Meaning | Main fields | | ----------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `PoolInitialized` | A direct or curve pool was initialized | Context, creator, pool ID, mints, vaults, hook program, tick spacing, configured fee, mode and initial square-root price | | `SwapExecuted` | One underlying swap settled, including swaps inside zaps | Context, actor, actor token accounts, direction, ExactOut flag, effective fee, protocol fee, prices, current tick, active liquidity and outcome | | `LiquidityChanged` | Position liquidity changed | Context, actor, owner, position, bounds, signed delta, amounts, resulting position liquidity and graduation flag | | `PositionFeesCollected` | Accrued LP fees were paid | Pool, position, owner and actual amounts | | `ProtocolFeesCollected` | Protocol debt was paid to its recipient | Pool, recipient, mints and amounts | | `ProtocolTreasuryInitialized` | The protocol recipient was bound | Recipient | `PoolInitialized`, `SwapExecuted` and `LiquidityChanged` carry `context.version = 1`, the pool address and its operation sequence. Fee collection does not advance that sequence. Deduplicate **all** events using `(genesis hash, program ID, transaction signature, log index)`, not sequence alone. A curve emits `PoolInitialized` with `mode = 1`. Graduation emits `LiquidityChanged` with `graduation = true` and preserves the pool address. Its amounts are existing curve reserves allocated to the creator's CLMM position, not a new wallet deposit. Ordinary add/remove events have `graduation = false`; the sign of `liquidityDelta` distinguishes them. This catalog does not emit a separate event for every instruction. Position open/close, tick-array allocation, donations, beneficiary claims, and curve remainder claims still require instruction/account inspection. Do not infer that these operations did not happen from the absence of a typed event. ## Swap amounts and prices | Field | Unit and interpretation | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `outcome.amountIn` | Actual gross input debited, in input-mint atoms; includes trading fee | | `outcome.amountOut` | Actual output credited to the actor, in output-mint atoms; excludes the separate hook output fee | | `outcome.feeAmount` | Trading fee in input-mint atoms | | `protocolFeeAmount` | Protocol share of that trading fee, including fractional carry across swaps/steps; never an added trader charge | | `outcome.outputFeeAmount` | Separate hook output fee in output-mint atoms | | `outcome.amountUnspent` | Input budget left with the actor, including ExactOut maximum-input remainder | | `feePpm` | Effective trading fee for this swap, including a hook override | | `sqrtPriceBeforeX64`, `outcome.sqrtPriceAfterX64` | Raw B/A square-root prices in Q64 fixed point | `aToB = true` means input mint A and output mint B. Otherwise the direction reverses. Obtain the mints from the validated pool catalog. The human B-per-A spot price is `(sqrtPriceX64 / 2^64)^2 × 10^(decimalsA-decimalsB)`; use rational or decimal arithmetic rather than converting raw amounts to JavaScript `Number`. Execution price can differ because of fees and traversal. Events do not contain a USD price or trusted metadata. `actor` is the authority authenticated by the core. Under a router it can be a PDA or another intermediary. It is not an assertion of the transaction fee payer, original buyer wallet, or beneficial owner. ## Decode a finalized receipt ```typescript import { decodeTransactionEvents } from "@grapl/sdk"; import type { Address } from "@solana/kit"; export function readTrades( programAddress: Address, meta: { err: unknown; logMessages: readonly string[] | null }, ) { const batch = decodeTransactionEvents(programAddress, meta); if (batch.status !== "complete") return batch; return batch.events.filter(({ event }) => event.name === "SwapExecuted"); } ``` The decoder tracks the runtime invocation stack. It accepts core-owned data logs when the program is called directly or through CPI, ignores identical logs emitted by a hook, and drops events from failed nested invocations even when an outer router catches the error. Event decoding does not fetch or establish finality; the caller supplies the receipt from the expected chain at the required commitment. | Decode status | Indexer action | | ------------- | ---------------------------------------------------------------------------------------------- | | `complete` | Persist the decoded events, possibly an empty list for an instruction without an event | | `failed` | Preserve transaction failure; publish no successful protocol events | | `unavailable` | Missing metadata or logs; mark the coverage gap | | `incomplete` | Truncated logs or an unclosed invocation; apply no partial batch | | `unsupported` | Unknown/malformed event or invocation structure; preserve the receipt for investigation/replay | These are log events, not `emit_cpi!` events. Providers can truncate logs. Complete receipt logs, retained transaction history and an explicit coverage record are required for reliable analytics. Account rescans recover current discovery, not lost swap history. Older deployed binaries do not gain these events retroactively. # Glossary Precise meanings for the terms used throughout the protocol docs. Source: https://docs.grapl.io/reference/glossary | Term | Meaning | | ---------------- | -------------------------------------------------------------------------------------- | | Active liquidity | Sum of liquidity for positions covering the current price interval | | Base unit | Smallest integer unit of a mint, before decimal formatting | | Beneficiary | Configured recipient of a defined external fee share | | CLMM | Concentrated-liquidity automated market maker | | Collect | Withdraw accrued fees without increasing position liquidity | | Compound | Explicitly reinvest fees into liquidity, with any required conversion | | CPI | Cross-program invocation within Solana execution | | Dust | Integer rounding remainder requiring an explicit accounting destination | | ExactOut | Swap mode specifying the exact net recipient output, bounded by maximum input | | ExactIn | Swap mode specifying the input amount; completion and boundary policy remain explicit | | Fee growth | Cumulative fee amount per unit of liquidity in fixed-point units | | Graduation | Proposed transition from a launch curve to the open CLMM | | Hook binding | Association between a pool, hook program, ABI, and capabilities | | Liquidity net | Signed liquidity change associated with crossing a boundary | | Metis | Jupiter's routing engine targeted by the adapter design | | PDA | Program-derived address; signing authority is scoped to its deriving program | | Position | Ownership and accounting record for a specific liquidity range | | Principal | Assets backing liquidity or curve inventory, separate from fee liabilities | | Q64.64 | Integer fixed-point encoding with 64 fractional bits | | Quote parity | Agreement of quoted and executed amounts for the same supported operation and snapshot | | Tick | Signed discrete price index | | Tick spacing | Allowed separation grid for initialized position endpoints | | Transfer hook | Mint-level Token-2022 transfer callback, separate from pool hooks | | WSOL | Wrapped SOL represented through a token account | | Zap | Composition that swaps part of a budget and deposits into liquidity | # Hook ABI Typed operation context, bounded replies, and explicit account permissions. Source: https://docs.grapl.io/reference/hook-abi **Source:** `programs/grapl-amm/HOOK_FRAMEWORK.md`, `src/hooks/abi/`, and the core dispatcher. For a runnable introduction, use [Hooks](https://docs.grapl.io/hooks/overview). [Callback timing](https://docs.grapl.io/hooks/lifecycle) explains the phase semantics; [permissions](https://docs.grapl.io/hooks/permissions) explains account access. ## Wire and state versions | Constant | Current value | | ---------------------------- | ------------------------------------------ | | `HOOK_ABI_VERSION` | `4` | | Call / reply tags | `GRAPLHK4` / `GRAPLOK4` | | `POOL_VERSION` | `5` | | Application payload | At most `512` bytes | | Extra accounts | Configured bound, at most `16` | | Writable extras | Configured bound, at most `8` per callback | | External program IDs | At most `4` | | Canonical policy PDA indices | `0..=3` | | Callback mask | Ten lifecycle bits, `1023` | Match the program, IDL, hook, and client versions. Old wire tags and pool versions reject; there is no parallel legacy decoder. ## Invocation layout After the eight-byte call tag, Anchor/Borsh serializes these fields in order: | Field | Rust type | | ----------------------------------- | ---------------------------------------------- | | `version` | `u8` | | `phase` | `HookPhase`, variants 0–9 in lifecycle order | | `pool`, `mint_a`, `mint_b`, `actor` | `Pubkey`, 32 bytes each | | `operation_sequence` | `u64` | | `clock` | `HookClock { slot: u64, unix_timestamp: i64 }` | | `before` | `HookPoolSnapshot` | | `after` | `Option` | | `details` | `HookOperationDetails` | | `account_access` | `Vec` | | `payload` | `Vec` | Integers are little-endian; vectors carry their own length prefix. Enum and option discriminants are part of the encoding. The outer application-payload vector is separate from any proof or rule format inside it. Decoding validates the current tag/version, bounds, and exact serialization; use the exported types instead of assembling offsets manually. `HookPoolSnapshot` contains `pool_mode: u8` (`0` for CLMM, `1` for curve), `sqrt_price_x64: u128`, `current_tick: i32`, `liquidity: u128`, and `fee_growth_global_a_x64` / `fee_growth_global_b_x64: u128`. Liquidity is the active pool amount, not a position balance. Before callbacks have `after: None`; after callbacks include the resulting snapshot. ## Typed operation details | Variant | Contents | | --------------------------------------- | --------------------------------------------------------------------------------- | | `Initialize` | No additional operation fields | | `ModifyLiquidity(HookLiquidityDetails)` | Position key, signed `i128` liquidity delta, unsigned `u64` A/B principal amounts | | `Donate(HookDonationDetails)` | Unsigned `u64` A/B contributions | | `Swap(Box)` | Direction, account/authority identities, user limits, and optional settled result | `HookSwapDetails` contains `exact_output: bool`; `direction: HookSwapDirection` (`AtoB` or `BtoA`); input/output mints and token accounts; `input_authority` and `output_owner`; `requested_input` and `minimum_output: u64`; `sqrt_price_limit_x64: u128`; and `settled: Option`. | Settled field | Meaning | | ------------------------ | ------------------------------------------------------------------------------------------ | | `consumed_input: u64` | Actual input debit, including the AMM fee | | `net_input: u64` | Consumed input less that fee | | `actual_output: u64` | Net output transferred to the actor's output account | | `output_fee_amount: u64` | Separate deduction in output-token base units; gross output equals this plus actual output | | `unspent_input: u64` | Requested input that traversal did not consume | | `fee_amount: u64` | AMM fee in input-token base units | | `fee_ppm: u32` | Applied fee rate | For ExactOut, `requested_input` is the maximum input and `minimum_output` is the exact net output target. For ExactIn, those fields retain their input-budget and minimum-output meanings. BeforeSwap has no settled result. AfterSwap runs after classic SPL settlement and includes it. `requested_input = consumed_input + unspent_input`; `consumed_input = net_input + fee_amount`. Reward effects remain separate from these AMM amounts. ## Account descriptors and permissions Each hookable instruction includes `hook_accounts: Vec` in exact remaining-account order. `HookAccountAccess` has `class: HookAccountClass` and `writable_phases: u16`; its eight classes are `PolicyPda { index }`, `HookState`, `ExternalState`, `ExternalProgram`, `CreatableHookState`, `ActorReadOnly`, `PoolMintReadOnly`, and `InstructionsReadOnly`. The latter accepts only the canonical readonly Instructions sysvar and requires explicit external-read permission; it grants no CPI signing or execution authority. Reserve mint views are restricted to either authenticated pool mint, with no signing or writing privilege. See [permissions](https://docs.grapl.io/hooks/permissions) for ownership and phase requirements. `HookCapabilities` contains `bits`, `profile`, and `permissions: HookAccountPermissions`. Permissions declare hook writes, external reads/writes/program access by phase, extra/write count limits, and a bounded external-program ID list. Defaults allow four read-only hook-owned extras and no external access. Descriptors do not override that configuration; an empty descriptor vector means zero extras. The first CPI account is the read-only core hook PDA signer. The second is the read-only pool. Extras never receive upstream signer privileges and cannot alias protected settlement/core accounts. The core validates classes, owners, declared programs, bounds and per-phase privileges; the hook additionally validates role-specific addresses, discriminators, and relationships. See [authentication](https://docs.grapl.io/hooks/authentication). ## Replies and identity `HookReply { version, phase, allow, fee_ppm, output_fee }` contains `output_fee: Option` (introduced in ABI 3). A successful reply must come from the configured hook, match the enabled phase/version/profile, and have `allow: true`. Only DynamicFee BeforeSwap supplies `Some(fee)` within the immutable cap; every other successful reply requires `None`. `output_fee` is independent: BeforeSwap can request `{ fee_ppm, destination }` under `permissions.max_output_fee_ppm`, which defaults to zero. Other phases must return `None`. Core floor-rounds the deduction, enforces minimum output on the net amount, and transfers the fee itself to a declared writable `ExternalState` token account matching the output mint. The destination cannot alias a protected account. General custom accounting and return deltas remain unsupported. The core reads Clock once per hookable operation. Both phases share that Clock and `operation_sequence`, which increments once for initialize, liquidity modification, swap, or donate, including no-hook operations. A failed operation rolls the increment back. `(core program, pool, sequence)` identifies the operation context; it is not randomness, a transaction signature, or an address that clients should predict for state preparation. Actor is the authenticated local token authority and may be a PDA. Caller payload bytes and a quoted actor are not signature evidence. All ten phases belong to this framework; opening/closing positions and collecting fees have no dedicated callbacks. Graduation uses the add-liquidity callback pair; it has no dedicated callback. ## Optional examples Fee schedules, windows, allowlists, and rewards belong under `examples/hooks`. They do not become native pool rules or dependencies of the core program. A single attached hook can compose its own rules. The lifecycle probe exercises callbacks without an economic policy; [reward credits](https://docs.grapl.io/hooks/rewards) demonstrate stateful effects under explicit permissions. Use [policy mechanics](https://docs.grapl.io/reference/policies) for the earlier pure examples and [Metis compatibility](https://docs.grapl.io/hooks/metis) for quote/execution obligations. Routing availability depends on a compatible evaluator and the routing provider’s integration. # Core instruction surface Find actual entrypoints, argument contracts, and the operations each hook observes. Source: https://docs.grapl.io/reference/instructions **Source:** `programs/grapl-amm/src/lib.rs`. The Pool 6 source increment defines 19 instructions and four Anchor account types. Generate clients from the matching hook ABI 4 / Pool 6 build; this overview is not a substitute for the Anchor account order. See [current validation status](https://docs.grapl.io/protocol/fees-and-revenue). ## Issuance and market creation | Entrypoint | Arguments | Behavior | | ----------------------- | ------------------------------------------ | --------------------------------------------------------------------- | | `create_token` | `CreateTokenArgs` | Issues an independent classic SPL token; no implicit pool or metadata | | `initialize_pool` | `InitializePoolArgs` | Creates a direct CLMM, directory, vaults, and optional hook binding | | `initialize_curve` | `InitializePoolArgs`, `CurveConfiguration` | Creates curve mode and deposits real base inventory | | `initialize_tick_array` | `InitializeTickArrayArgs` | Creates a canonical fixed or compact tick page | | `reserve_tick_array` | `capacity: u8` | Funds rent and grows an existing compact page | `InitializePoolArgs` contains `pool_id: u64`, `sqrt_price_x64: u128`, `configuration: PoolConfiguration`, `hook_payload: Vec`, and `hook_accounts: Vec`. Curve initialization derives the initial price from the virtual-reserve configuration. See [token factory](https://docs.grapl.io/guides/token-factory) and [bonding curve](https://docs.grapl.io/concepts/bonding-curve) for their parameter tables and authority decisions. ## Positions and fees | Entrypoint | Arguments | Behavior | | ------------------------------ | ----------------------- | --------------------------------------------------------------------------------------- | | `open_position` | `OpenPositionArgs` | Creates an owner-bound empty position with a valid range | | `close_position` | None | Closes only when liquidity and both owed fee amounts are zero | | `modify_liquidity` | `ModifyLiquidityArgs` | Adds/removes signed liquidity and settles principal | | `collect_position_fees` | None | Crystallizes and transfers LP fees without changing liquidity | | `collect_beneficiary_fees` | `beneficiary_index: u8` | Pays the selected beneficiary's accrued A/B debt to that signer | | `initialize_protocol_treasury` | `recipient: Pubkey` | Initializes the immutable global recipient through the program's upgrade authority | | `collect_protocol_fees` | None | Pays one pool's protocol A/B debt to the treasury recipient; preserves fractional carry | | `donate` | `DonateArgs` | Adds to active LP fee growth without minting liquidity | `OpenPositionArgs` contains `position_id: u64`, `tick_lower: i32`, and `tick_upper: i32`. Opening is not a deposit. `ModifyLiquidityArgs` contains `liquidity_delta: i128`, `limit_a: u64`, `limit_b: u64`, `hook_payload`, and `hook_accounts`. Positive deltas add principal subject to maximum deposits; negative deltas withdraw principal subject to minimum outputs. Public liquidity deposits are unavailable while the pool remains in curve mode. `DonateArgs` contains `amount_a: u64`, `amount_b: u64`, `hook_payload`, and `hook_accounts`. At least one amount must be nonzero; LP accrual needs active liquidity. ## Swaps and composition | Entrypoint | Arguments | Behavior | | ------------------------- | ------------------- | ---------------------------------------------------------------------------- | | `swap` | `SwapArgs` | ExactIn; reports consumed input, net output, and unspent budget | | `swap_exact_out` | `ExactOutArgs` | Exact net output with a maximum gross input | | `zap_in` | `ZapInArgs` | Swaps a caller-selected amount, then funds the owned CLMM position | | `zap_out` | `ZapOutArgs` | Removes principal, then converts the withdrawn side into one asset | | `graduate_curve` | `GraduateCurveArgs` | Converts the eligible curve into a full-range CLMM in one atomic instruction | | `collect_curve_remainder` | None | Lets the creator collect recorded leftover assets after graduation | `SwapArgs` contains `amount_in: u64`, `minimum_out: u64`, `sqrt_price_limit_x64: u128`, `a_to_b: bool`, `hook_payload`, and `hook_accounts`. Reaching a CLMM price limit may leave input unspent. Metis rejects incomplete ExactIn quotes. `ExactOutArgs` replaces the amount fields with `maximum_in: u64` and `amount_out: u64`; the target is **net of a hook output fee**. The direction, price limit, payload, and descriptors remain explicit. An incomplete ExactOut fails. See [swap semantics](https://docs.grapl.io/concepts/swaps) and [zaps](https://docs.grapl.io/guides/zaps) for user limits and callback order. Graduation uses add-liquidity callbacks, preserves the market identity, and creates a position owned by the stored creator. It is separate from the last curve swap. ## Pool configuration | Field | Validation or meaning | | ---------------------------------------- | -------------------------------------------------------------------------------- | | `mode` | `0`: CLMM; `1`: virtual-reserve curve through `initialize_curve` | | `tick_spacing` | `1..=32768` | | `trade_fee_ppm` | At most the immutable `max_fee_ppm` | | `max_fee_ppm` | At most `990000` ppm, or 99% | | `lp_share_bps` + beneficiary shares | Exactly `10000` basis points of the remainder after mandatory protocol retention | | `beneficiaries` | Four slots; nonzero recipient iff nonzero share; recipients unique | | `hook_capabilities` | Supported lifecycle bits/profile and bounded account permissions | | `max_output_fee_ppm` in hook permissions | Separate output-deduction cap; defaults to zero | These are validation bounds, not recommended economic settings. Share weights use basis points; swap fee rates use parts per million. The fixed 1,000 bps protocol share is enforced before the configured remainder split and is not a `PoolConfiguration` opt-out. Per-pool, per-mint carry preserves fractional protocol allocations. Donations and hook output deductions follow separate paths. Read [fee accounting](https://docs.grapl.io/concepts/fees). ## Remaining accounts Swap and modify-liquidity use ascending signed tick-page indices first, then hook extras in descriptor order. Each page is validated against the pool and canonical address. Collect-position-fees receives endpoint pages. Other hookable operations receive their corresponding extras. A zap partitions remaining accounts into its two legs using an explicit count. Supply each leg's pages and hook extras under that leg's descriptors. Neither leg silently inherits the other's permissions. For all methods, the Anchor `Accounts` structs define signer, owner, mint, mutability, and PDA constraints. General custom hook deltas, automatic compounding, and position transfer are not part of the instruction surface. # Math library Current fixed-point domain, public primitives, and rounding contracts. Source: https://docs.grapl.io/reference/math **Source:** `programs/grapl-amm/src/math/mod.rs` and its focused modules. The library performs pure calculations; it does not validate Solana account ownership or move tokens. ## Numeric domain | Constant | Value | | ----------------------- | --------------------------------------------------------------------- | | `Q64` | `1 << 64` | | `MIN_TICK` | `-443636` | | `MAX_TICK` | `443636` | | `MIN_SQRT_PRICE` | `4295048016` | | `MAX_SQRT_PRICE` | `79226673515401279992447579061` | | `FEE_DENOMINATOR` | `1000000` | | `MAX_FEE_BENEFICIARIES` | `8` in the math helper; the core currently has four beneficiary slots | Square-root prices use **Q64.64**. The tick lattice uses the implementation's Q192 factors and integer algorithm; matching a domain used by another CLMM does not promise bit-for-bit parity with that protocol. ## Public primitives | Module | Exports | | ------------ | ---------------------------------------------------------------------------------------- | | `wide` | `mul_div`, `Rounding::{Down, Up}` | | `ticks` | `tick_to_sqrt_price`, `sqrt_price_to_tick`, `validate_sqrt_price`, `validate_tick_range` | | `quantities` | `TokenAmounts`, `checked_token_amount`, `add_liquidity_delta` | | `liquidity` | `amount_a_delta`, `amount_b_delta`, `amounts_for_liquidity`, `liquidity_from_amounts` | | `swap` | `compute_swap_step`, `SwapDirection`, `SwapStepParams`, `SwapStep` | | `fees` | `FeeRate`, `split_fee_amount` | | `growth` | `accrue_fee_growth`, `fees_owed` | `sqrt_price_to_tick` returns the greatest tick whose encoded square-root price does not exceed the input. `validate_tick_range` requires ordered endpoints and a positive spacing with aligned ticks. ## Fees and residues `FeeRate::new` accepts rates below one million ppm; a pool imposes its own lower cap. `net_from_gross` rounds the net budget down. `fee_from_net` rounds the required fee up. `split_fee_amount(fee, shares, residue_index)` floors each ppm allocation and sends the remaining units to the selected nonzero share. It is a **per-event** allocation helper, not cumulative beneficiary entitlement accounting. `accrue_fee_growth` rounds growth down and uses checked accumulation rather than wrapping. `fees_owed` rounds the liquidity-times-growth claim down. The caller is responsible for inside-growth computation, atomic checkpoints, and retaining rounding residue as a liability. ## A swap step is not a complete swap `compute_swap_step` moves within one interval toward a target price, reporting input, output, fee, remaining amount, and whether the target was reached. Full execution still needs tick traversal, account validation, settlement, limits, and rollback. Do not treat a successful step calculation as a tradable pool. Check arithmetic against independent rational or high-precision calculations, including one-unit rounding and boundary cases. See [outcome validation](https://docs.grapl.io/security/validation). ## Curves and exact output `curve_output` floor-rounds the virtual-reserve output for net input. `curve_input` ceil-rounds the net input needed for a requested output; `curve_sqrt_price` derives the bounded Q64.64 marginal price. `compute_exact_output_step` performs a requested-output CLMM step; settlement separately includes input and output fees. Read [worked AMM equations](https://docs.grapl.io/concepts/amm-math) for a numerical example and [ExactIn / ExactOut](https://docs.grapl.io/concepts/swaps) for the user-facing contract. Pure math never treats virtual reserves as permission to withdraw unbacked assets. # Example hook policies Optional example implementations of fee schedules, trading windows, and Merkle membership. Source: https://docs.grapl.io/reference/policies **Optional example policies.** Read `examples/hooks/shared` and the three economic programs in `examples/hooks/*`. Those fee/window/allowlist programs require exactly BeforeSwap. The lifecycle probe and composed [reward example](https://docs.grapl.io/hooks/rewards) have separate contracts. Start with the [Hooks guides](https://docs.grapl.io/hooks/overview) for implementation guides. These policies and their shared helpers belong to the examples, not the core library/program. The core supplies ABI/dispatch and supports no-hook pools. An allowlist is not a pool or launch prerequisite. ## Dynamic fee `FeeSchedule` stores `peak_ppm`, `floor_ppm`, `starts_at`, and `ends_at`. Validation requires an ordered time interval, floor at most peak, and peak at most the authenticated pool cap. `fee_at(timestamp, cap)` clamps time to the endpoints, decreases the premium linearly from peak to floor, and rounds the premium up using integer arithmetic. Before the start it returns peak; at or after the end it returns floor. The core's tighter cap still applies. The hook reads runtime Clock. A quote implementation needs the same time snapshot, formula, rounding, and cap; a process wall clock is not an equivalent input. ## Trading window `TradingWindow { opens_at, closes_at }` uses the half-open interval `[opens_at, closes_at)`. The opening timestamp is accepted; the closing timestamp is rejected. Empty and reversed windows reject. The module name `timelock` does not imply an LP vesting vault, a withdrawal lock, or delayed governance. It implements an explicitly configured trading window. ## Merkle allowlist Membership is tied to a domain containing 32-byte chain namespace, protocol, pool, mint, and `root_version: u64`. The leaf then includes the authenticated trader and `expires_at: i64`. | Property | Current rule | | ---------------- | ---------------------------------------------------- | | Leaf tag | `grapl:allowlist:leaf:v1` | | Inner-node tag | `grapl:allowlist:node:v1` | | Integer encoding | Little-endian root version and expiry | | Node ordering | Lexicographically sorted child hashes | | Proof length | At most 12 sibling nodes | | Empty proof | Root is the leaf itself | | Expiration | `now >= expires_at` rejects | | Chain namespace | Explicit configured 32 bytes; not inferred from time | The runtime uses SHA-256 over exact concatenations, without extra field-length prefixes: ```text leaf = SHA256(leaf_tag | chain32 | protocol32 | pool32 | mint32 | root_version_u64_le | trader32 | expires_at_i64_le) parent = SHA256(node_tag | lexicographically_smaller_hash32 | larger_hash32) payload = version_u8(1) | expires_at_i64_le | sibling_count_u8 | siblings32 ``` Siblings are ordered from leaf to root; there is no leaf index or implicit padding. Twelve siblings produce a 394-byte payload. The namespace is creator-configured and is not automatically checked against runtime genesis. The root has no update operation in this example; valid membership can be reused before expiry. The pure policy injects a `MerkleHasher` interface. Callers must use the exact hasher selected by the runtime implementation and the same byte ordering. Proof generation is not a frontend permission grant: the actor comes from core-authenticated context, not a pubkey supplied in proof bytes. ## Integration limits Quoting these examples requires their policy state and evaluation logic; actor-dependent policies also require explicit identity and proof context. See [Metis compatibility](https://docs.grapl.io/hooks/metis) for adapter support. Preserve the same policy decisions in quoting and execution. See [hook ABI](https://docs.grapl.io/reference/hook-abi), [wallet context](https://docs.grapl.io/integrations/wallet-context), and [validation](https://docs.grapl.io/security/validation). # Sources and references Official documentation and pinned prior art, with clear limits on what they establish. Source: https://docs.grapl.io/reference/resources grapl studies established protocols without treating their audits, deployments, branding, or integrations as its own. References below informed the architecture; they do not certify this implementation. ## Documentation structure The site separates concepts, task guides, and reference in the spirit of the [Uniswap v4 developer documentation](https://developers.uniswap.org/docs/protocols/v4/overview). The site uses [Fumadocs MDX](https://www.fumadocs.dev/docs/mdx), [Next.js integration](https://www.fumadocs.dev/docs/mdx/next), and the [Fumadocs docs layout](https://www.fumadocs.dev/docs/ui/layouts/docs). ## Pinned protocol research The architecture research pinned these revisions on 2026-10-08. They identify research sources, not the revisions deployed by those protocols. | Source | Revision | Research purpose | | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------- | | [Uniswap v4 core](https://github.com/Uniswap/v4-core/tree/46c6834698c48bc4a463a86d8420f4eb1d7f3b75) | `46c6834698c48bc4a463a86d8420f4eb1d7f3b75` | Lifecycle, deltas, tick and rounding semantics | | [Orca Whirlpools](https://github.com/orca-so/whirlpools/tree/f2a3d13fa04eb15cf5b5a309ef9b226fd5d34e36) | `f2a3d13fa04eb15cf5b5a309ef9b226fd5d34e36` | Rust CLMM, liquidity deltas, swap steps, fee growth | | [Raydium CLMM](https://github.com/raydium-io/raydium-clmm/tree/ed1eb41519d5355755f7df52b43fa9610938b60b) | `ed1eb41519d5355755f7df52b43fa9610938b60b` | Account validation and fee configuration | | [Meteora DBC](https://github.com/MeteoraAg/dynamic-bonding-curve/tree/f552f20aa3c1c7631427c3827aeea7c58b902813) | `f552f20aa3c1c7631427c3827aeea7c58b902813` | Segmented curves and graduation requirements | | [Jupiter AMM interface](https://github.com/jup-ag/jupiter-amm-interface/tree/95bd18485e580625a39eb92a0c0ad2061579daad) | `95bd18485e580625a39eb92a0c0ad2061579daad` | Snapshot quoting and execution integration | ## Solana and integration references - [Solana CPI](https://solana.com/docs/core/cpi): privilege propagation and invocation behavior. - [Token-2022 transfer hooks](https://solana.com/docs/tokens/extensions/transfer-hook): mint-level callbacks. - [Sync Native](https://solana.com/docs/tokens/basics/sync-native): wrapped SOL mechanics. - [Jupiter AMM integration](https://developers.jup.ag/docs/swap/routing/amm/integration): interface, parity, and review process. - [Anchor documentation](https://www.anchor-lang.com/docs): framework concepts; match examples to the installed version. ## Implementation reference Use the [source map](https://docs.grapl.io/reference/source-map) to locate actual implementations. Numeric constants and callable instruction signatures come from the matching checkout, not from an upstream protocol's documentation. # Source map Find each responsibility in the checkout and distinguish architectural sources. Source: https://docs.grapl.io/reference/source-map Protocol paths are relative to [grapl-io/grapl](https://github.com/grapl-io/grapl). The documentation lives separately in [grapl-io/docs](https://github.com/grapl-io/docs). | Path | Responsibility | | -------------------------------------- | ---------------------------------------------------------------------------------- | | `Cargo.toml` | The single package `grapl-amm`, with optional integration/test features | | `programs/grapl-amm/src/math/` | Integer arithmetic, tick prices, ranges, swap steps, fees | | `programs/grapl-amm/HOOK_FRAMEWORK.md` | Shared rich-context and state-permission contract | | `programs/grapl-amm/src/hooks/abi/` | Typed callback/reply ABI, context and bounded account declarations | | `programs/grapl-amm/src/hooks/` | Core callback construction and dispatch | | `programs/grapl-amm/src/instructions/` | Pool, positions, trading, and fee operations | | `programs/grapl-amm/src/state/` | Pool, curve state, positions, and paginated tick storage | | `programs/grapl-amm/src/settlement/` | Validated token accounts and token movement | | `examples/hooks/shared/` | Pure dynamic-fee, trading-window, and Merkle allowlist policies | | `examples/hooks/dynamic-fee/` | Independently deployable dynamic-fee demonstration | | `examples/hooks/decaying-fee/` | Optional 99%-to-floor linear launch-fee schedule | | `examples/hooks/timelock/` | Independently deployable trading-window demonstration | | `examples/hooks/allowlist/` | Independently deployable allowlist demonstration | | `examples/hooks/lifecycle/` | Ten-phase callback and rejection example | | `examples/hooks/rewards/` | Optional composed RewardCredits rule and runtime | | `integrations/metis/` | Optional integration under feature `metis` | | `integrations/analytics/` | Host-only valuation, observed fee APR, and projected APY under feature `analytics` | | `examples/client-inputs/src/` | TypeScript client utilities | | `tests/math/`, `tests/invariants/` | Math and independent numerical verification | | `tests/metis/`, `tests/svm/` | Quote contracts and isolated runtime fixtures | | `tests/analytics/` | Synthetic fee-performance calculation fixtures | | `Makefile` | Setup, checks, SBF builds, and local runtime | | `Anchor.toml` | Anchor local program configuration | Use the matching exported types and IDL when constructing accounts or decoding state. ## Expanded market operations | Source | Responsibility | | ---------------------------------------------------------- | ------------------------------------------------------------------------ | | `programs/grapl-amm/src/instructions/tokens/` | Independent classic SPL token factory | | `programs/grapl-amm/src/instructions/curve/` | Inventory-backed curve initialization, swaps, graduation, and remainders | | `programs/grapl-amm/src/instructions/zaps/` | Atomic swap and position composition | | `programs/grapl-amm/src/instructions/trading/exact_out.rs` | Exact net-output entrypoint | | `programs/grapl-amm/src/math/curve.rs` | Pure virtual-reserve equations | Documentation pages are in `content/docs/` at the root of the documentation repository. # Protocol invariants Properties the implementation and every supported extension must preserve. Source: https://docs.grapl.io/security/invariants These invariants are requirements and test targets. Their presence in this page is not a proof that all have been verified. ## Conservation and solvency For each asset, reconcile spendable vault balance against principal, LP fee debts, protocol debts, beneficiary debts, segregated allocations, and identified excess. No unit backs two liabilities. Virtual curve reserves never finance a withdrawal. ## User limits The input debit stays within authorization; the output satisfies the applicable minimum. Hook fees, supported transfer fees, and custom deltas must be included in the applicable amount calculation. A zero intermediate-hop minimum may rely on a final route check; this must be explicit, not assumed. ## Liquidity and ticks Active liquidity matches positions covering the current interval. Crossing applies the correct signed net liquidity in each direction. Removing liquidity cannot underflow a position or withdraw another position's principal. Capacity and gap behavior are explicit. ## Fee ownership Only active positions earn fees for a traded segment. Checkpoints prevent repeat claims. Collect does not increase liquidity; compound is separate. Protocol retention and remainder splits conserve the charged fee. Per-mint fractional carry survives claims, and split residue remains accounted for. ## Immutable relationships The pair, spacing, hook binding, and supported policy version cannot change silently beneath LPs or quote adapters. If a future configuration is mutable, its authority, bounds, and treatment of existing claims must be defined. ## Call and account authenticity Validate owner programs, discriminators, seeds, signers, mint identities, token authority, and hook executability. A matching byte layout or supplied pubkey does not authenticate an account. Hooks receive only the privileges required by their contract. ## Atomic failure A rejected hook, insufficient output, bad account, or arithmetic error must leave no committed partial settlement. Runtime tests must inspect state and token balances after rejection, not only the error string. ## Graduation Graduation must happen once, uses real unencumbered assets, preserves the approved destination plan, and establishes a compatible CLMM price. Repetition and failure paths require tests as much as the happy path. # Current limitations Implementation and integration boundaries to preserve when building on grapl. Source: https://docs.grapl.io/security/limitations Match your application to [supported capabilities](https://docs.grapl.io/start/status) and the current ABI. The constraints below affect market design and transaction construction. ## Protocol constraints - Pool mode `0` is CLMM; mode `1` is the virtual-reserve curve. Curve swaps do not cross into CLMM inside one trade; graduation is a separate instruction. - Pool settlement accepts classic SPL Token. A separately declared Token-2022 reward CPI does not enable Token-2022 reserve assets. - Initialization permits classic reserve mints with issuer freeze authority. Issuers can freeze token accounts, including pool vaults, preventing transfers, swaps and withdrawals involving those accounts. The independent factory still creates no freeze authority and no metadata. - One swap accepts at most 16 supplied tick pages; transaction bytes, hooks, and CUs can impose tighter limits. - Compact pages can grow through funded reservation; shrinking or rent refund for those pages is not implemented. - The core has four external fee-beneficiary slots. General custom hook accounting and return deltas reject. - Hook payloads and extras are bounded. Per-phase permissions do not grant reserve custody or wallet signatures. - Zaps require an existing owned CLMM position and a caller-selected conversion amount. They do not optimize the split or collect LP fees automatically. - A curve's graduation position is creator-owned; retained LP-share fees fund its bootstrap. Protocol and beneficiary debts are excluded. Applications must explain this ownership policy. ## Hook and route boundaries Only one hook program binds to a pool; developers may combine rules within it. A pool hook does not constrain token transfers or trading on unrelated pools. A generic quote does not prove wallet eligibility. Public sponsored rewards can assemble wallet accounts later, but contextual allowlists need an explicit proof/identity path. Some hook effects require pre-funded sponsor SOL. Neither issuance caps nor fee revenue guarantee sufficient rent funding or resistance to new-wallet abuse. Public Jupiter registration, discovery, shared-account routes, and multi-hop execution require a separate routing integration. ## Deferred capabilities Confidential balances and settlement are deliberately deferred. Also outside the current interface: Token-2022 reserve extensions, verifiable asynchronous randomness, strategy vaults, automatic compounding, and transferable LP positions. ## Release boundaries There is no public mainnet address registry, completed grapl audit, guaranteed liquidity, production indexer, or verified Jupiter admission in these docs. The treasury increment adds ProtocolTreasuryInitialized and ProtocolFeesCollected events. Those events and instruction/probe logs do not provide a complete stable observer API for every operation. # Security model Understand which component is trusted to authorize, price, and settle an operation. Source: https://docs.grapl.io/security/model The core must enforce account ownership, authorization, user limits, and solvency for every caller. A helpful SDK cannot protect a program from transactions that bypass it. ## Trust boundaries | Boundary | Required enforcement | | --------------------- | -------------------------------------------------------------------------------- | | Client to core | Validate inputs, signers, mints, program owners, PDAs, and account relationships | | Core to math | Validate numerical domains and checked results | | Core to pool hook | Authenticate the callback; restrict accounts and accepted replies | | Core to token program | Use the configured token program and actual vault/mint/authority | | Snapshot to adapter | Verify layout/version and reject missing state or unsupported policy | | Quote to execution | Apply user limits to the actual state and token movements | ## Hooks have power within a contract A hook can reject an operation, return a bounded fee where enabled, and mutate declared state within per-phase permissions. The core checks account classes, program/owner declarations, limits and protected aliases; the hook checks exact role-specific identities and state invariants. It must not borrow the user's signer or vault authority. Custom accounting needs a separate conservation and quote contract and is currently rejected. An upgradeable hook or core introduces an upgrade authority into the trust model. An immutable binding does not freeze the code behind an address. No upgrade governance or emergency-exit guarantee should be inferred from the existence of an ABI. ## Assets and identities Keep signing secrets outside application logs and documentation. Separate transaction fee payment, token authority, hook authority, and program upgrade authority in your application’s trust model. The trader, transfer authority, and output owner have distinct meanings. A real signer proves control of a key for the transaction; it does not prove civil identity or the final recipient of an arbitrary multi-hop route. ## Evidence and review No audit or production-readiness claim is made by this site. Security claims must identify the inspected revision, tested path, and remaining limitations. Continue with [invariants](https://docs.grapl.io/security/invariants) and [validation](https://docs.grapl.io/security/validation). # Verify transaction outcomes Connect user intent, successful execution, and reconciled state changes. Source: https://docs.grapl.io/security/validation A signature identifies a submitted transaction; a successful result and its account changes establish what happened. Your application should distinguish pending, rejected, and settled operations. ## Establish success before reading effects Use the transaction's result and the commitment level required by your application. Logs and inner instructions may survive in the record of a failed transaction even though its state changes were rolled back. Never credit volume, fees, or rewards from a callback log alone. ## Reconcile the operation | Operation | Inspect after success | | ---------------- | -------------------------------------------------------------------------- | | Swap | Actual input, net output, fee destinations, price and liquidity | | Liquidity change | Position liquidity, principal transfers, ticks, fee checkpoints | | Fee collection | Position, protocol, or beneficiary claim and corresponding token credit | | Graduation | Pool mode, creator position, price continuity, recorded remainder | | Reward issuance | Eligible settled output, recipient balance, issuance counter, sponsor cost | Compare these effects with the user's limits and the expected account identities. Token amounts use atoms; formatting belongs at the presentation boundary. ## Keep failure atomic A denied callback, malformed reply, insufficient budget, or failed nested CPI must not leave partial settlement. Check the pool, tick pages, positions, token balances, and any hook-controlled state together. Network transaction fees can still be charged for failed execution. ## Make observations reproducible Record the program/interface version, pool, operation, relevant slot, and snapshot needed to explain a result. Protect private application data and signing secrets. Where pricing is involved, compare deterministic integer calculations with an independent model as well as shared quote code. The core does not currently expose a typed Anchor event schema. Build observers around successful transactions and authoritative account state, and account for finality in your indexing strategy. Continue with [invariants](https://docs.grapl.io/security/invariants) and [integration validation](https://docs.grapl.io/guides/local-tests). # Build your first integration Go from a pool configuration to a quoted trade and a verified result. Source: https://docs.grapl.io/start/quickstart For a concrete source-SDK example, start with the [RPC and wallet quickstart](https://docs.grapl.io/developers/sdk). This page explains the integration decisions around that flow. grapl combines concentrated liquidity with optional pool hooks. A useful first integration reads one pool, explains its behavior, and constructs one bounded swap. The same primitives support direct trading, routed swaps, and applications with custom market rules. ## 1. Choose the market A **direct pool** opens at a configured price and uses funded CLMM positions. A **bonding curve** begins with virtual-reserve pricing backed by real inventory, then graduates into a CLMM. Both paths can attach a hook; a pool without one is valid. Read [market creation](https://docs.grapl.io/concepts/launches) before creating a pool, or [AMM math](https://docs.grapl.io/concepts/amm-math) to understand how an existing pool quotes a trade. ## 2. Read the pool's contract Resolve the pool under the program ID supplied by your deployment configuration. Check its version, ordered reserve mints, token programs, current mode, price, liquidity, fees, and optional hook. Derive accounts using the [account reference](https://docs.grapl.io/reference/accounts). Use a matching **Pool 6 / hook ABI 4** interface for the current source increment. Pool 5 accounts require an explicit migration strategy; there is no automatic runtime conversion. These docs do not publish a program-address registry; a source identifier alone is not deployment discovery. A hooked pool also needs its declared policy accounts. Inspect enabled callbacks and account permissions before deciding which operations your application supports. ## 3. Express the user's limits | User intent | Mode | Protect with | | ----------------------- | -------- | ---------------------------------- | | Spend a chosen amount | ExactIn | Minimum net output and price limit | | Receive a chosen amount | ExactOut | Maximum input and price limit | Parse decimal strings into integer token atoms using the mint's decimals. Keep amounts in integer types throughout quoting and instruction construction. [Client amounts](https://docs.grapl.io/integrations/sdk) explains parsing; [swap modes](https://docs.grapl.io/concepts/swaps) explains partial fills and net output. ## 4. Quote a complete snapshot Load the pool, required tick pages, and hook state from a coherent snapshot. For a hook, reproduce its time, fee, eligibility, and account requirements. If a required policy evaluator or identity context is unavailable, reject the quote. The [Metis adapter](https://docs.grapl.io/integrations/metis) accepts injected hook evaluators and shares the core traversal. A successful quote is a price estimate against that snapshot; execution validates current state again. ## 5. Construct and authorize the operation Use the [instruction reference](https://docs.grapl.io/reference/instructions) and matching IDL for account order. Supply canonical tick pages followed by the declared hook extras. Bind source and destination token accounts to the intended authority and mints. The transaction signer authorizes the operation. A callback's actor may be a router PDA, so it is not always the wallet shown by your application. Read [wallet context](https://docs.grapl.io/integrations/wallet-context) before supporting routed or sponsored flows. ## 6. Show the settled result Confirm transaction success before presenting a completed trade. Reconcile actual input, net output, fees, and relevant hook effects. A rejected after callback rolls back the operation's state changes; transaction fees can still be charged. Continue with [integration validation](https://docs.grapl.io/guides/local-tests), [liquidity positions](https://docs.grapl.io/concepts/lp-lifecycle), or [your first hook](https://docs.grapl.io/hooks/first-hook). # Protocol capabilities Find supported market operations, interface versions, and integration boundaries. Source: https://docs.grapl.io/start/status grapl exposes an integer CLMM, a virtual-reserve launch path, and an optional lifecycle hook interface. Choose the capabilities your application needs and reject any operation you cannot reproduce faithfully. ## Markets and liquidity | Capability | Behavior | Guide | | ------------- | --------------------------------------------------------------------- | ---------------------------------------- | | Direct pools | Concentrated and aligned full-range positions | [Create a market](https://docs.grapl.io/concepts/launches) | | LP positions | Open, add/remove principal, collect fees, close empty positions | [LP lifecycle](https://docs.grapl.io/concepts/lp-lifecycle) | | Swaps | ExactIn and ExactOut with net-output and price protection | [Swap modes](https://docs.grapl.io/concepts/swaps) | | Token factory | Classic SPL issuance with explicit final mint authority | [Token factory](https://docs.grapl.io/guides/token-factory) | | Bonding curve | Virtual pricing, real inventory, net quote target | [Curve markets](https://docs.grapl.io/concepts/bonding-curve) | | Graduation | Same pool, mints, vaults, and hook; creator-owned full-range position | [Graduation](https://docs.grapl.io/concepts/bonding-curve) | | Zaps | Atomic swap and liquidity change on an existing position | [Zaps](https://docs.grapl.io/guides/zaps) | ## Custom behavior Hooks can validate ten before/after phases, request bounded swap fees, and update explicitly permitted state. Optional examples cover eligibility, trading windows, fee schedules, and non-transferable reward credits. No hook is required by default. Custom accounting and arbitrary return deltas are unsupported. Position open/close and fee collection have no dedicated callbacks. Graduation uses the add-liquidity pair. See [hook lifecycle](https://docs.grapl.io/hooks/lifecycle) and [permissions](https://docs.grapl.io/hooks/permissions). ## Interface compatibility The current source increment uses **Pool version 6**, **hook ABI 4**, wire tags `GRAPLHK4` / `GRAPLOK4`, **19 core instructions**, and four Anchor account types. Pool 6 adds mandatory protocol fee accounting and a treasury record. Match clients and hook programs to that contract; Pool 5 accounts reject, and no automatic runtime migration is provided. See [revenue validation status](https://docs.grapl.io/protocol/fees-and-revenue). The [Metis integration](https://docs.grapl.io/integrations/metis) supports both swap modes with shared math and injected policy adapters. Its availability in a public routing service requires that service's own integration and admission. ## Boundaries to design around - Reserve assets use classic SPL Token; Token-2022 reserve assets and confidential settlement are unsupported. - Zaps take a caller-selected split and an existing position. They do not optimize ranges or compound automatically. - Position transfer, strategy vaults, and token metadata issuance are not exposed by this interface. - The treasury increment emits initialization and protocol-fee collection events; this is not complete typed event coverage for every operation. - APR calculations require supplied observations; there is no live yield feed in the core. These docs provide no public deployment registry or completed audit claim. Use [security limitations](https://docs.grapl.io/security/limitations) for the implications and [instructions](https://docs.grapl.io/reference/instructions) for the exact operation surface. # User Guides Your path from a first swap to providing liquidity, managing positions, and launching a market. Source: https://docs.grapl.io/user-guides grapl provides concentrated liquidity pools on Solana. Traders exchange tokens against liquidity supplied by position owners. An optional hook can add rules or fees to a particular pool. Start with your goal, then follow the complete operation: prepare, review, sign, and verify. These guides explain the protocol through a compatible application without assuming a particular wallet interface or a publicly deployed market. Check [current capabilities](https://docs.grapl.io/start/status) before starting. ## Start here [Getting started](https://docs.grapl.io/user-guides/getting-started) Prepare your wallet, verify assets, and know what a signature authorizes. [Choose a pool](https://docs.grapl.io/user-guides/choose-pool) Compare your trade size, active liquidity, costs, and pool rules. [Swap tokens](https://docs.grapl.io/user-guides/swaps) Choose ExactIn or ExactOut, set limits, and confirm the settled amounts. [Understand a pool](https://docs.grapl.io/user-guides/pools) Learn how assets, price, liquidity, and fees fit together. ## Become a liquidity provider Providing liquidity changes your exposure to both tokens as people trade. Start by choosing a range and understanding the assets you may hold at its edges. Then review the deposit and the ongoing management it needs. [Choose your liquidity](https://docs.grapl.io/user-guides/liquidity) Compare concentrated, full-range, and one-sided positions. [Add liquidity](https://docs.grapl.io/user-guides/add-liquidity) Set your range and budget, then open and fund a position. [Full-range liquidity](https://docs.grapl.io/user-guides/full-range) Understand broad price coverage and its capital tradeoff. [One-sided liquidity and zaps](https://docs.grapl.io/user-guides/one-sided) Distinguish an out-of-range deposit from a swap-and-deposit operation. ## Manage and exit a position Principal, earned fees, and account deposits are separate balances. Use the guide for the balance you want to manage; removing liquidity does not automatically finish every step of an exit. | Task | Guide | Outcome to verify | | ------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------- | | Monitor an existing position | [Manage your position](https://docs.grapl.io/user-guides/positions) | Range status, changing principal, and accrued fees | | Claim trading income | [Collect your fees](https://docs.grapl.io/user-guides/collect-fees) | Confirmed transfers of the claimed assets | | Withdraw part or all of the principal | [Remove liquidity](https://docs.grapl.io/user-guides/remove-liquidity) | Actual receipts and remaining liquidity | | Close an emptied position | [Position lifecycle](https://docs.grapl.io/user-guides/positions#collect-and-close) | Cleared liquidity and fees, then returned position rent | ## Create a market [Create a pool](https://docs.grapl.io/user-guides/create-pool) Choose the assets, starting price, fee allocation, and initial liquidity. [Understand curve launches](https://docs.grapl.io/user-guides/curve-launches) Follow real inventory, the graduation target, and the transition to CLMM. ## Costs, rules, and common problems Read [fees and account deposits](https://docs.grapl.io/user-guides/fees) to separate trading costs from SOL fees and recoverable deposits. Review [hooks and risks](https://docs.grapl.io/user-guides/hooks-and-risks) for the policy and exposure of your chosen pool. If an operation fails or its outcome is unclear, follow [troubleshooting](https://docs.grapl.io/user-guides/troubleshooting) before repeating it. Building an application? Start with the dedicated [grapl SDK guide](https://docs.grapl.io/developers/sdk) or explore [Developer Guides](https://docs.grapl.io/developers). # Add liquidity Choose a range, review the required token mix, and distinguish opening a position from funding it. Source: https://docs.grapl.io/user-guides/add-liquidity Adding liquidity deposits assets into an owned position. That position contributes to trades while its range is active and earns its eligible share of LP fees. Depositing does not guarantee a return or preserve the original token mix. ## Before you begin Use a CLMM pool, including a curve that has successfully graduated. Confirm the mint addresses, current price, fee allocation, hook requirements, and your token and SOL balances. Ordinary LP deposits are disabled while a pool is still in curve mode. Decide between a [chosen range](https://docs.grapl.io/user-guides/liquidity), [full range](https://docs.grapl.io/user-guides/full-range), or a deliberate [one-sided range](https://docs.grapl.io/user-guides/one-sided). The range belongs to the position; adding more liquidity later does not change its boundaries. ## 1. Choose and review your range A range containing the current price generally needs both assets. Below or above the range, the position holds one asset. The application should show the price direction, effective lower and upper boundaries, whether the position starts active, and the amounts required after aligning boundaries to the pool's tick spacing. Do not judge a range only by its apparent fee rate. A narrower position can spend more time outside the market and require more active management. ## 2. Set the deposit budget Review the requested amounts of both tokens and maximum acceptable debits. If price moves before execution, the required mix can change; the maxima cap what the deposit may take. An arbitrary pair of wallet balances is not necessarily the ratio the position needs. Starting with only one token does not make an active two-asset position possible without conversion. A supported zap can combine a swap and deposit, with its own costs and limits. Read [one-sided liquidity and zaps](https://docs.grapl.io/user-guides/one-sided). ## 3. Open and fund the position Opening creates an empty ownership and accounting record. Adding liquidity funds that record. A compatible integration may compose operations where supported, but it must explain if it uses separate transactions. If opening succeeds and funding fails in a later transaction, an empty position can remain. Check which operation confirmed before retrying. Token account deposits, position rent, and trading principal should appear as separate amounts in the review. ## 4. Verify the funded result After confirmation, check the position owner, pool, range, liquidity, and actual token debits. A liquidity number is an accounting quantity, not a count of either deposited token. Record principal and fees separately. Adding liquidity to an existing position preserves accounting for previously earned fees; the new deposit does not earn historical fees. Continue with [position management](https://docs.grapl.io/user-guides/positions) to monitor your range, collect fees, or plan an exit. # Choose a pool Compare markets using token identity, executable amounts, active liquidity, costs, and pool rules. Source: https://docs.grapl.io/user-guides/choose-pool There can be several pools for the same token pair. The best fit depends on what you want to do and the amount involved. A trader needs an executable result; a liquidity provider also needs a range and a plan for managing inventory. ## Confirm you are comparing the same assets Match both mint addresses and the network. Then check the pool address and whether it is already a CLMM or still in [curve mode](https://docs.grapl.io/user-guides/curve-launches). Ordinary public LP deposits are unavailable during curve mode. Read the price direction carefully. “Token B per token A” and its inverse describe the same market using different numbers. Decimal differences also matter; a raw ratio of token atoms is not necessarily the displayed ratio of whole tokens. ## For a swap, compare your actual size Ask for an estimate using the amount you intend to trade. Compare expected net output for the same input, or expected input for the same required output. Include pool fees and any permitted hook output fee. Review network fees and account deposits separately because those are paid in SOL. The headline fee rate alone cannot rank pools. A pool with a lower rate may have less active liquidity, causing more price impact. Likewise, a large total value locked can include liquidity outside the price intervals your trade crosses. | Check | Why it matters | | -------------------------------- | ----------------------------------------------------------- | | Active liquidity along the trade | Determines available depth as the trade moves price | | Net quote for your amount | Reflects size, fees, and available liquidity together | | Quote age and execution limits | Bound what may happen after conditions change | | Hook requirements | May require eligibility or restrict this operation | | Result for the actual wallet | A generic estimate may not represent wallet-specific policy | ## For liquidity, compare the position you would create Look beyond a pool-wide APR. Ask whether your chosen range is active, how much competing liquidity occupies it, how often the price has left it, and what assets you would hold at either edge. Historical activity describes the past; it does not establish future fee income. Check the protocol retention and what fraction of the remaining trading fees goes to LPs versus beneficiaries. A hook may introduce additional rules or fees. Those conditions should be understandable before you fund the position. ## Make the choice reviewable Record the pool, pair, range or trade amount, fee policy, and limits in the operation review. If a new quote chooses another pool, review that pool's rules too. A familiar token does not imply identical policy across all its markets. Continue with [swapping](https://docs.grapl.io/user-guides/swaps) or [adding liquidity](https://docs.grapl.io/user-guides/add-liquidity). For costs and policy questions, use [fees](https://docs.grapl.io/user-guides/fees) and [hooks and risks](https://docs.grapl.io/user-guides/hooks-and-risks). # Collect your fees Move earned LP fees to your token accounts without confusing income, principal, and reinvestment. Source: https://docs.grapl.io/user-guides/collect-fees LP fees accrue to your position while its liquidity participates in trades. Collecting transfers that existing claim to your token accounts. It leaves the position's liquidity and range unchanged. ## Understand the balance you are claiming After mandatory protocol retention, the remaining trading fees are split between LPs and configured beneficiaries. Your position earns from the LP allocation according to its participation in the traded intervals. Volume elsewhere in the pool does not guarantee income for an inactive range. Fees are denominated in the assets in which they accrued. You may have token A fees, token B fees, or both. An estimated dollar value is not a dollar-denominated balance, and collection does not automatically convert the assets into USDC or SOL. An up-to-date estimate includes fee growth since the position's last accounting checkpoint. A raw stored balance may therefore differ from a fresh claim calculation. Integer rounding can also leave small amounts that are not immediately claimable. ## Review and collect 1. Select the owned position and verify its pool and token mints. 2. Review the estimated claim in each asset and the recipient token accounts. 3. Compare the claim with the network fee and any account deposits required. 4. Sign, confirm, and inspect the actual token transfers. The owner must authorize collection. If the transfer fails, the claim clearing and transfers roll back together. If the transaction's status is unclear, check its signature before sending a replacement. ## Fees can remain after withdrawal Removing all liquidity can leave unpaid fees. A zero-liquidity position is not necessarily empty: collect the outstanding claim before closing it. An out-of-range position can also retain fees earned earlier, even though it currently earns no new swap fees. ## Collection does not compound automatically Collected tokens become wallet balances. Reinvesting requires a new liquidity deposit with the range's required asset mix. A conversion may be necessary, introducing trading fees, price impact, and new execution limits. When tracking performance, count earned fees once. Collecting them changes where they are held; it is not a second income event. Compare the position and collected assets against the original deposits using a consistent valuation time. For a deeper explanation, see [LP performance](https://docs.grapl.io/concepts/lp-performance). ## Creator and partner claims are separate A configured fee beneficiary uses a different claim path from an LP position owner. Being the pool creator alone does not entitle a wallet to every pool fee. See [Fees and revenue](https://docs.grapl.io/protocol/fees-and-revenue) for recipient configuration, authorization, and current protocol-revenue boundaries. # Create a pool Choose a market path, starting price, fee allocation, and initial liquidity before creating a market. Source: https://docs.grapl.io/user-guides/create-pool Creating a pool establishes a market between two supported tokens. It does not create demand, establish a fair valuation, or automatically provide all the liquidity needed for trading. Token issuance and market creation are separate operations. ## 1. Choose a direct pool or a curve launch A direct CLMM starts at a selected price and is funded through liquidity positions. A curve launch begins with real inventory and a configured virtual-reserve price path, then can graduate into a CLMM after its conditions are met. Use a direct pool when you intend to choose the initial price and liquidity distribution. Read [Curve launches](https://docs.grapl.io/user-guides/curve-launches) when a staged market is appropriate. These paths have different funding and ownership implications. ## 2. Verify the assets and starting price Confirm both mint addresses, decimals, remaining token authorities, and the quote asset. Current reserve support is classic SPL Token; Token-2022 reserve assets are not supported. Review the displayed price in an explicit direction, such as units of quote token per base token. The integration must account for mint order and decimals when encoding it. A starting price is a configuration choice, not evidence of an external market price. For a direct CLMM, decide which ranges you will fund and whether the initial market will have liquidity around the starting price. An initialized pool with no usable liquidity cannot fill ordinary trades. ## 3. Decide the fees and rules | Setting | Decision to make | | ----------------------- | --------------------------------------------------------------- | | Trading fee and ceiling | What trades may cost, including any permitted dynamic fee | | LP share | What portion of the post-protocol remainder is allocated to LPs | | Beneficiaries | Which fixed recipients receive configured shares | | Tick spacing | Which position boundaries the pool supports | | Optional hook | Which policies and operations the hook controls | The current fee shares and beneficiary recipients are fixed when the pool is created. Verify addresses and allocation before signing. The Pool 6 source increment reserves 10% of trading fees before this split. Review [Fees and revenue](https://docs.grapl.io/protocol/fees-and-revenue) for its validation status and claim rules. A pool without a hook is valid. If you attach one, understand its permissions, authority, supported operations, and withdrawal conditions rather than relying on a policy name alone. ## 4. Create, fund, and verify Review account deposits and network fees separately from the assets intended for liquidity. After creation confirms, verify the pool's assets, initial price, fee configuration, and hook binding. Then [open and fund the intended position](https://docs.grapl.io/user-guides/add-liquidity). Creating a pool does not imply automatic inclusion in aggregators or discovery interfaces. Confirm actual integration availability independently. Developers can continue with [token and pool creation](https://docs.grapl.io/developers/create-market). # Understand curve launches Follow inventory, real backing, and the transition from a virtual-reserve curve to a CLMM. Source: https://docs.grapl.io/user-guides/curve-launches A curve launch starts trading against a configured price path before the pool becomes a CLMM. Its virtual reserves shape prices; its real tokens determine what it can actually pay. Virtual balances are not collateral or spendable assets. ## Read the launch terms Identify the base token being launched, the quote token used to buy it, actual inventory, protected bootstrap allocation, and graduation target. The target is measured in net quote principal, excluding fees. A target denominated in WSOL is an amount of SOL, not a fixed dollar valuation. A changing quote-token price changes any estimated fiat value of that target. No automatic oracle conversion is implied. ## Trading before graduation Buys and sells change the curve's virtual reserves and marginal price. Fees, price boundaries, wallet limits, inventory floors, and hook rules still apply. A displayed virtual quote balance does not establish that the pool can fund a seller's requested payout; it needs real quote backing after liabilities. Ordinary public LP deposits are disabled in curve mode. The configured LP fee allocation is retained for graduation, while protocol and beneficiary amounts remain separate claims. Buying the launch token does not create an LP position for the buyer. Virtual reserves determine pricing but cannot pay withdrawals. Real vault balances back principal, retained LP fees, protocol claims, and beneficiary claims; only available principal can be spent. ## What reaching the target means Reaching the target makes graduation eligible under the protocol's checks. It does not by itself complete graduation. A separate on-chain transaction must succeed, including its applicable policy and settlement checks. Graduation uses real principal and retained LP-share fees to fund a creator-owned full-range CLMM position. The pool address, vaults, mints, hook binding, and existing protocol and beneficiary liabilities remain associated with the market. The marginal price is preserved at the transition, but liquidity depth changes. Equal price immediately before and after graduation does not guarantee equal price impact for the next trade. ## Check the completed transition Look for the confirmed transaction and resulting CLMM pool mode and position. A progress bar reaching its target or a submitted signature is insufficient evidence. A failed graduation rolls back its changes. The hook stays bound to the pool, but a policy can behave differently in curve and CLMM modes. Read its rules for both phases. After successful graduation, ordinary CLMM operations become available subject to those rules. Any recorded graduation remainder is distinct from user funds, protocol claims, and beneficiary fees; the creator has the supported claim path for that remainder. It is not an automatic distribution to token holders. For configuration and exact accounting, see [Virtual reserves and graduation](https://docs.grapl.io/concepts/bonding-curve). For trading review, use [Swap tokens](https://docs.grapl.io/user-guides/swaps). # Fees and account deposits Separate trading costs, Solana network fees, and refundable account deposits. Source: https://docs.grapl.io/user-guides/fees The amount you trade or deposit is not your only cost. A useful transaction review shows each category independently. | Cost | Paid in | What happens to it | | ---------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------- | | Pool trading fee | The input token | 10% protocol retention, then the remainder to LPs and configured beneficiaries | | Optional hook output fee | The output token | Deducted under the pool's cap and paid to its declared recipient | | Solana network fee | SOL | Pays for the transaction, including any selected priority fee | | Account deposit, often called rent | SOL | Funds a new on-chain account; recoverable only when that account supports closure and is closed | The Pool 6 source increment retains 10% of the trading fee for the protocol. Creator or partner shares come from the remainder and require configured beneficiaries. See [Fees and revenue](https://docs.grapl.io/protocol/fees-and-revenue). ## Pool and hook fees The pool configuration sets its fee allocation. A supported hook can choose a dynamic input fee within the pool's ceiling or request an allowed output fee. Rates can differ across pools and over time. Check the quote and the amount you will receive after output fees. For an illustrative 10-unit trading fee, the protocol retains 1 unit. A 70% LP share of the remaining 9 units allocates 6.3 to LPs and 2.7 to beneficiaries. The 70/30 split is an example, not a required configuration; actual amounts use integer token atoms and rounding. [Fee accounting](https://docs.grapl.io/concepts/fees) explains the exact rules. ## Network fees are separate Solana charges transaction fees to the fee payer. A failed on-chain transaction can still incur that fee. A priority fee is a network cost, not additional income for the pool or a promise that the transaction will succeed. See [Solana's fee documentation](https://solana.com/docs/core/fees). ## Rent is an account deposit Creating a token account or position may lock SOL as a rent-exempt balance. It is not a trading fee or a recurring charge on each swap. Some deposits can be recovered when an eligible account closes; shared pool storage does not automatically close when you exit a position. The required balance depends on account size and network parameters. See [Solana accounts](https://solana.com/docs/core/accounts). # Provide full-range liquidity Understand the broadest supported range, its capital tradeoff, and what it does not automate. Source: https://docs.grapl.io/user-guides/full-range A full-range position uses the widest valid boundaries supported by the pool's tick spacing. It uses the same principal and fee accounting as other concentrated-liquidity positions; it is not a different token or a managed vault. ## What changes when you choose full range A wider range distributes your capital across more prices. Compared with a narrower position using the same budget near the current market, it generally supplies less liquidity at that particular price. The tradeoff is less sensitivity to ordinary movement beyond a narrowly chosen boundary. “Full range” still means the protocol's supported, aligned price interval. It does not mean an unlimited price domain or a promise of continuous trading and income. ## Funding still depends on price At an interior price, funding a full-range position needs both assets in the required proportions. A wallet holding only token A cannot skip the token B requirement. A supported zap can obtain the second asset through a swap, but that introduces conversion costs and limits. Use [Add liquidity](https://docs.grapl.io/user-guides/add-liquidity) to review the effective boundaries, required amounts, maximum debits, and account deposits. The application should identify that you selected full range and show the actual range used. ## The token mix continues to change As trading moves price, your position's asset composition changes. Full range does not preserve a fixed 50/50 wallet allocation. It also does not protect against a token losing value, an issuer changing supply, or a pool or hook failure. Fee income depends on trading through active liquidity and the pool's fee split. It may not compensate for the difference between providing liquidity and simply holding the original assets. ## Manage it as a position Full range does not collect fees, rebalance, or reinvest automatically. You still own a position with separate principal and fee claims. Partial withdrawals keep the remaining range; full withdrawal, fee collection, and closure are distinct operations. A curve's graduation can create a creator-owned full-range position. That does not make curve buyers owners of that position. Read [Curve launches](https://docs.grapl.io/user-guides/curve-launches) before interpreting graduation as an LP allocation. Continue with [position management](https://docs.grapl.io/user-guides/positions) or compare [one-sided liquidity](https://docs.grapl.io/user-guides/one-sided). # Getting started Prepare your wallet, identify the right assets, and understand the steps from a quote to a confirmed operation. Source: https://docs.grapl.io/user-guides/getting-started Start with three things: a wallet you control, the correct network, and a clear goal. A swap changes the tokens you hold. Providing liquidity creates a position whose token mix changes as other people trade. Creating a pool establishes a market that still needs liquidity. These guides describe grapl through a compatible application. They do not assume a particular set of buttons or advertise a public deployment. Check [protocol status](https://docs.grapl.io/start/status) for the current availability of each capability. ## Prepare your wallet and balances Your wallet authorizes operations; connecting it to an application is not the same as signing a transaction. Confirm that the application and wallet use the intended network and program deployment before reviewing a request. You need the assets being traded or deposited, plus SOL for transaction fees and any account deposits you fund. Leaving exactly your trade amount in the wallet may be insufficient if new token or position accounts are needed. The transaction review should show these costs separately; there is no universal SOL amount that fits every operation. Native SOL and wrapped SOL are related but distinct balances. A pool using WSOL trades the SPL token representation. Wrapping and unwrapping must be handled explicitly by the integration; do not assume that any native SOL balance is immediately spendable by the pool. ## Identify tokens by address Check the mint address of each asset and the address of the pool. Names, symbols, and logos can be reused. Two markets with the same displayed pair can differ in their reserve assets, fees, available liquidity, and hook rules. If a token is unfamiliar, investigate its issuance and remaining authorities before using it. A token appearing in a pool does not make its value stable or establish the issuer's trustworthiness. ## Pick your first path | Your goal | Start here | What to decide | | ------------------------------ | ----------------------------------------------- | ----------------------------------------------------------------- | | Exchange one token for another | [Swap tokens](https://docs.grapl.io/user-guides/swaps) | Input budget or required output, execution limits, and pool rules | | Earn a share of trading fees | [Choose your liquidity](https://docs.grapl.io/user-guides/liquidity) | Range, token budget, and how you will monitor the position | | Open a market for a token | [Create a pool](https://docs.grapl.io/user-guides/create-pool) | Assets, starting price, fee allocation, and initial liquidity | | Understand a curve launch | [Curve launches](https://docs.grapl.io/user-guides/curve-launches) | Real backing, target, and graduation conditions | ## Review, sign, and verify Before signing, confirm the assets, spending limits, minimum receipts, recipients, pool rules, and SOL costs. A successful simulation checks the proposed operation against a recent state; it does not reserve that state. After submission, wait for confirmation and inspect the executed result. A pending signature is not a completed swap or funded position. Use the [troubleshooting guide](https://docs.grapl.io/user-guides/troubleshooting) if the result is unclear, and retain the signature before trying again. # Pool rules and risks Review optional hooks, changing prices, and the limits of a successful simulation. Source: https://docs.grapl.io/user-guides/hooks-and-risks A **hook** is an external program attached to a pool. It can enforce rules at specific moments, such as before a trade or after a liquidity change. A pool without a hook is valid. Allowlists, trading windows, reward policies, and dynamic fees are optional examples, not restrictions on every grapl pool. ## Read the rules before entering Check which operations the hook controls, who can update its configuration or program, what proofs it needs, and its permitted fees. A policy that permits entry may place separate conditions on withdrawal. An application that does not understand the required hook should reject the action rather than omit it. Rewards belong to the particular hook. A credit balance is not automatically transferable, redeemable, or part of your swap output. ## Understand liquidity risk Relative token prices change the assets held by your position. Compared with holding the original tokens, providing liquidity can leave you with less value; fee income may not offset the difference. Out-of-range positions stop earning new swap fees until the market returns. Full range still exposes you to token price changes. Also consider token issuer powers, pool depth, hook and protocol bugs, upgrade authorities, and the interface or RPC you use. A test result or simulation does not establish an independent audit or production readiness. ## Review what you sign Verify the network, program, assets, recipients, spend limits, minimum received amounts, fee payer, and accounts being created or closed. Simulation checks one state snapshot. The state can change before inclusion, so your on-chain limits remain essential. See [Security](https://docs.grapl.io/security/model) for trust boundaries, [limitations](https://docs.grapl.io/security/limitations) for unsupported features, and [Hooks](https://docs.grapl.io/hooks/overview) for technical details. # Choose your liquidity Compare concentrated, full-range, and one-sided liquidity without starting from the math. Source: https://docs.grapl.io/user-guides/liquidity A position sets a lower and upper price. It contributes to trading while the market price is inside that range. As traders exchange tokens, the mix of assets in your position changes. Range example: positions A and B contain the current price and contribute active liquidity. Position C is out of range and inactive. This is a schematic, not market data. | Choice | What it does | What to consider | | ------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | Concentrated range | Puts liquidity in a chosen price interval | More liquidity near that interval, but the position can become inactive | | Full range | Uses the widest supported, aligned interval | Spreads capital across more prices; it does not promise returns or eliminate price risk | | One-sided range | Deposits one asset when the entire range is on one side of the current price | Starts out of range and does not earn swap fees until trading reaches it | ## When two assets are needed An ordinary deposit into a range containing the current price needs both assets in the pool's required proportions. Selecting a narrow range does not remove that requirement. If the price is outside the range, the position consists of one asset. A **zap** combines a swap with a liquidity action so you can enter or exit from one asset. That conversion has its own fees, price impact, limits, and possible hook requirements. A one-sided deposit outside the range and a zap into an active range are different operations. ## When your position leaves the range The position stops contributing active liquidity and earning new swap fees until price returns. Accrued fees remain a separate claim. You can remove liquidity, then open a different range; a position does not automatically follow the market. Choose a range only after understanding [position management](https://docs.grapl.io/user-guides/positions) and [risks](https://docs.grapl.io/user-guides/hooks-and-risks). Continue with the practical guides to [full range](https://docs.grapl.io/user-guides/full-range), [one-sided liquidity and zaps](https://docs.grapl.io/user-guides/one-sided), or [adding liquidity](https://docs.grapl.io/user-guides/add-liquidity). The [technical range guide](https://docs.grapl.io/concepts/concentrated-liquidity) explains ticks and alignment. # One-sided liquidity and zaps Distinguish an out-of-range deposit from converting one asset into an active liquidity position. Source: https://docs.grapl.io/user-guides/one-sided Starting with one asset can lead to two different operations. You can fund a position whose range lies entirely on one side of the current price, or use a supported zap to convert part of your asset and fund a range that needs both tokens. ## A one-sided range starts outside the market When price is outside a position's range, its principal is concentrated in one asset. This can make a one-token deposit possible, depending on which side of the current price the range occupies. The position initially supplies no active trading liquidity and earns no new swap fees. If trading moves into its range, its composition starts changing and it can participate in fee accrual. If price later moves back, that composition can reverse. This is not an irreversible limit-order fill. The position does not automatically withdraw after converting to the other asset. Native limit orders and automated position management are separate product proposals, not features of this position type. ## A zap obtains the required second asset For an active or full-range position, a zap in combines a swap and a liquidity deposit. You supply a single-asset budget, a chosen conversion amount, and limits. The operation uses its actual swap output plus the remaining budget to fund liquidity at the resulting price. The correct conversion is not always half your budget. It depends on price, range, fees, and the swap's own price impact. The current protocol does not include an optimal-split planner; a compatible integration must prepare and simulate the complete composition. | Review item | Why it matters | | ------------------------------ | --------------------------------------------------------------- | | Total input budget | Caps the single asset committed across the operation | | Swap output and price limits | Bound the conversion leg | | Minimum liquidity | Prevents accepting too little liquidity after conversion | | Both sets of hook requirements | The swap and deposit each retain their applicable rules | | Unused assets | Rounding and ratio differences can leave balances in the wallet | ## Exiting into one asset A zap out removes principal and converts the other withdrawn asset toward your desired token. It does not collect outstanding LP fees or spend unrelated wallet balances. If there is insufficient liquidity to complete conversion, the whole operation fails. Removing the pool's last usable liquidity may leave nothing against which to swap. A one-asset exit therefore cannot be assumed for every position or every full withdrawal. The protocol supports zap instructions, but the direct SDK has no dedicated zap convenience methods yet. Availability in an application depends on its integration. For detailed semantics, read [Zaps](https://docs.grapl.io/guides/zaps); for ordinary withdrawal, use [Remove liquidity](https://docs.grapl.io/user-guides/remove-liquidity). # Understand a pool Learn what a liquidity pool holds and how a trade changes its price. Source: https://docs.grapl.io/user-guides/pools A pool connects two tokens. Liquidity providers deposit assets into positions; traders pay a fee to exchange one asset for the other. The pool's available liquidity and current price determine how much a trade can receive. ## Identify the market Check both token mint addresses, their decimals, the pool address, and the network. A familiar ticker or logo does not prove token identity. Two pools for the same pair can have different liquidity, fees, and hooks. ## Understand your trade **ExactIn** starts from an input budget and a minimum acceptable output. **ExactOut** requests an output amount and caps the input. A quote reflects a moment in time. Your transaction's limits protect against a worse result; they do not reserve the quoted liquidity. Price impact is the movement caused by your own trade. Slippage tolerance is the extra movement you permit between the quote and execution. A deeper pool can offer a better result even when its percentage fee is higher. The protocol can expose partial consumption in some ExactIn operations. An application should clearly show the settled amounts and unspent input. Never interpret a submitted signature as a completed trade. ## Two ways a market begins A **direct pool** starts with a price and liquidity positions. A **bonding curve** starts with a configured price path and real inventory; when its conditions are met, it can graduate into a concentrated liquidity pool. Graduation changes the trading phase and needs a successful on-chain transaction. Continue with [liquidity choices](https://docs.grapl.io/user-guides/liquidity), [fees and costs](https://docs.grapl.io/user-guides/fees), or the deeper [launch explanation](https://docs.grapl.io/concepts/launches). # Manage your position Follow a position from its first deposit through withdrawal, fee collection, and closure. Source: https://docs.grapl.io/user-guides/positions A position records your range, liquidity, and claimable fees. Its liquidity amount is an accounting unit; it is not the same as the number of tokens you deposited. ## Open and fund Review the token pair, range, pool rules, and maximum token amounts before signing. Opening a position creates its account. Adding liquidity funds it and may require other accounts to be created. Your interface should distinguish the deposit from the SOL needed for fees and account deposits. Follow [Add liquidity](https://docs.grapl.io/user-guides/add-liquidity) for the complete sequence. ## Track principal and fees separately **Principal** is the changing mix of assets backing your liquidity. **Accrued fees** are your share of fees earned while your range participates in swaps. A position's apparent value can rise or fall even while it earns fees. Collecting fees does not automatically reinvest them. An APR or projected APY is not a guaranteed return and does not include every risk or cost. ## Remove liquidity Removing some or all liquidity returns the assets represented by that liquidity at the current price. Set minimum acceptable amounts. You may receive a different mix from your original deposit, including only one asset when out of range. Use [Remove liquidity](https://docs.grapl.io/user-guides/remove-liquidity) to review partial withdrawals and one-asset exits. ## Collect and close [Collect outstanding fees](https://docs.grapl.io/user-guides/collect-fees) separately. A position can close only once its liquidity and owed balances are cleared. Closing an eligible position returns its remaining SOL deposit to its owner; it does not close every shared pool or tick account. Check the confirmed result and balances after each operation. A hook may impose additional requirements on position actions. See [costs](https://docs.grapl.io/user-guides/fees) and the [LP lifecycle reference](https://docs.grapl.io/concepts/lp-lifecycle). # Remove liquidity Withdraw part or all of a position while preserving minimum receipts and outstanding fee claims. Source: https://docs.grapl.io/user-guides/remove-liquidity Removing liquidity returns the principal represented by part or all of your position. The token mix depends on the current price and your range. You may receive different proportions from your initial deposit, or a single asset when the position is outside its range. ## 1. Decide how much to remove A partial withdrawal keeps the remaining liquidity in the same range. A full withdrawal takes liquidity to zero, but it does not automatically claim every fee or close the position account. Check the position owner and pool before selecting an amount. A displayed currency valuation is an estimate; the withdrawal settles quantities of the pool's actual tokens. ## 2. Review expected receipts and minima The review should show expected token A and token B principal, minimum acceptable receipts, and network costs. Removal uses minimum receipts, unlike an addition's maximum spending limits. Price movement and integer rounding can change the exact amounts. An expected zero amount on one side may be correct for an out-of-range position. Understand that composition before accepting the transaction; a generic “withdraw value” number can hide which asset you will receive. ## 3. Satisfy the pool's rules The owner must authorize removal, and a configured removal hook may impose additional requirements. Eligibility to deposit does not necessarily imply unconditional eligibility to withdraw. Read the pool's withdrawal policy before funding it, and check the specific rejection if removal is denied. A normal withdrawal returns the pool assets. A supported zap out also converts withdrawn principal toward a chosen asset, adding swap fees, price impact, execution limits, and swap-hook requirements. It does not collect outstanding LP fees. Removing the last usable liquidity can make that conversion impossible in the same pool. ## 4. Confirm principal, then handle fees Verify the signature, actual receipts, and remaining position liquidity. A failed transaction rolls back the liquidity change and transfers together; an unclear status should be resolved before retrying. Full removal can leave accrued fees on a position with zero liquidity. [Collect those fees](https://docs.grapl.io/user-guides/collect-fees) separately. Once both liquidity and fee balances are zero, the position can close and return its account deposit to the owner. Shared pool and tick storage is not automatically closed when you leave. Closing your position also does not close unrelated wallet token accounts. For the complete sequence, use [Manage your position](https://docs.grapl.io/user-guides/positions). # Swap tokens Choose an input or output amount, set meaningful limits, and verify what your trade actually settled. Source: https://docs.grapl.io/user-guides/swaps A swap exchanges one pool asset for the other. Before starting, confirm your [wallet and token balances](https://docs.grapl.io/user-guides/getting-started), identify the [pool](https://docs.grapl.io/user-guides/choose-pool), and read any hook rules that apply to your wallet. ## 1. Choose the amount you want to control **ExactIn** starts with an input budget and a minimum acceptable output. **ExactOut** starts with the output you require and a maximum acceptable input. Use the mode that matches your intent, then review both sides of the estimate. An ExactIn operation can leave input unspent when a price limit is reached, provided its output requirements are still met. Do not assume that every successful ExactIn trade spends the entire displayed budget. ExactOut must satisfy its requested output while respecting its input cap. ## 2. Read the estimate and costs Check expected input, net output, price impact, trading fees, and any hook output fee. Network and priority fees are separate SOL costs. Creating a missing token account may also require a SOL deposit. Price impact comes from your own trade moving through available liquidity. Slippage tolerance covers permitted movement between the estimate and execution. Increasing tolerance does not create more liquidity or improve the quoted price. For illustration, an estimated net receipt of 100 tokens with a 1% tolerance corresponds to a 99-token minimum before any required atom rounding. This is an explanation of the limit, not a suggested setting. The integration must apply its limits to the actual settlement semantics, including permitted fees. ## 3. Review the transaction Confirm the two mints, pool, recipient accounts, input maximum or budget, output requirement, and price boundary. A hook may require proof or reject a wallet; accepting a larger slippage tolerance does not satisfy that policy. Sign only after the review describes the operation you intended. Simulation can reveal a current failure, but conditions may change before the transaction lands. ## 4. Confirm and reconcile Keep the signature and wait for the result. Check the actual debited and credited token amounts, network fee, and any unspent input. Account creation and unrelated wallet activity can make a single total-balance change misleading; the transaction receipt is the starting point. If the transaction fails, its state changes roll back, although an on-chain failure can still cost a network fee. If its status is unknown, check the original signature before submitting a replacement. See [troubleshooting](https://docs.grapl.io/user-guides/troubleshooting). ## When a swap cannot complete A stale estimate, insufficient active liquidity, a strict price boundary, an expired blockhash, insufficient SOL, or a hook rejection can stop a trade. Review the specific reason and obtain a fresh estimate. Removing every limit or repeatedly resubmitting the same intent is not a reliable recovery method. For fee details, continue to [fees and account deposits](https://docs.grapl.io/user-guides/fees). Developers can use the [swap integration guide](https://docs.grapl.io/developers/swaps). # Troubleshoot an operation Resolve unclear status, failed swaps, inactive positions, and leftover fee claims without repeating an operation blindly. Source: https://docs.grapl.io/user-guides/troubleshooting Start with the operation's signature, network, pool, and wallet. Separate what the application prepared, what the wallet signed, what was submitted, and what the network confirmed. Those stages describe different outcomes. ## A transaction is pending or missing Check the original signature through the integration's status view or a compatible explorer on the correct network. An RPC timeout means the response is missing; it does not prove the transaction failed. If the transaction expired without landing, the application must prepare a fresh one and request new signatures. If the result remains unknown, resolve that uncertainty before repeating a deposit, swap, or claim. A second valid transaction can execute the operation again. ## A swap failed | Reported cause | What to review next | | ---------------------------------------- | ---------------------------------------------------------------------------------- | | Minimum output or maximum input exceeded | Refresh the estimate and review market movement and your chosen bounds | | Price limit or insufficient liquidity | Check available depth for your amount and direction | | Insufficient token balance | Check the actual mint and spendable token account, including WSOL where applicable | | Insufficient SOL | Review the network fee and account deposits in addition to the token budget | | Hook or eligibility rejection | Read the policy, required proof, and supported wallet context | | Oversized transaction | The integration may need valid ALTs or a redesigned explicit sequence | Do not remove limits merely to make a failure disappear. A different pool can have different fees and hook rules, so review any replacement operation from the beginning. ## A position is not earning fees Check whether the position is funded and its range is active. An empty account earns nothing, and an out-of-range position does not participate until trading reaches its interval. Pool-wide volume is not evidence that your range was crossed. Next, check the pool's LP allocation and whether the display includes fee growth since the last checkpoint. Keep principal changes separate from fee income. See [Collect your fees](https://docs.grapl.io/user-guides/collect-fees). ## Withdrawal left tokens or the position will not close A normal withdrawal returns the current token mix, which can differ from the deposit. Removing all liquidity can leave unclaimed fees. Closure requires zero liquidity and cleared fee balances, and refunds only the eligible position account's deposit. If a one-asset exit failed, its conversion may lack liquidity or violate a hook or execution limit. An ordinary two-asset withdrawal has different semantics; review it as a new operation. ## Information to retain for support Keep the public transaction signature, pool and mint addresses, intended action, timestamp, displayed error, and application version where available. Do not share seed phrases, private keys, API secrets, or wallet recovery material. The [security model](https://docs.grapl.io/security/model) and [known limitations](https://docs.grapl.io/security/limitations) explain integration boundaries. A confirmed result and reconciled balances are stronger evidence than a success toast alone.