# Get Pool
Source: https://docs.hadron.fi/api-reference/get-pool
GET https://api.hadron.fi/pools/{address}
Returns detailed information for a single Hadron pool.
## Path Parameters
The pool config PDA address.
## Query Parameters
Solana cluster — `mainnet-beta` or `devnet`.
## Response
Returns the same pool object shape as the [List Pools](/api-reference/list-pools) endpoint.
```json theme={null}
{
"address": "7ZRLTJBvJSy9VDyZmDoGW4b7noorCYnLUisyAXh7hxMz",
"name": "SOL/USDC",
"token_x": {
"address": "So11111111111111111111111111111111111111112",
"symbol": "SOL",
"name": "SOL",
"decimals": 9,
"icon": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png"
},
"token_y": {
"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"symbol": "USDC",
"name": "USDC",
"decimals": 6,
"icon": ""
},
"token_x_amount": 5413038034,
"token_y_amount": 515256215,
"pool_config": {
"authority": "Bk2qYNMa4hUkbutBcb4JHhMqAbGEFZq4KjicpMCwC2wn",
"quoting_authority": "Bk2qYNMa4hUkbutBcb4JHhMqAbGEFZq4KjicpMCwC2wn",
"oracle_mode": 0,
"pool_state": "Initialized",
"base_spread_bps": 5.0
},
"tvl": 990.81,
"current_price": 87.85,
"volume": { "24h": 364.16 },
"fees": { "24h": 0.59 }
}
```
## Errors
| Status | Message |
| - | - |
| `404` | `Pool not found` |
# List Pools
Source: https://docs.hadron.fi/api-reference/list-pools
GET https://api.hadron.fi/pools
Returns a paginated list of all Hadron pools with token metadata, TVL, volume, and fees.
## Query Parameters
All filters combine with logical **AND** and stack with `query` search and
`cluster`. `total` and `pages` reflect the filtered result set. An unrecognized
`pool_state` value simply returns zero matches.
1-based page number.
Number of pools per page. Maximum `100`.
Search by pool name, token symbol, mint address, or pool address (configPda).
Filter to pools whose `authority` exactly matches this pubkey.
Filter to pools whose `quoting_authority` exactly matches this pubkey.
Filter to pools that include this mint address on **either** side (`token_x` or `token_y`).
Filter to pools whose base token (`token_x`) mint matches this address.
Filter to pools whose quote token (`token_y`) mint matches this address.
Filter by pool state. Case-insensitive. One of `Uninitialized`, `Initialized`, `Paused`, `RejectSwaps`, `Closed`.
Sort field and direction. Format: `field:direction`.
**Fields:** `tvl`, `volume_24h`, `fees_24h`
**Directions:** `asc`, `desc`
Solana cluster — `mainnet-beta` or `devnet`.
## Response
Total number of pools matching the query.
Total number of pages.
Current page number.
Number of pools per page.
Array of pool objects.
Pool config PDA address.
Pool display name (e.g. `SOL/USDC`).
Base token metadata.
Mint address.
Token symbol.
Token name.
Token decimals.
Token icon URL.
Quote token metadata (same shape as `token_x`).
Raw vault X balance (base token).
Raw vault Y balance (quote token).
Pool configuration.
Pool authority pubkey.
Quoting authority pubkey.
Oracle mode (0 = default).
Pool state — one of `Uninitialized`, `Initialized`, `Paused`, `RejectSwaps`, or `Closed`.
Current base spread in basis points.
Total value locked in USD.
Current midprice (human-readable, e.g. `87.85` for SOL/USDC).
24-hour trading volume in USD.
24-hour fees collected in USD.
```json theme={null}
{
"total": 3,
"pages": 1,
"current_page": 1,
"page_size": 20,
"data": [
{
"address": "7ZRLTJBvJSy9VDyZmDoGW4b7noorCYnLUisyAXh7hxMz",
"name": "SOL/USDC",
"token_x": {
"address": "So11111111111111111111111111111111111111112",
"symbol": "SOL",
"name": "SOL",
"decimals": 9,
"icon": "https://raw.githubusercontent.com/solana-labs/token-list/main/assets/mainnet/So11111111111111111111111111111111111111112/logo.png"
},
"token_y": {
"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"symbol": "USDC",
"name": "USDC",
"decimals": 6,
"icon": ""
},
"token_x_amount": 5413038034,
"token_y_amount": 515256215,
"pool_config": {
"authority": "Bk2qYNMa4hUkbutBcb4JHhMqAbGEFZq4KjicpMCwC2wn",
"quoting_authority": "Bk2qYNMa4hUkbutBcb4JHhMqAbGEFZq4KjicpMCwC2wn",
"oracle_mode": 0,
"pool_state": "Initialized",
"base_spread_bps": 5.0
},
"tvl": 990.81,
"current_price": 87.85,
"volume": { "24h": 364.16 },
"fees": { "24h": 0.59 }
}
]
}
```
# Midprice History
Source: https://docs.hadron.fi/api-reference/midprice-history
GET https://api.hadron.fi/pools/{address}/midprice/history
Returns sampled midprice history for a pool over a time range.
## Path Parameters
The pool config PDA address.
## Query Parameters
Sample interval. One of: `1m`, `5m`, `30m`, `1h`, `2h`, `4h`, `12h`, `24h`, `raw`.
When set to `raw`, every recorded midprice point is returned without bucketing/sampling. Results are paginated via `cursor`.
Start of the time range as a Unix timestamp in seconds (inclusive). Defaults to `end_time - 86400` (24 hours ago).
End of the time range as a Unix timestamp in seconds (inclusive). Defaults to current time.
Number of records per page (1–1000). Only applicable when `timeframe=raw`.
Unix timestamp in seconds. Fetch records **before** this timestamp (newest first). Use the `next_cursor` value from a previous response to paginate backwards through history. Only applicable when `timeframe=raw`.
Solana cluster — `mainnet-beta` or `devnet`.
## Response
Start of the requested range (Unix seconds).
End of the requested range (Unix seconds).
The sample interval used.
Timestamp of the oldest record in the current page (Unix seconds). Pass as `cursor` to fetch the next (older) page. `null` when there are no more records. Only present when `timeframe=raw`.
Array of midprice samples. One entry per time bucket that contains data (sampled modes), or every recorded point (`raw` mode).
Sample timestamp (Unix seconds).
Solana slot number when this midprice update was confirmed on-chain.
Raw midprice value. This is the on-chain Q32-decoded midprice representing `token_y per token_x` in raw units. To get a human-readable price, multiply by `10^(decimals_x - decimals_y)`.
Base spread factor (Q32-decoded). A value near `1.0` means minimal spread. The spread in basis points is `(1 - base_spread_factor) * 10000`.
Base58 transaction signature of the `UpdateMidprice` or `UpdateMidpriceAndSpreadFactor` instruction that produced this update. `null` for historical records ingested before signature tracking was added.
```json Sampled (1h) theme={null}
{
"start_time": 1773164464,
"end_time": 1773250864,
"timeframe": "1h",
"data": [
{
"timestamp": 1773164464,
"slot": 410333466,
"midprice": 0.08771598688326776,
"base_spread_factor": 0.9994999999180436,
"signature": "E6cGjfbdRs4ShbmZc1AHeLZg7vN79PYHFWe4..."
},
{
"timestamp": 1773168065,
"slot": 410342211,
"midprice": 0.08653769129887223,
"base_spread_factor": 0.9994999999180436,
"signature": "3Ni7GhiHoPmnUxcgpPMYW61qPkxUunj4WwB9..."
}
]
}
```
```json Raw (paginated) theme={null}
{
"start_time": 1710000000,
"end_time": 1710086400,
"timeframe": "raw",
"next_cursor": 1710003600,
"data": [
{
"timestamp": 1710000000,
"slot": 409267135,
"midprice": 145.23,
"base_spread_factor": 0.9997,
"signature": "44txvmp2QQJa9XLkG8Gj2zHnBScRkXxL5dHd..."
},
{
"timestamp": 1710000400,
"slot": 409268012,
"midprice": 145.45,
"base_spread_factor": 0.9997,
"signature": "5AUySbnZ8qjk1Zr7VqTW8tPKJ9Gm3CfWgJ7x..."
},
{
"timestamp": 1710003600,
"slot": 409275890,
"midprice": 145.12,
"base_spread_factor": 0.9995,
"signature": null
}
]
}
```
## Errors
| Status | Message |
| - | - |
| `400` | `Invalid timeframe` |
| `400` | `start_time and end_time must be valid unix timestamps in seconds` |
| `400` | `cursor must be a valid unix timestamp in seconds` |
# API Overview
Source: https://docs.hadron.fi/api-reference/overview
REST API for accessing Hadron pool data, midprice history, volume, swaps, transactions, fees, and protocol metrics.
## Base URL
```
https://api.hadron.fi
```
## Authentication
Pass an optional API key via the `x-api-key` header to unlock higher rate limits.
| Tier | Rate Limit | Identifier |
| - | - | - |
| No API key | 10 req/min | IP address |
| With API key | Default 120 req/min | API key |
Contact the Hadron team to request an API key for production integrations.
# Pool Fees
Source: https://docs.hadron.fi/api-reference/pool-fees
GET https://api.hadron.fi/pools/{address}/fees
Fees a pool captured over a time window — pool-captured spread and protocol fee, with the protocol fee broken down per fee-split recipient.
Aggregates the fees a pool earned over the selected window into two parts:
* **Pool-captured spread** — the spread the pool's curve earned (price curve, risk curve, base spread), excluding the protocol fee.
* **Protocol fee** — the protocol fee captured, broken down per recipient for pools that route fees through the fee-split sidecar.
Computed on-the-fly over the window's swaps. For very high-volume windows the response sets `truncated: true` (see the field below).
## Path Parameters
The pool config PDA address.
## Query Parameters
Lookback window ending now. One of `1h`, `24h`, `7d`, `30d`, or `all`
(`all` aggregates the pool's full history).
Solana cluster — `mainnet-beta` or `devnet`.
## Response
The interval that was applied (echoes the request).
Start of the window (Unix seconds). `0` when `interval=all`.
End of the window (Unix seconds) — the request time.
Pool's token X (base) mint.
Pool's token Y (quote) mint.
Aggregate fee totals over the window.
Total swap volume over the window, quote-denominated.
Pool-captured spread over the window (price/risk curve + base spread), quote-denominated. Excludes the protocol fee.
Total protocol fee captured over the window, quote-denominated.
Protocol fee collected in raw token-X atoms (from sells).
Protocol fee collected in raw token-Y atoms (from buys).
Number of swaps in the window.
Per-recipient split of the protocol fee over the window. Empty (`[]`) for pools that use the default (single) fee recipient, for windows with no fee-bearing split swaps, or for windows entirely before per-swap split tracking began.
The sum of recipients' `fee_usd` equals `totals.protocol_fee_usd` only when **every** fee-bearing swap in the window carried a split; it is less when the window also includes default-fee swaps or pre-tracking swaps.
Recipient wallet address.
Most recently observed split weight (parts-per-million) for this recipient in the window.
Recipient's share of the protocol fee over the window, quote-denominated.
Recipient's share in raw token-X atoms (from sells).
Recipient's share in raw token-Y atoms (from buys).
`true` if the per-query swap cap was reached, meaning the window contains more swaps than were aggregated. Narrow the `interval` for a complete result.
```json theme={null}
{
"interval": "7d",
"start_time": 1772634648,
"end_time": 1773239448,
"mint_x": "So11111111111111111111111111111111111111112",
"mint_y": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"totals": {
"volume_usd": 184320.45,
"spread_usd": 122.84,
"protocol_fee_usd": 36.91,
"protocol_fee_x_raw": 41230000,
"protocol_fee_y_raw": 33110000,
"swap_count": 612
},
"protocol_fee_split": [
{
"recipient": "Rec1",
"ppm": 700000,
"fee_usd": 25.84,
"fee_x_raw": 28861000,
"fee_y_raw": 23177000
},
{
"recipient": "Rec2",
"ppm": 300000,
"fee_usd": 11.07,
"fee_x_raw": 12369000,
"fee_y_raw": 9933000
}
],
"truncated": false
}
```
# Protocol Metrics
Source: https://docs.hadron.fi/api-reference/protocol-metrics
GET https://api.hadron.fi/stats/protocol_metrics
Returns protocol-wide aggregate statistics across all Hadron pools.
## Query Parameters
Solana cluster — `mainnet-beta` or `devnet`.
## Response
Total value locked across all pools in USD.
Total 24-hour trading volume across all pools in USD.
Total 24-hour fees collected across all pools in USD.
Total number of swaps in the last 24 hours.
Total number of Hadron pools.
```json theme={null}
{
"total_tvl": 0,
"volume_24h": 364.16,
"fee_24h": 0.59,
"swap_count_24h": 6,
"total_pools": 3
}
```
# Recent Swaps
Source: https://docs.hadron.fi/api-reference/recent-swaps
GET https://api.hadron.fi/pools/{address}/swaps
Returns swap events for a pool, ordered newest first. Cursor-paginated — you can walk the full history of a pool by following `next_cursor`.
## Path Parameters
The pool config PDA address.
## Query Parameters
Page size — number of swaps to return per request. Maximum `200`.
Opaque pagination cursor. Omit on the first request; on subsequent requests, pass the `next_cursor` value from the previous response to fetch the next page. When the server returns `next_cursor: null` (equivalently `has_more: false`), you have reached the end of the pool's history.
Solana cluster — `mainnet-beta` or `devnet`.
Only return swaps whose **quote (Y) leg** is at least this many raw quote atoms. The quote leg is `input_amount` on a `buy` (Y to X) and `output_amount` on a `sell` (X to Y). For a USDC-quoted pool, `1000000` means "at least 1 USDC". Use this to keep dust trades out of a page.
Only return swaps with `slot` strictly greater than this value. Made for polling: remember the highest `slot` you have processed and pass it back on the next call to receive just the swaps that landed since. `has_more: false` in the response means the walk reached the bound and nothing was left out.
Only return swaps with `timestamp` strictly greater than this value, compared in whole seconds (the unit the response uses), so feeding back `data[0].timestamp` never repeats a swap. Accepts unix seconds (`1788659015`) or an ISO-8601 date-time (`2026-09-06T02:23:35Z`); a string without a zone designator is read as UTC. Numbers of `1e12` or more are treated as unix milliseconds. Two swaps landing in the same second as your bound are excluded together; when a gapless feed matters, prefer `after_slot`, which is exact.
**How filtered pages are built.** `min_quote_amount`, `after_slot` and `after_timestamp` can be combined. The API walks the pool's history newest-first in blocks of 200 rows and keeps the rows that pass, until it has `limit` of them, reaches the `after_slot` or `after_timestamp` bound, or has scanned 1,000 rows. If the scan budget runs out, the page is returned with `has_more: true` and a `next_cursor` that resumes the walk, and it may hold fewer than `limit` rows (even none). Cursors from filtered requests look different from unfiltered ones; both are opaque, so always pass back exactly what you received. Following a filtered cursor never repeats or skips a swap, even when new swaps arrive in between.
Polling recipe (every minute, ignoring anything under 5 USDC):
```bash theme={null}
curl 'https://api.hadron.fi/pools/{address}/swaps?after_slot=LAST_SEEN_SLOT&min_quote_amount=5000000' \
--header 'x-api-key: YOUR_KEY'
```
Take `data[0].slot` from a non-empty response as the next `LAST_SEEN_SLOT`.
## Response
Array of swap events on this page, ordered by most recent first.
Solana transaction signature.
Swap timestamp (Unix seconds).
Solana slot number.
Swap direction — `buy` (Y to X) or `sell` (X to Y).
Raw input token amount.
Raw output token amount.
Input token mint address.
Output token mint address.
The pool midprice at the time of the swap (raw Q32-decoded value).
Effective execution price in Y-per-X terms, excluding the protocol fee. Directly comparable to `midprice_at_swap`.
The pool's base spread factor read from the oracle at the time of the swap. `1.0` means no base spread; e.g. `0.9995` means the LP charged ≈5 bps of base spread before any inventory skew. Values above `1.0` indicate the curve was skewed in the trader's favor (the LP gave up base spread to rebalance inventory) — this is the conceptual fee the LP is *still* earning even when `realized_spread_quote_raw` is negative. `null` for swaps recorded before this field was tracked.
Vault X balance after the swap.
Vault Y balance after the swap.
Fee and execution quality metrics for the swap.
Protocol fee in raw Y (quote-side) atoms. For sell swaps (fee paid in X), converted to Y-equivalent using midprice.
Protocol fee in raw atoms of the **fee token** (the swap's input token — X on a sell, Y on a buy). Unlike `protocol_fee_quote_raw`, this is not midprice-converted.
Mint of the fee token (equals `input_mint`).
Spread captured by the pool in raw Y (quote-side) atoms. Positive values indicate pool profit; negative values indicate the trade executed better than midprice. For buy swaps this is a Y-equivalent conversion of the X atoms the pool saved.
Spread captured by the pool in basis points of the trade's notional value at midprice. Unit-independent — directly comparable across pools and directions.
Per-recipient breakdown of the protocol fee for pools whose fee routes through the **fee-split sidecar**. `null` for pools that use the default (single) fee recipient, and for swaps recorded before this field was tracked.
Amounts are exact proportional shares of `protocol_fee_raw` (`protocol_fee_raw × ppm / Σ ppm`) — they sum to `protocol_fee_raw`. The split ratio is captured at the swap's slot, so it is exact even if the pool's recipient weights change later.
Recipient wallet address.
Recipient's split weight in parts-per-million, as configured on-chain at the time of the swap. The denominator is the sum of all recipients' `ppm` (not a fixed 1,000,000).
Recipient's share of `protocol_fee_raw`, in fee-token atoms.
Cursor to pass as `?cursor=...` on the next request to fetch the following page. `null` when there are no more swaps (end of history).
`true` if more swaps are available beyond this page, `false` if this is the last page.
```json theme={null}
{
"data": [
{
"signature": "5ZVLukBf3HfmT7d9Emte1fobjhtHsjps5TXtNZboTK3di76NbVUxHsLDFuzcnyhnjYLNnVdJajKg9k5NJCs9CLCi",
"timestamp": 1773239448,
"slot": 405713180,
"direction": "buy",
"input_amount": 86476334,
"output_amount": 998378024,
"input_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"output_mint": "So11111111111111111111111111111111111111112",
"midprice_at_swap": 0.08647878072224557,
"execution_price_at_swap": 0.08662,
"base_spread_factor_at_swap": 0.9995,
"vault_x_after": 5413038034,
"vault_y_after": 515256215,
"fees": {
"protocol_fee_quote_raw": 0,
"protocol_fee_raw": 0,
"protocol_fee_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"realized_spread_quote_raw": 142136,
"realized_spread_bps": 16.44,
"protocol_fee_split": null
}
},
{
"signature": "4b76z7T6gqn5aNjvDN3eE4X9dLrdR88DNYomkugjRvExxLRrYAgumaoGbzN8NYPFXvoXaeM9bbmD6P2tp5aTT2Tn",
"timestamp": 1773239444,
"slot": 405713171,
"direction": "sell",
"input_amount": 1000000000,
"output_amount": 86347675,
"input_mint": "So11111111111111111111111111111111111111112",
"output_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"midprice_at_swap": 0.08649538387544453,
"execution_price_at_swap": 0.08634768,
"base_spread_factor_at_swap": 0.9995,
"vault_x_after": 6411416058,
"vault_y_after": 428792852,
"fees": {
"protocol_fee_quote_raw": 0,
"protocol_fee_raw": 0,
"protocol_fee_mint": "So11111111111111111111111111111111111111112",
"realized_spread_quote_raw": 147709,
"realized_spread_bps": 17.08,
"protocol_fee_split": null
}
}
],
"next_cursor": "eyJpZCI6ImozZDd4a203...",
"has_more": true
}
```
For a pool whose protocol fee routes through the fee-split sidecar, the
`fees` object includes a populated `protocol_fee_split` (shares sum to
`protocol_fee_raw`):
```json theme={null}
"fees": {
"protocol_fee_quote_raw": 12500,
"protocol_fee_raw": 12500,
"protocol_fee_mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"realized_spread_quote_raw": 142136,
"realized_spread_bps": 16.44,
"protocol_fee_split": [
{ "recipient": "Rec1", "ppm": 700000, "amount_raw": 8750 },
{ "recipient": "Rec2", "ppm": 300000, "amount_raw": 3750 }
]
}
```
# Recent Transactions
Source: https://docs.hadron.fi/api-reference/recent-transactions
GET https://api.hadron.fi/pools/{address}/transactions
Returns Hadron and fee-split (sidecar) transactions for a pool, ordered newest first. Cursor-paginated — you can walk the full history of a pool by following `next_cursor`.
## Path Parameters
The pool config PDA address.
## Query Parameters
Page size — number of transactions to return per request. Maximum `200`.
Opaque pagination cursor. Omit on the first request; on subsequent
requests, pass the `next_cursor` value from the previous response to
fetch the next page. When the server returns `next_cursor: null`
(equivalently `has_more: false`), you have reached the end of the
pool's history.
Filter by instruction type (e.g. `SwapExactIn`, `Deposit`, `Distribute`). See the Instruction Types and Fee-Split Instruction Types tables below for possible values. When set, only transactions matching this type are returned. Pages may contain fewer than `limit` results.
Solana cluster — `mainnet-beta` or `devnet`.
## Response
Array of transaction records on this page, ordered by most recent first.
Solana transaction signature.
Transaction timestamp (Unix seconds).
Solana slot number.
Zero-based ordinal among the **Hadron** instructions in this
transaction — not the absolute Solana instruction position. The
first Hadron invocation is `0`, the second is `1`, and so on;
non-Hadron instructions (compute budget, token transfers, other
programs) are skipped when counting. Top-level Hadron
instructions are counted before Hadron CPIs from inner
instructions. A single Solana transaction can invoke the Hadron
program multiple times (e.g. a router or batch); each invocation
is recorded as its own row, disambiguated by `(signature, ix_index)`.
Fee-split (sidecar) instructions in a transaction are recorded with a
separate, higher `ix_index` range so they never collide with the Hadron
rows of the same transaction (a fee-split call often CPIs into Hadron).
The instruction that was executed — a Hadron instruction or a fee-split (sidecar) instruction. See the tables below for possible values.
Whether the transaction succeeded.
Error message if the transaction failed, `null` otherwise.
Cursor to pass as `?cursor=...` on the next request to fetch the
following page. `null` when there are no more transactions (end of history).
`true` if more transactions are available beyond this page, `false`
if this is the last page.
### Instruction Types
| Type | Description |
| - | - |
| `Initialize` | Pool initialization |
| `Deposit` | Liquidity deposit |
| `Withdraw` | Liquidity withdrawal |
| `SwapExactIn` | Swap with exact input amount |
| `SetCurve` | Price curve update |
| `UpdateMidprice` | Oracle midprice update |
| `InitializeFeeConfig` | Global fee config initialization |
| `UpdateFeeConfig` | Global fee config update |
| `SetRiskCurve` | Risk curve update |
| `UpdateBaseSpread` | Base spread factor update |
| `UpdateMidpriceAndBaseSpread` | Combined midprice and spread update |
| `SwitchPriceCurve` | Switch active price curve slot |
| `SwitchRiskCurve` | Switch active risk curve slot |
| `InitializeSpreadConfig` | Spread config initialization |
| `UpdateSpreadConfig` | Spread config update |
| `UpdateDeltaStaleness` | Delta staleness parameter update |
| `NominateAuthority` | Nominate new pool authority |
| `AcceptAuthority` | Authority transfer acceptance |
| `SubmitCurveUpdates` | Batch curve update submission |
| `ApplyCurveUpdates` | Apply pending curve updates |
| `ClosePool` | Close and reclaim pool rent |
| `SetPoolState` | Pool state change (pause/freeze/resume) |
| `AllocateCurvePrefabs` | Allocate curve prefab storage |
| `RotateFeeAdmin` | Rotate fee admin key |
| `InitializePoolFeeConfig` | Per-pool fee config initialization |
| `UpdatePoolFeeConfig` | Per-pool fee config update |
| `SetAssetFee` | Set a per-asset-pair fee override on the global fee config |
| `ValidateCurveUpdates` | Dry-run validation of queued curve updates |
| `RemoveSpreadTriggers` | Remove specific spread triggers |
| `UpdateDrawdownGuard` | Update drawdown guard threshold / rotation interval |
| `UpdateFactorClamp` | Update the combined-factor clamp ceiling |
| `ClosePoolFeeConfig` | Revert a pool from per-pool fee back to the global fee |
| `RotateDepositAuthority` | Rotate the deposit authority |
| `RotateWithdrawAuthority` | Rotate the withdraw authority |
| `RotateQuotingAuthority` | Rotate the quoting authority (replaces `SetQuotingAuthority`) |
| `RotateSpreadAdmin` | Rotate the spread config admin |
| `RotatePauseAuthority` | Rotate the pause authority |
### Fee-Split Instruction Types
Instructions from the fee-split sidecar program. They appear alongside Hadron instructions in this endpoint, attributed to the pool via the fee-split config's `hadron_pool_config`, and are filterable with `instruction_type`. Per-recipient credit and distribution amounts are not on these rows — those are tracked separately by the sidecar indexer.
| Type | Description |
| - | - |
| `InitializeSplit` | Fee-split config created for the pool |
| `UpdateCustomerDomain` | Recipient set / split-weight (ppm) update (the "update fee split" operation) |
| `UpdateHadronSlot` | Hadron-slot (protocol recipient) update |
| `Distribute` | Pending fees paid out to a recipient |
| `Reinvest` | A recipient's pending fees reinvested into the pool |
| `CloseSplit` | Fee-split config closed |
```json theme={null}
{
"data": [
{
"signature": "3Qm9q3hP8oLpAaQ7s2y1Wb6kZ4uVnRceJ2hN1xVtQz8Kfw5rXdCgu7mEb9PtVZ2sYHn4aLrDk6oW3fJq1RcT5Bv",
"timestamp": 1773239510,
"slot": 405713340,
"ix_index": 1000,
"instruction_type": "Distribute",
"success": true,
"error": null
},
{
"signature": "XK7Cu5FSNkBP55ZMXLcMv1ZVUTB2cYCVrngtWHWf7ci8L3XjuVgnw1FwNQa7vsVNZ2QrHdwGVHkQqboLENp4Qj2",
"timestamp": 1773239503,
"slot": 405713326,
"ix_index": 0,
"instruction_type": "UpdateDeltaStaleness",
"success": true,
"error": null
},
{
"signature": "5ZVLukBf3HfmT7d9Emte1fobjhtHsjps5TXtNZboTK3di76NbVUxHsLDFuzcnyhnjYLNnVdJajKg9k5NJCs9CLCi",
"timestamp": 1773239448,
"slot": 405713180,
"ix_index": 0,
"instruction_type": "SwapExactIn",
"success": true,
"error": null
}
],
"next_cursor": "eyJpZCI6ImozZDd4a203...",
"has_more": true
}
```
# Volume History
Source: https://docs.hadron.fi/api-reference/volume-history
GET https://api.hadron.fi/pools/{address}/volume/history
Returns historical trading volume for a pool in daily buckets.
## Path Parameters
The pool config PDA address.
## Query Parameters
Bucket size. Currently only `24h` is supported.
Start of the time range as a Unix timestamp in seconds. Defaults to 7 days ago.
End of the time range as a Unix timestamp in seconds. Defaults to current time.
Solana cluster — `mainnet-beta` or `devnet`.
## Response
Start of the requested range (Unix seconds).
End of the requested range (Unix seconds).
The bucket size used.
Array of volume buckets aligned to UTC days.
Bucket start timestamp (Unix seconds, UTC midnight).
Trading volume in USD for this bucket.
Fees collected in USD for this bucket.
```json theme={null}
{
"start_time": 1772646065,
"end_time": 1773250865,
"timeframe": "24h",
"data": [
{
"timestamp": 1772582400,
"volume": 115.41,
"fees": 0
},
{
"timestamp": 1772668800,
"volume": 1058.62,
"fees": 0
},
{
"timestamp": 1772755200,
"volume": 267.98,
"fees": 0
}
]
}
```
Sub-day timeframes (`1h`, `4h`, `12h`) are planned for a future release.
## Errors
| Status | Message |
| - | - |
| `400` | `Only 24h timeframe is supported for volume history in v1` |
# Curves
Source: https://docs.hadron.fi/concepts/curves
## Curves in Hadron
At a high-level, curves allow makers to express **parameterized functions**. Curves determine the following:
1. **Price Curve**: *"I have defined a mid-price and base spread, but how do my quotes scale if a user tries to swap 1, 10, 100, 1000, 10000 SOL?"*
2. **Risk Curve**: *"I just received multiple orders within or across blocks, how do I automatically change my quotes now that my inventory is imbalanced?"*
*Take a look at this amazing project put together by the Ghost Team, where you can see how different PropAMM quotes change as a function of volume. These graphs, in Hadron, can be thought of as Price Curves:*
[*https://sandwiched.me/experiments*](https://sandwiched.me/experiments)
## Curve Types
Each side of the market (bid and ask) has its own pricing curve. **These curves define how price changes as a function of input size**. They allow makers to express spread ladders, nonlinear depth, and asymmetric behavior between buying and selling.
A curve is defined by a set of control points - example curve constructed [here](https://docs.hadron.fi/examples/bid-and-ask).
Inventory curves apply an additional spread based on the pool’s current base/quote balance.
They allow makers to:
* Penalize trades that worsen imbalance
* Incentivize trades that rebalance inventory
* Express risk as a continuous function of inventory
Importantly, inventory skew is applied without canceling or reposting orders, **enabling real-time risk adjustment and reducing intra-block pickoff**.
# Points and Interpolations
Source: https://docs.hadron.fi/concepts/interpolations
## Overview
Points and interpolations are the means by which you define curves.
## Defining Curves
Each bid or ask curve consists of **control points**.
Every point specifies:
* **Input Amount:** Trade size in atoms
* **Price Factor:** Adjustment relative to mid-price - equivalent to spread
* **Interpolation Mode:** How pricing transitions to the next point
Points define the shape of the curve; interpolation determines the behavior between them.
## Interpolation Modes
Hadron supports multiple interpolation types, which can be mixed across points:
* **Step** (0): Constant price factor between point A → B.
* **Linear** (1): Linearly interpolates price between points.
* **Marginal Step** (2): Points store average spread, producing constant marginal pricing between A → B. Gives orderbook-like behavior where each size tier has a flat marginal price.
* **Hyperbolic** (3): Rational easing with shape parameter `k` .
`k=1.0` is exactly linear, `k<1.0` is concave (saturating), `k>1.0` is convex (accelerating).
* **Quadratic** (4): Quadratic easing with shape parameter `k`. Formula: `ease(t) = t + k*(t² - t)`. `k=0` is linear, `k>0` is convex (stays near f0 longer), `k<0` is concave (moves toward f1 quickly).
* **Cubic** (5): Cubic easing with 2 shape parameters `a` and `b`. Formula: `ease(t) = t + a*(t² - t) + b*(t³ - t)`. `a=0,b=0` is linear. Allows S-curves and richer shapes.
By combining points and interpolation types, makers can construct virtually any curve shape — from order-book ladders to smooth nonlinear depths.
# Mid-price And Base Spread
Source: https://docs.hadron.fi/concepts/mid-price
## Hadron Curves Are Anchored Around Midprice
Mid-price and base-spread are initialized and updated by the maker, and anchor the entire swap function. The instruction to update these parameters sit \< 40 CUs.
**Importantly**, mid-price must be *actively* managed, by the maker, sending updates to make sure price has not gone stale.
# Toxic Flow Prevention
Source: https://docs.hadron.fi/concepts/toxic-flow-prevention
### Segmenting Toxic Flow With Hadron
Hadron is designed to give operators granular control over toxic flow. Hadron allows users to monitor for various accounts in instructions and control quoting logic during runtime.
**How it works:** On Solana, the SVM requires all accounts touched in a transaction to be passed into the instructions for safety. Hadron let’s operators apply conditions that affect the quoting logic based on the accounts passed into the transaction at runtime.
* Operators decide which accounts are affected and the quoting behavior for those accounts
* Example: Add 3Bps spreads to swaps not from whitelisted routers
* Example: Reduce spreads for swaps with a phantom or fomo fee wallet
* Example: Blacklist known arbitrage wallet and prevent trades from this wallet
# Core Design
Source: https://docs.hadron.fi/core-design
Designed For Modularity
## **Generalizable**
Instead of hard-coding a single formula, Hadron treats pricing as an explicit, updatable function defined by the maker. ***This means that makers can iterate on their entire pricing logic without needing to update the underlying contract.***
### Functionality
As a baseline, all Hadron pools (PropAMM instances) are composed of the following:
* **Mid-price**: Price anchor for all swaps
* **Base Spread**: Static spread applied per side
* **Bid/Ask Curve**: Customizeble curves that define quoting logic as a function of input atoms
* **Inventory curve**: Customizable curves that express risk as a function of inventory imbalance
### Isolated Liquidity
The maker that creates the pool is the sole authority of that pool. They are the only party that can control the pool, including withdrawing, depositing, and updating quoting logic.
### Toxic-Flow Management
Each maker can define wallets or programs that pool should not interact with. For instance, you can widen or tighten spread for (Jito, OKX, a specific wallet, etc.).
# Update Oracle and Curves
Source: https://docs.hadron.fi/examples/bid-and-ask
## **Defining mid-price**
Defining and updating mid-price on Hadron has three variables
* **Input:** Mid-price is defined in **quote atoms per base atom**
* **Sequence:** Sequence is a nonce sent by the operator to prevent stale quotes from block builders re-ordering transactions. Instructions with a sequence strictly less than the current sequence will be fail.
* **Base spread (optional):** Operators can initialize an optional base spread with the mid-price update.
```typescript theme={null}
// `pool` is a loaded Hadron instance; `authority` is the pool authority (see SDK ▸ Quickstart)
// Updating a midprice of 117.20 SOL/USDC and a base spread of 5 bps
const ix = pool.updateMidpriceAndBaseSpread(authority, {
// midprice = quote atoms per base atom (USDC 6 decimals, SOL 9 decimals)
midpriceQ32: toQ32(117.20 * 1e6 / 1e9), // $117.20 SOL/USDC
spreadFactorQ32: spreadBpsToQ32(5), // 5 bps base spread
sequence: 1n, // strictly-increasing nonce
});
```
## Building Bid and Ask Curves
**Simple 3 point curve**
Points on Hadron are a function of 3 input values:
* **Volume**: Input volume in atoms
* **Spread**: Distance from the Mid-price
* **Interpolation mode**: Moving between any two points
```typescript theme={null}
// 3 point bid curve with a midprice of 1.00
// Points: Input Amount 0 (base), 1000, 2000 units
// Spread of 10, 20, 50 Bps from Mid
// Interpolating step wise for all 3 points
const ix = pool.setCurve(authority, {
side: Side.Bid,
defaultInterpolation: Interpolation.Step,
points: [
{ amountIn: 0n, priceFactor: 0.9990 }, // 0-1000: 10bps fee
{ amountIn: 1000n, priceFactor: 0.9980 }, // 1000-5000: 20bps fee
{ amountIn: 5000n, priceFactor: 0.9950 }, // 5000+: 50bps fee
],
});
```
### Curve Visualization:
# HumidiFi
Source: https://docs.hadron.fi/examples/humidifi
Rebuilding HumidiFi's Curve With Hadron
**HumidiFi:** HumidiFi is the largest propAMM on Solana, with over \$50bn in 90day volume.
*Source: Bid/Ask curve plotted from running the HumidiFi binaries in liteSVM. Note that HumidiFi curves follow this general shape, widening, shrinking, and shifting every slot.*
For simplicity, we will linearly approximate humidifi's curve with just a few knots, although you can get even closer with more data points or hyperbolic interpolation.\
\
Under the hood, Humidifi is approximating log decay with very small linear segments.
HumidiFi Bid/Ask Curve:
| **amount\_in\_sol** | **average\_price\_usdc\_per\_sol** |
| :- | :- |
| 1 | 101.11448 |
| 491.12256128 | 101.109647509533 |
| 493.12306153 | 101.10963548795 |
| 4000 | 101.08382844375 |
| **amount\_in\_sol** | **average\_price\_usdc\_per\_sol** |
| :- | :- |
| 1.0 | 101.135357068354 |
| 491.122561279057 | 101.141372888682 |
| 493.123061523928 | 101.141398399866 |
| 4000.0 | 101.172231918849 |
Creating this curve with Hadron:
```typescript theme={null}
// `pool` is a loaded Hadron instance; `authority` is the pool authority (see SDK ▸ Quickstart)
// creating a 4 point bid/ask curve with hadron with midprice of 101.125
const bidCurveIx = pool.setCurve(authority, {
side: Side.Bid,
defaultInterpolation: Interpolation.Linear,
points: [
{ amountIn: 1n, priceFactor: 0.99989678, interpolation: Interpolation.Step },
{ amountIn: 491n, priceFactor: 0.99984899, interpolation: Interpolation.Linear },
{ amountIn: 493n, priceFactor: 0.99984887, interpolation: Interpolation.Linear },
{ amountIn: 4000n, priceFactor: 0.99959367, interpolation: Interpolation.Step },
],
});
const askCurveIx = pool.setCurve(authority, {
side: Side.Ask,
defaultInterpolation: Interpolation.Linear,
points: [
{ amountIn: 1n, priceFactor: 1.00010322, interpolation: Interpolation.Step },
{ amountIn: 491n, priceFactor: 1.00016271, interpolation: Interpolation.Linear },
{ amountIn: 493n, priceFactor: 1.00016297, interpolation: Interpolation.Linear },
{ amountIn: 4000n, priceFactor: 1.00046787, interpolation: Interpolation.Step },
],
});
```
*Note: This is a simple linear based approximation of the curve, however the Hadron quadratic and other non linear interpolation leads to a smoother replica of the HumidiFi curve*
# Mixed Interpolation
Source: https://docs.hadron.fi/examples/mixed-interpolation
Points can be connected with any interpolation method, allowing you to create nearly any quoting function.
In this example, we will look at a mixed interpolation curve. Here, we define a curve with "step" and "linear".
```typescript theme={null}
// `pool` is a loaded Hadron instance; `authority` is the pool authority (see SDK ▸ Quickstart)
// 5 point ask curve with a midprice of 1.00
// Points: Input Amount 0 (base), 1000, 2000, 5000, 10000 units
// Spread of 10, 20, 50, 100, 150 Bps from Mid
// Mixed Interpolation across points
const askCurveIx = pool.setCurve(authority, {
side: Side.Ask,
defaultInterpolation: Interpolation.Linear,
points: [
{ amountIn: 0n, priceFactor: 1.0010, interpolation: Interpolation.Step }, // 10bps
{ amountIn: 1000n, priceFactor: 1.0020, interpolation: Interpolation.Linear }, // 20bps
{ amountIn: 2000n, priceFactor: 1.0050, interpolation: Interpolation.Linear }, // 50bps
{ amountIn: 5000n, priceFactor: 1.0100, interpolation: Interpolation.Step }, // 100bps
{ amountIn: 10000n, priceFactor: 1.0150, interpolation: Interpolation.Step }, // 150bps
],
});
```
## Curve Visualization:
*This is a mixed curve with no inventory skew logic*
# Integrating as an Order Book
Source: https://docs.hadron.fi/examples/order-book
Hadron is fully reducible to an order book, with one key difference.
## Why traditional spot order books have failed on-chain
1. **You can't update your full book atomically**: on a CEX, you batch-cancel and batch-place orders. On Solana, transaction size limits cap how many orders you can place at once. Refreshing a book takes multiple transactions, and between them, your quotes are partially stale. Searchers often see this and pick you off.
2. **Block latency**: from the moment you submit a cancel until it confirms, your resting order is live. On Solana it's at least one block (\~400ms), and block priority favors low-CU transactions, so a searcher's single-instruction pickoff lands higher in the block than your multi-instruction cancel/replace.
On-chain order books have fundamentally been designed for takers. For example, a batch update on Manifest (a large Solana-native orderbook) costs \~50,000 CUs. Even if that txn lands in the same block as a price move, a searcher's low-CU pick-off txn gets scheduled higher in the block, so your refresh arrives after.
## Hadron
Unlike traditional on-chain order books, with Hadron you can efficiently place/cancel orders and segment flow, preventing pick-off. Instead of placing individual orders that each cost CUs to create, modify, and cancel, you define a **continuous function** that maps any incoming trade to a price.
**Refreshing your entire book (curve) is a single mid-price update: \~32 CUs.** That's 1,500× cheaper than a Manifest batch update, 3,000× cheaper than adding/removing concentrated liquidity in an AMM, and you land near the top of the block for a structural maker advantage.
### How does this work, I thought Hadron was a PropAM?
Every Hadron pool is defined by a single function:
```
output = f(input_size, mid_price, price_curve, inventory_curve)
```
where `price_curve` and `inventory_curve` are parameterized functions of "points" and "interpolations."
To quote as an Order Book, we use **only** the inventory curve, with `step`-interpolated points. When a swap arrives, Hadron walks through the inventory curve like an order book, applying different spreads to the mid-price at each level.
**Key advantage:** because all prices are defined as **spreads relative to mid-price**, a single mid-price update (\~32 CUs) instantly shifts every level in the book. That is equivalent to cancelling and re-placing your entire ladder of limit orders in one atomic operation.
In the example below, change the mid-price to see the entire book shift, then increase "Trade size" to watch orders get filled:
```ts theme={null}
import { HadronOrderbook } from "@hadron-fi/sdk-v2";
const book = await HadronOrderbook.load({
connection,
pool: poolAddress,
});
// Bid orders — increasing size, widening spread from mid
book.placeOrder({ side: "bid", size: 500, spreadBps: 15 });
book.placeOrder({ side: "bid", size: 1000, spreadBps: 30 });
book.placeOrder({ side: "bid", size: 2000, spreadBps: 50 });
book.placeOrder({ side: "bid", size: 3500, spreadBps: 80 });
book.placeOrder({ side: "bid", size: 5000, spreadBps: 120 });
book.placeOrder({ side: "bid", size: 7000, spreadBps: 170 });
book.placeOrder({ side: "bid", size: 11000, spreadBps: 230 });
// Ask orders — mirror of bid side
book.placeOrder({ side: "ask", size: 500, spreadBps: 15 });
book.placeOrder({ side: "ask", size: 1000, spreadBps: 30 });
book.placeOrder({ side: "ask", size: 2000, spreadBps: 50 });
book.placeOrder({ side: "ask", size: 3500, spreadBps: 80 });
book.placeOrder({ side: "ask", size: 5000, spreadBps: 120 });
book.placeOrder({ side: "ask", size: 7000, spreadBps: 170 });
book.placeOrder({ side: "ask", size: 11000, spreadBps: 230 });
const ixs = book.push(authority);
```
## Integrating Hadron as an order book
Hadron is reducible to a fully on-chain order book: we give you an interface that feels like an order book, with one key difference, **Hadron has no matching engine**. The matching engine is the aggregator (Jupiter, Titan, etc.).
### SDK (TypeScript) example
**1. Load your pool**
```ts theme={null}
import { HadronOrderbook } from "@hadron-fi/sdk-v2";
const book = await HadronOrderbook.load({
connection,
pool: poolAddress,
});
```
**2. Place orders**
Place limit orders just like on any exchange:
```ts theme={null}
// Place bid orders (prices defined as spread below mid)
book.placeOrder({ side: "bid", size: 500, spreadBps: 15 });
book.placeOrder({ side: "bid", size: 1000, spreadBps: 30 });
book.placeOrder({ side: "bid", size: 2000, spreadBps: 50 });
book.placeOrder({ side: "bid", size: 5000, spreadBps: 80 });
book.placeOrder({ side: "bid", size: 10000, spreadBps: 120 });
// Place ask orders (spread above mid)
book.placeOrder({ side: "ask", size: 500, spreadBps: 15 });
book.placeOrder({ side: "ask", size: 1000, spreadBps: 30 });
book.placeOrder({ side: "ask", size: 2000, spreadBps: 50 });
book.placeOrder({ side: "ask", size: 5000, spreadBps: 80 });
book.placeOrder({ side: "ask", size: 10000, spreadBps: 120 });
const ixs = book.push(authority);
```
Orders are staged locally until you call `.push(authority)`, which returns the instruction(s) to send.
**3. Update mid-price (move the entire book)**
One instruction, \~32 CUs. This atomically shifts every order — equivalent to cancelling and re-placing your entire ladder at new prices:
```ts theme={null}
const ix = book.updateMidprice(authority, 121.05);
// Every bid and ask order is now centered around 121.05
```
**4. Amend and cancel orders**
Stage any combination of new orders, amendments, and cancellations, then send them in one shot:
```ts theme={null}
book.amendOrder({ side: "bid", level: 2, spreadBps: 70 });
book.amendOrder({ side: "ask", level: 1, size: 2000 });
book.cancelOrder({ side: "bid", level: 0 });
book.placeOrder({ side: "ask", size: 15000, spreadBps: 150 });
const ixs = book.push(authority);
```
Under the hood, `.push(authority)` packs the staged operations into instruction(s) (\~50 CUs). Updates are applied atomically on the next swap.
**5. Read book state**
```ts theme={null}
const state = book.getBookState();
// { midprice, bids, asks, inventory }
```
**6. Tracking updates**
The SDK does not expose a push/subscribe API. Re-read the book with `getBookState()` (step 5) on an interval — or after each confirmed swap — to track midprice, levels, and inventory:
```ts theme={null}
setInterval(() => {
const state = book.getBookState();
// react to state.midprice, state.bids, state.asks, state.inventory
}, 1000);
```
# Risk Curves
Source: https://docs.hadron.fi/examples/risk-curves
**Example Three:** Mixed curve with inventory curve logic
Let’s take the same curve as before and add an inventory skew curve to this 5 point mixed curve.
Risk curves have three input values:
* **Base/Quote Amount**: Quantum of inventory. This can be expressed in two ways
* Base/Quote Percentage: This is the relative percentage between Base and Quote amounts, normalized by your mid-price. (basePct + quotePct = 1)
* Base/Quote Amount: This is an absolute amount of tokens expressed as atoms
* **Price Factor**: Basis points added to the swap based on the value of inventory
* **Interpolation**: Shape of the curve between two base/quote points
```typescript theme={null}
// `pool` is a loaded Hadron instance; `authority` is the pool authority (see SDK ▸ Quickstart)
// 5 point ask curve with a midprice of 1.00
// Points: Input Amount 0 (base), 1000, 2000, 5000, 10000 units
// Spread of 10, 20, 50, 100, 150 Bps from Mid
// Mixed Interpolation across points
// Initialize risk curve below yellow line
// Same curve as previous example
const askCurveIx = pool.setCurve(authority, {
side: Side.Ask,
defaultInterpolation: Interpolation.Linear,
points: [
{ amountIn: 0n, priceFactor: 1.0010, interpolation: Interpolation.Step }, // 10bps
{ amountIn: 1000n, priceFactor: 1.0020, interpolation: Interpolation.Linear }, // 20bps
{ amountIn: 2000n, priceFactor: 1.0050, interpolation: Interpolation.Linear }, // 50bps
{ amountIn: 5000n, priceFactor: 1.0100, interpolation: Interpolation.Step }, // 100bps
{ amountIn: 10000n, priceFactor: 1.0150, interpolation: Interpolation.Step }, // 150bps
],
});
// --------------------------------------------------------------------------- //
// Risk (inventory) curve uses percent-of-vault on the x-axis (pctBase: 0.0–1.0).
// setRiskCurveBoth returns [bidIx, askIx].
const [riskBidIx, riskAskIx] = pool.setRiskCurveBoth(authority, {
bid: {
defaultInterpolation: Interpolation.Linear,
points: [
{ pctBase: 0.0, priceFactor: 1.0050 },
{ pctBase: 0.25, priceFactor: 1.0025 },
{ pctBase: 0.50, priceFactor: 1.0000 }, // optional
{ pctBase: 0.75, priceFactor: 0.9975 },
{ pctBase: 1.0, priceFactor: 0.9900 },
],
},
ask: {
defaultInterpolation: Interpolation.Linear,
points: [
{ pctBase: 0.0, priceFactor: 0.9900 },
{ pctBase: 0.25, priceFactor: 0.9975 },
{ pctBase: 0.50, priceFactor: 1.0000 }, // optional
{ pctBase: 0.75, priceFactor: 1.0025 },
{ pctBase: 1.0, priceFactor: 1.0050 },
],
},
});
```
Let's now visualize these curves - **try dragging the inventory slider**:
# Solfi V2
Source: https://docs.hadron.fi/examples/solfiv2
Rebuilding Solfi's Curve With Hadron
**SolFi**: SolFi is one of the earliest PropAMM teams on Solana, started by the Ellipsis labs team.
*Source: Bid/Ask curve plotted from running the SolFi binaries in liteSVM.*
| **x\_in\_sol** | **price\_usdc\_per\_sol** | **price\_factor** |
| :- | :- | :- |
| 1.0 | 100.830592 | 0.9995861558038308 |
| 113.028014007 | 100.828181509888 | 0.9995622593603323 |
| 175.04352176 | 100.82738451297 | 0.9995543583149288 |
| 243.060530265 | 100.826506793518 | 0.9995456570250177 |
| 317.079039519 | 100.825538280604 | 0.9995360556522462 |
| 369.092046023 | 100.824842160595 | 0.9995291546521632 |
| 435.108554277 | 100.82394426121 | 0.9995202533080696 |
| 4000.0 | 100.79455585925 | 0.9992289107784094 |
| **x\_in\_sol** | **price\_usdc\_per\_sol** | **price\_factor** |
| :- | :- | :- |
| 1.0 | 100.9140828628 | 1.0004138441961692 |
| 695.173586791993 | 100.924742587534 | 1.0005195195974281 |
| 2803.70085042246 | 100.942493800252 | 1.000695496611246 |
| 3037.759379683103 | 100.944281384436 | 1.0007132178639653 |
| 3271.817908953663 | 100.946069031882 | 1.0007309397438335 |
| 3505.876438214307 | 100.947856742665 | 1.0007486622515946 |
| 3739.934967474951 | 100.949644516784 | 1.0007663853872384 |
| 3999.999999990082 | 100.951644474717 | 1.0007862120115034 |
```typescript theme={null}
// `pool` is a loaded Hadron instance; `authority` is the pool authority (see SDK ▸ Quickstart)
// 8 point curve with a midprice of 100.87
const bidCurveIx = pool.setCurve(authority, {
side: Side.Bid,
defaultInterpolation: Interpolation.Linear,
points: [
{ amountIn: 1n, priceFactor: 0.99958616, interpolation: Interpolation.Step },
{ amountIn: 113n, priceFactor: 0.99956226, interpolation: Interpolation.Linear },
{ amountIn: 175n, priceFactor: 0.99955436, interpolation: Interpolation.Linear },
{ amountIn: 243n, priceFactor: 0.99954566, interpolation: Interpolation.Linear },
{ amountIn: 317n, priceFactor: 0.99953606, interpolation: Interpolation.Linear },
{ amountIn: 369n, priceFactor: 0.99952915, interpolation: Interpolation.Linear },
{ amountIn: 435n, priceFactor: 0.99952025, interpolation: Interpolation.Linear },
{ amountIn: 4000n, priceFactor: 0.99922891, interpolation: Interpolation.Step },
],
});
const askCurveIx = pool.setCurve(authority, {
side: Side.Ask,
defaultInterpolation: Interpolation.Linear,
points: [
{ amountIn: 1n, priceFactor: 1.00041384, interpolation: Interpolation.Step },
{ amountIn: 695n, priceFactor: 1.00051952, interpolation: Interpolation.Linear },
{ amountIn: 2804n, priceFactor: 1.00069550, interpolation: Interpolation.Linear },
{ amountIn: 3038n, priceFactor: 1.00071322, interpolation: Interpolation.Linear },
{ amountIn: 3272n, priceFactor: 1.00073094, interpolation: Interpolation.Linear },
{ amountIn: 3506n, priceFactor: 1.00074866, interpolation: Interpolation.Linear },
{ amountIn: 3740n, priceFactor: 1.00076639, interpolation: Interpolation.Linear },
{ amountIn: 4000n, priceFactor: 1.00078621, interpolation: Interpolation.Step },
],
});
```
# Fees
Source: https://docs.hadron.fi/fees
Hadron fees vary by asset type. We offer rebates to high volume makers quoting through us and makers interested in leverage. For more information on pricing and custom rates, reach out to the team.
**Telegram**: @adamgpeters, @srhhadron
**Email**: [adam@hadron.fi](mailto:adam@hadron.fi), [srinath@hadron.fi](mailto:srinath@hadron.fi)
# Introduction
Source: https://docs.hadron.fi/introduction
## Overview
Hadron is a PropAMM primitive built for professional market makers to provide institutional grade liquidity on Solana. Hadron allows operators to initialize any quoting logic and stores an **explicit updatable pricing curve**, controlling spread and inventory aware pricing. Hadron also gives operators explicit tools to monitor and react to toxic flow.
**We recommend reading through all examples to see the power of this generalizable system!**
## Hadron At A Glance
| Instruction | Cost |
| - | -: |
| Update Midprice | 37 CUs |
| Update Midprice And Spread | 37 CUs |
| Update Spread Factor | 30 CUs |
| Switch Price Curves | 50 CUs |
| Switch Risk Curves | 49 CUs |
| Submit Curve Updates | 58 CUs |
| Set Pool State | 327 CUs |
| Set Curve (16 points) | 39327 CUs |
| Swap | 40440 CUs |
## Why Hadron?
Access to market making on PropAMMs is severely gated today; makers must navigate the nuances of the SVM, secure landing services, endure slow aggregator integrations, and commit months of engineering effort and audit capital just to get started. Existing PropAMM teams are constrained by trading edge and bandwidth to cover multiple asset types.
New makers with expertise in other assets (RWAs, long-tails, prediction markets, etc.) are bottlenecked by time to market and access to aggregators to even get started. Hadron fixes this.
Hadron at a glance
What are curves in Hadron?
See Hadron replicate the quoting logic of an order book
See Hadron replicate the quoting logic of HumidiFi
# Pool Admin Controls
Source: https://docs.hadron.fi/sdk/admin-ts
## **setPoolState**
Pools are initialised by default as Reject Swaps. This is done to deposit funds to prevent indexing from aggregators
```typescript theme={null}
pool.setPoolState(authority, { newState: PoolState.Paused });
//// Send the instructions
```
| **Param** | **Description** |
| :- | :- |
| `params.Initialized` | Pool is live and will be indexed by aggregators |
| `params.Paused` | All operations are paused in the pool |
| `params.RejectSwaps` | Swaps are rejected, but the user can perform all other actions |
## **nominateAuthority / acceptAuthority**
Hadron pools have multiple authorities that can each be delegated to different wallets to split the surface area of control.
```typescript theme={null}
pool.nominateAuthority(authority, {
newAuthority: newPk,
expirySlot: 300_000_000n });
// Expiry - optional parameter for the number of slots
// newAuthority signs: pool.acceptAuthority(newPk);
```
The pool has three states that can be rotated as follows by various authorities:
| From → To | Pool Authority | Quoting Authority | Pauser Authority |
| :- | :- | :- | :- |
| `Initialized → Paused` | ✅ | ❌ | ✅ |
| `Initialized → RejectSwaps` | ✅ | ✅ | ❌ |
| `Paused → Initialized` | ✅ | ❌ | ❌ |
| `Paused → RejectSwaps` | ✅ | ❌ | ❌ |
| `RejectSwaps → Initialized` | ✅ | ❌ | ❌ |
| `RejectSwaps → Paused` | ✅ | ❌ | ✅ |
What each state blocks:
| State | Swaps | Deposits | Withdrawals |
| :- | :- | :- | :- |
| `Initialized` | ✅ | ✅ | ✅ |
| `RejectSwaps` | ❌ | ✅ | ✅ |
| `Paused` | ❌ | ❌ | ❌ |
## **closePool**
Closes the pool and reclaims rent. Requires authority and all vaults to be empty.
```typescript theme={null}
pool.closePool(authority);
```
## **Properties**
These are available on any`Hadron`instance after creation:
| **Property** | **Type** | **Description** |
| :- | :- | :- |
| `pool.poolAddress` | `PublicKey` | The pool’s on-chain address |
| `pool.programId` | `PublicKey` | Program ID |
| `pool.addresses` | `PoolAddresses` | All derived PDA addresses for this pool |
| `pool.config` | `DecodedConfig` | Decoded config account data |
| `pool.oracle` | `DecodedMidpriceOracle` | Decoded oracle data |
| `pool.curveMeta` | `DecodedCurveMeta` | Decoded curve metadata |
# Configuring Your Pool
Source: https://docs.hadron.fi/sdk/configuring-your-pool
All Hadron Pools have price and risk curves which help maker encode their trading strategy into the on-chain PDA for the operator's propAMM.
A operator deploys a pool by describing a quoting function:
The operator picks the midprice, picks how price changes with trade size, picks how price shifts with their inventory. Hadron is the engine that runs that function on chain.
## Hadron Quoting Pipeline
## **updateBaseSpread**
Add a flat pool level fee to all swaps. This is added to any additional spread initialised later in the curve setup by default.
```typescript theme={null}
pool.updateBaseSpread(authority, { spreadFactorQ32: newSpreadQ32 });
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `params.spreadFactorQ32` | `bigint` | Spread discount factor in Q32 (e.g. `toQ32(0.9995)` = 5 bps) |
| `params.sequence?` | `bigint` | Deprecated — ignored in v2. The standalone base-spread instruction no longer carries a sequence. |
## **Creating Price Curves**
Write a price curve to a prefab slot.
* Curves comprise of three key ideas
* `AmountIn`: Stored in atoms(lowest unit of the token based on the token decimal) of the amount inputed by the taker. Stored internally as `Bigint`
* `PriceFactor` : Distance from the midprice in BPS
```typescript theme={null}
const ix = pool.setCurve(authority, {
side: Side.Bid,
defaultInterpolation: Interpolation.Linear,
points: [
//Midprice is initialized as 1. AmountIn is based on the decimals for the input amount
{ amountIn: 100_000n, priceFactor: 0.99 }, // For an input of
{ amountIn: 500_000n, priceFactor: 0.95 },
], slot: 0,
});
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `authority` | `PublicKey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.defaultInterpolation` | `Interpolation` | `Step`, `Linear`, `MarginalStep`, `Hyperbolic`, `Quadratic`, `Cubic` |
| `params.points` | `SetCurvePointInput[]` | Array of `{ amountIn, priceFactor, interpolation?, params? }` |
| `params.slot?` | `number` | Target prefab slot (default: 0) |
| `params.xMode?` | `CurveXMode` | X-axis mode |
To move a few points without rewriting the whole curve, see [Curve Management](/sdk/curve-management).
## **switchPriceCurve**
Switch between different price curves in a single instruction. Up to 16 per curve side, set at pool initialization.
```typescript theme={null}
pool.switchPriceCurve(authority, { sequence: 1n, side: Side.Bid, slot: 1 });
//slot number set presets
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `authority` | `PublicKey` | Pool authority |
| `params.sequence` | `bigint` | Monotonic ordering guard. Must be ≥ the pool's current switch sequence (`pool.curveMeta.switchSequence`). |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.slot` | `number` | Slot to activate |
Hadron protocol fees are added at the pool level to all pools and need to be factored into pools to remain competitive. For the latest fee rates go to [https://docs.hadron.fi/fees](https://docs.hadron.fi/fees)
## **setRiskCurve**
Write a risk curve using percent-of-vault x-axis.
```typescript theme={null}
pool.setRiskCurve(authority, {
side: Side.Bid,
defaultInterpolation: Interpolation.Linear,
points: [
{ pctBase: 0.25, priceFactor: 0.995 },
{ pctBase: 0.75, priceFactor: 1.005 },
],
});
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `authority` | `PublicKey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.defaultInterpolation` | `Interpolation` | Interpolation mode |
| `params.points` | `SetRiskCurvePointInput[]` | `{ pctBase: 0.0–1.0, priceFactor, ... }` |
| `params.slot?` | `number` | Target prefab slot |
| `params.riskMode?` | `RiskMode` | `Virtual` or `Integrated` |
## **setRiskCurveAbsolute**
Write a risk curve using absolute token amounts on the x-axis.
```typescript theme={null}
pool.setRiskCurveAbsolute(authority, {
side: Side.Ask,
defaultInterpolation: Interpolation.Step,
points: [
{ vaultBalance: 1_000_000_000n, priceFactor: 1.002 },
{ vaultBalance: 2_000_000_000n, priceFactor: 1.006 },
],
});
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `authority` | `PublicKey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.defaultInterpolation` | `Interpolation` | Interpolation mode |
| `params.points` | `SetRiskCurveAbsolutePointInput[]` | `{ vaultBalance, priceFactor, ... }` |
| `params.slot?` | `number` | Target prefab slot |
| `params.riskMode?` | `RiskMode` | `Virtual` or `Integrated` |
## **switchRiskCurve**
Switch between different risk curves in a single instruction. Up to 16 per curve side, set at pool initialization.
```typescript theme={null}
pool.switchRiskCurve(authority, { sequence: 1n, side: Side.Ask, slot: 2 });
//slot number set presets
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `authority` | `PublicKey` | Pool authority |
| `params.sequence` | `bigint` | Monotonic ordering guard. Must be ≥ the pool's current switch sequence (`pool.curveMeta.switchSequence`). |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.slot` | `number` | Slot to activate |
For a visual helper please visit [https://dashboard.hadron.fi/curves](https://dashboard.hadron.fi/curves)
# Crank
Source: https://docs.hadron.fi/sdk/crank-ts
Hadron pools are intended to be used with active management around price. Pools do not update mid-price based on reserves and the operator needs to update this based on their price engine.
```tsx theme={null}
Code example coming soon.
```
Passive pools
```tsx theme={null}
Coming soon
```
# Curve Management
Source: https://docs.hadron.fi/sdk/curve-management
Queue curve point edits with submitCurveUpdates for 58 CU alongside your oracle updates, validate them in simulation, and let the next swap apply them.
`setCurve` rewrites an entire curve slot and validates every segment of it in the same transaction. For smooth curves this can cost tens of thousands of compute units, all in your update transaction.
Queued curve updates split that work in two:
* **Your transaction** only writes the edits into a small buffer. `submitCurveUpdates` costs **58 CU** regardless of batch size.
* **The validation work** runs when the batch is applied, which by default is the **next swap** on the pool.
A smaller compute-unit limit means a cheaper priority fee for the same price per CU, and a transaction that lands more reliably when blocks are busy. That makes queued updates a good fit for operators who update the curve alongside every oracle tick.
Three instructions make up this flow:
| **Instruction** | **What it does** | **Accounts** |
| :- | :- | :- |
| `submitCurveUpdates` | Writes a batch of up to 28 point edits into the pool's `CurveUpdates` buffer. Nothing on the curve changes yet. | authority, curveUpdates |
| `validateCurveUpdates` | Dry-runs the queued batch against the live curve, then **always reverts**. Use it only in simulation. | authority, curveMeta, curvePrefabs, curveUpdates |
| `applyCurveUpdates` | Applies the queued batch to the curve and clears the buffer. | authority, curveMeta, curvePrefabs, curveUpdates |
Because the next swap applies the queued batch, an invalid batch makes **every swap on the pool fail** with that batch's error until you replace it. Always validate a batch with `validateCurveUpdates` before you send it.
***
## The recommended flow
1. Build the `submitCurveUpdates` instruction.
2. **Simulate** a transaction containing `submitCurveUpdates` followed by `validateCurveUpdates`.
* Error `73` (`ValidationSimulationSuccess`) on the validate instruction means the batch is valid.
* Any other error is the exact error the real apply would return.
3. **Remove** `validateCurveUpdates` and send `submitCurveUpdates`, typically in the same transaction as your `updateMidprice`. The next swap applies the batch.
`validateCurveUpdates` never commits state, so a transaction that contains it always fails. Include it only when simulating.
```mermaid theme={null}
flowchart LR
A[Build submitCurveUpdates] --> B["simulate: submit + validate"]
B -->|"error 73"| C["send: updateMidprice + submit"]
B -->|"any other error"| D[Fix the ops]
D --> A
C --> E["next swap applies the batch"]
```
***
## submitCurveUpdates
```typescript theme={null}
import {
Hadron,
CurveType,
CurveUpdateOpKind,
Interpolation,
decodeCurveUpdates,
toQ32,
} from "@hadron-fi/sdk-v2";
const pool = await Hadron.load(connection, poolAddress);
// Read the live curve so each op can pin the point it expects to edit.
const bid = pool.getActiveCurves().priceBid;
const slot = pool.getActiveCurveSlots().priceBid;
// `sequence` must be >= the buffer's current sequence.
const updatesInfo = await connection.getAccountInfo(pool.addresses.curveUpdates);
const { sequence } = decodeCurveUpdates(updatesInfo!.data);
const submitIx = pool.submitCurveUpdates(authority.publicKey, sequence + 1n, [
{
curveType: CurveType.PriceBid,
opKind: CurveUpdateOpKind.Edit,
pointIndex: 3,
interpolation: Interpolation.Linear,
amountIn: bid.points[3].amountIn, // keep the same x, move the price
priceFactorQ32: toQ32(0.9975),
params: new Uint8Array(4),
targetSlot: slot, // the active PriceBid slot
expectedXIn: bid.points[3].amountIn, // current x_in at pointIndex
},
]);
```
| **Field** | **Type** | **Description** |
| :- | :- | :- |
| `sequence` | `bigint` | Must be ≥ the buffer's current sequence. Protects against out-of-order submissions. |
| `curveType` | `CurveType` | `PriceBid`, `PriceAsk`, `RiskBid` or `RiskAsk` |
| `opKind` | `CurveUpdateOpKind` | `Edit` overwrites the point at `pointIndex`. `Add` inserts before it. `Remove` deletes it. |
| `pointIndex` | `number` | Index of the point to edit, insert before, or remove |
| `interpolation` | `Interpolation` | Interpolation for the edited or added point |
| `amountIn` | `bigint` | The point's x value, in the same units as the curve's x mode |
| `priceFactorQ32` | `bigint` | New price (or risk) factor in Q32. Use `toQ32(n)`. |
| `params` | `Uint8Array(4)` | Interpolation parameters, for example the Hyperbolic `k` |
| `targetSlot` | `number` | The slot that was active when you built the op. The op fails with `CurveUpdateSlotMismatch` (67) if the active slot has changed. |
| `expectedXIn` | `bigint` | The x value currently at `pointIndex`. The op fails with `CurveUpdateExpectedXMismatch` (72) if the curve has moved. Ignored for `Add`. |
Each submit **replaces** the buffer; it does not append. If you submit twice before the buffer is applied, only the second batch remains.
***
## validateCurveUpdates
Simulate `submitCurveUpdates` and `validateCurveUpdates` together, so the dry-run checks the batch you are about to send:
```typescript theme={null}
import {
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { getHadronErrorMessage } from "@hadron-fi/sdk-v2";
const VALIDATION_SIMULATION_SUCCESS = 73;
const validateIx = pool.validateCurveUpdates(authority.publicKey);
const { blockhash } = await connection.getLatestBlockhash();
const simTx = new VersionedTransaction(
new TransactionMessage({
payerKey: authority.publicKey,
recentBlockhash: blockhash,
instructions: [submitIx, validateIx],
}).compileToV0Message()
);
const sim = await connection.simulateTransaction(simTx, { sigVerify: false });
const err = sim.value.err as { InstructionError?: [number, { Custom?: number } | string] } | null;
const code =
err?.InstructionError && typeof err.InstructionError[1] === "object"
? err.InstructionError[1].Custom
: undefined;
if (code !== VALIDATION_SIMULATION_SUCCESS) {
throw new Error(
`Curve update rejected: ${code !== undefined ? getHadronErrorMessage(code) : JSON.stringify(sim.value.err)}`
);
}
```
On failure the simulation logs name the op that failed, for example `L-06 apply[2] ct=0 kind=Edit idx=4 fail=point-op-validation`.
You can also call `validateCurveUpdates` on its own to check a batch that is already queued on-chain.
***
## Send the update
**Deferred validation can halt swaps on your pool.** `submitCurveUpdates` does not check your ops. They are only checked when the batch is applied, which is usually inside the next swap. If the batch is invalid, **every swap on the pool fails** until you submit a valid batch to replace it.
This is the tradeoff for keeping validation out of your update transaction. Never send a batch without first simulating it with `validateCurveUpdates`. If you cannot simulate first, use `setCurve`, which rejects a bad curve in your own transaction.
After the simulation passes, remove `validateCurveUpdates` and send `submitCurveUpdates` with your oracle update under a tight compute-unit limit:
```typescript theme={null}
import { ComputeBudgetProgram } from "@solana/web3.js";
const midIx = pool.updateMidprice(authority.publicKey, { midpriceQ32: toQ32(142.35) });
const { blockhash: sendBlockhash } = await connection.getLatestBlockhash();
const tx = new VersionedTransaction(
new TransactionMessage({
payerKey: authority.publicKey,
recentBlockhash: sendBlockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }),
midIx,
submitIx, // validateIx removed
],
}).compileToV0Message()
);
tx.sign([authority]);
await connection.sendTransaction(tx);
```
The Hadron instructions in this transaction use under 100 CU (`updateMidprice` about 37, `submitCurveUpdates` 58). Each compute-budget instruction adds about 150 CU, so a limit of 1,000 leaves plenty of headroom.
Until the next swap applies the batch, the curve account still holds the old points, and off-chain quoters (including aggregators) quote from it. The swap that applies the batch executes against the new points. Send `applyCurveUpdates` yourself if you need the new points live and quoted before the next trade.
***
## applyCurveUpdates
```typescript theme={null}
const ix = pool.applyCurveUpdates(authority.publicKey);
```
Applies every queued op in order and clears the buffer, exactly as the next swap would. If any op fails, the whole instruction fails and nothing changes. Applying an empty buffer is a no-op. Use it when you want the edits live immediately and are willing to pay the validation CU yourself.
***
## Batch rules
* **Ops apply in order.** Each op is checked against the curve as the earlier ops in the same batch left it, not against the final curve. A set of edits that is valid in the end can still fail midway. For example, to move two neighbouring points to the right, edit the right-hand point first. If you edit the left-hand point first, it lands past its old neighbour and the op fails with `CurvePointsNotSorted` (error `9`). Validate catches this, so reorder the ops and simulate again.
* **Indexes follow earlier ops.** `pointIndex` and `expectedXIn` refer to the curve after the earlier ops in the batch. After an `Add` or `Remove`, the points to its right shift by one.
* **One failure fails the batch.** No partial apply.
* **Max 28 ops** per submit.
***
## Where the compute units go
Validation cost depends on the curve's **interpolation** and on **how many segments are checked**. `setCurve` checks every segment. A queued op checks only the segments next to the point it touches (at most two). Measured on the deployed program:
| **Curve** | **`setCurve`, paid by you** | **1 queued op, paid at apply** |
| :- | -: | -: |
| Flat Step, 2 points | 667 | 678 |
| MarginalStep, 5 points | 1,535 | 1,003 |
| MarginalStep, 21 points | 5,407 | 1,003 |
| Linear, 2 points | 3,151 | 3,167 |
| Linear, 5 points | 10,903 | 5,687 |
| Linear, 21 points | 52,247 | 5,687 |
With the queue, your transaction pays only the 58 CU for `submitCurveUpdates`. The right-hand column is paid by whoever applies the batch: the next swap, or you if you send `applyCurveUpdates`. `validateCurveUpdates` costs the same as an apply, but only in simulation.
* **Updating the curve with every oracle tick:** use the queue. Your update transaction stays under 100 CU of Hadron work however long or smooth the curve is.
* **Short or cheap curves:** `setCurve` on a 2-point flat curve already costs under 700 CU, so the queue saves little.
* **Keep batches small.** The swap that applies the batch pays about 1,000–5,700 CU per op on top of its own cost. A large batch can push an aggregator's swap over the compute budget it simulated. For large reshapes, use `setCurve` or send `applyCurveUpdates` yourself.
***
## Errors
| **Code** | **Name** | **Meaning** |
| :- | :- | :- |
| 9 | `CurvePointsNotSorted` | The op would leave x values out of order |
| 61 | `ConcavityViolation` | The op would make the curve non-concave |
| 62 | `PriceFactorTooLarge` | Price factor above 1.0 (`Q32_ONE`) |
| 67 | `CurveUpdateSlotMismatch` | `targetSlot` is not the active slot for that curve type |
| 72 | `CurveUpdateExpectedXMismatch` | `expectedXIn` does not match the point at `pointIndex` |
| 73 | `ValidationSimulationSuccess` | `validateCurveUpdates` dry-run passed (intentional revert) |
See [Errors](/sdk/errors) for the full list.
# Error Reference
Source: https://docs.hadron.fi/sdk/errors
All Hadron program error codes and what they mean.
When a transaction fails, the program returns a custom error code. Match the code from your transaction logs to this table.
```ts theme={null}
import { HadronError, getHadronErrorMessage } from "@hadron-fi/sdk-v2";
HadronError.StaleOracle; // 37
HadronError[37]; // "StaleOracle"
getHadronErrorMessage(37); // "Stale oracle"
```
## Common errors
| Code | Name | Description |
| - | - | - |
| 4 | `Unauthorized` | Signer is not the pool authority |
| 7 | `InsufficientLiquidity` | Not enough liquidity in the pool to fill the swap |
| 8 | `SlippageExceeded` | Output amount is below `minOut` |
| 15 | `ZeroSwapAmount` | Swap input amount is zero |
| 16 | `PoolPaused` | Pool is paused — no swaps or deposits allowed |
| 37 | `StaleOracle` | Oracle hasn't been updated recently enough (exceeds `deltaStaleness`) |
| 38 | `Expired` | Transaction slot deadline has passed |
## All error codes
| Code | Name | Description |
| - | - | - |
| 0 | `NotSigner` | Account is not a signer |
| 1 | `InvalidOwner` | Account has wrong owner |
| 2 | `InvalidAccountData` | Account data is malformed or wrong size |
| 3 | `InvalidAddress` | Account address doesn't match expected PDA |
| 4 | `Unauthorized` | Signer is not the pool authority |
| 5 | `PoolNotInitialized` | Pool has not been initialized yet |
| 6 | `PoolAlreadyInitialized` | Pool has already been initialized |
| 7 | `InsufficientLiquidity` | Not enough liquidity to fill the swap |
| 8 | `SlippageExceeded` | Output amount is below `minOut` |
| 9 | `CurvePointsNotSorted` | Curve points must be sorted by ascending x value |
| 10 | `CurveDuplicateX` | Curve has duplicate x values |
| 11 | `CurveTooManyPoints` | Curve exceeds the max points limit |
| 12 | `CurveEmpty` | Curve has zero points |
| 13 | `InvalidMidprice` | Midprice value is invalid |
| 14 | `MathOverflow` | Arithmetic overflow during calculation |
| 15 | `ZeroSwapAmount` | Swap input amount is zero |
| 16 | `PoolPaused` | Pool is paused |
| 17 | `FeeConfigAlreadyInitialized` | Global fee config already exists |
| 18 | `FeeConfigNotInitialized` | Global fee config doesn't exist yet |
| 19 | `NotFeeAdmin` | Signer is not the fee admin |
| 20 | `InvalidFeePpm` | Fee PPM value is out of range |
| 21 | `RiskCurveXOutOfRange` | Risk curve x-value must be between 0 and Q32\_ONE |
| 22 | `SequenceTooOld` | Oracle update sequence must be >= current sequence |
| 23 | `PrefabSlotOutOfRange` | Prefab slot index exceeds pool's max slots |
| 25 | `PrefabSlotNotInitialized` | Target prefab slot has no curve data |
| 27 | `SpreadConfigAlreadyInitialized` | Spread config already exists |
| 28 | `SpreadConfigNotInitialized` | Spread config doesn't exist yet |
| 29 | `TooManySpreadTriggers` | Too many spread triggers |
| 30 | `SpreadConfigRequired` | Pool has spread config but it wasn't provided in accounts |
| 31 | `SpreadConfigInvalidOwner` | Spread config account not owned by program |
| 32 | `ConfigInvalidOwner` | Config account not owned by program |
| 33 | `MidpriceOracleInvalidOwner` | Oracle account not owned by program |
| 34 | `CurveMetaInvalidOwner` | CurveMeta account not owned by program |
| 35 | `CurvePrefabsInvalidOwner` | CurvePrefabs account not owned by program |
| 36 | `FeeConfigInvalidOwner` | FeeConfig account not owned by program |
| 37 | `StaleOracle` | Oracle hasn't been updated within `deltaStaleness` slots |
| 38 | `Expired` | Transaction slot deadline has passed |
| 39 | `NoPendingNomination` | No pending authority transfer to accept |
| 40 | `NominationExpired` | Authority nomination has expired |
| 41 | `InvalidNominee` | Signer is not the nominated authority |
| 42 | `NominateSelf` | Cannot nominate current authority as new authority |
| 43 | `InvalidNominationExpiry` | Nomination expiry slot must be in the future |
| 44 | `InvalidTokenProgram` | Must be SPL Token or Token-2022 |
| 45 | `PriceCurveXOutOfRange` | Price curve x-value out of range in percent mode |
| 46 | `CurveUpdatesBufferFull` | Curve updates buffer is full — apply before submitting more |
| 47 | `CurveUpdatesInvalidOwner` | CurveUpdates account not owned by program |
| 48 | `CurveMetaMismatch` | CurveMeta address doesn't belong to this pool |
| 50 | `InvalidRiskMode` | Risk mode must be 0 (Virtual) or 1 (Integrated) |
| 51 | `IntegratedRiskUnsupportedInterpolation` | Integrated risk mode doesn't support this interpolation type |
| 52 | `VaultsNotEmpty` | Vaults must be empty before closing pool |
| 53 | `InvalidPoolState` | Pool state must be 1 (Initialized), 2 (Paused), or 3 (RejectSwaps) and different from current |
| 54 | `MaxPrefabSlotsOutOfRange` | Max prefab slots must be 1–16 |
| 55 | `MaxCurvePointsOutOfRange` | Max curve points must be 1–128 |
| 56 | `VaultAddressMismatch` | Vault address does not match derived PDA |
| 57 | `VaultTokenAccountTooSmall` | Vault token account data too small |
| 58 | `UserTokenAccountTooSmall` | User token account data too small |
| 59 | `VaultMintMismatch` | Vault token account mint does not match config |
| 60 | `UserTokenAccountInvalid` | User token account has wrong owner or mint |
| 61 | `ConcavityViolation` | Curve interpolation params violate output concavity constraints |
| 62 | `PriceFactorTooLarge` | Price factor exceeds 1.0 (Q32\_ONE) — price curves must have factors ≤ 1.0 |
| 63 | `PoolFeeConfigAlreadyInitialized` | Pool fee config already initialized |
| 64 | `PoolFeeConfigNotInitialized` | Pool fee config not initialized |
| 66 | `AssetFeeCapExceeded` | Per-asset-pair fee table is full (max entries reached) |
| 67 | `CurveUpdateSlotMismatch` | Curve update `target_slot` does not match the active slot for the op's curve type |
| 68 | `RiskCurveMonotonicityViolation` | Risk curve is not monotone (segment shape or factor sequence) |
| 70 | `InvalidTriggerSpreadBps` | Spread trigger `spread_bps` must be non-zero |
| 71 | `PoolAlreadyClosed` | Pool is closed (terminal state) — operation not permitted |
| 72 | `CurveUpdateExpectedXMismatch` | Curve update `expected_x_in` does not match the current point at `point_index` |
| 73 | `ValidationSimulationSuccess` | `ValidateCurveUpdates` dry-run succeeded (intentional revert; surfaced via simulation) |
| 74 | `NotDepositAuthority` | Signer is not the deposit authority |
| 75 | `NotWithdrawAuthority` | Signer is not the withdraw authority |
| 76 | `RotateToZero` | Rotation target is the zero pubkey |
| 77 | `RotateNoOp` | Rotation target equals the current authority (no-op) |
| 78 | `DrawdownGuardDisabled` | Drawdown guard is disabled |
# Oracle Management
Source: https://docs.hadron.fi/sdk/oracle-management
## **updateMidprice**
Update your pools midprice. Midprice can be updated when pools are in the `RejectSwaps` mode.
```typescript theme={null}
pool.updateMidprice(authority, { midpriceQ32: newMidQ32 });
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `params.midpriceQ32` | `bigint` | New midprice in Q32 format |
| `params.sequence?` | `bigint` | Optional sequence number for ordering |
Q32 (more precisely Q32.32) is a fixed-point number format where the value is stored as an integer scaled by 2^32.
In short: **stored = realValue \* 2^32**
The SDK offers a **.toQ32()** helpers to convert numbers to fixed point numbers
## **updateMidpriceAndBaseSpread**
Atomic update of both mid price and base spread values in a single instruction.
```typescript theme={null}
pool.updateMidpriceAndBaseSpread(authority, {
midpriceQ32: newMidQ32,
spreadFactorQ32: newSpreadQ32,
});
```
## **updateDeltaStaleness**
Delta staleness is a kill-switch. It allows the operator to set the threshold after which, if no oracle update exists, the pool stops quoting the asset.
```typescript theme={null}
pool.updateDeltaStaleness(authority, { deltaStaleness: 150 }); //measured in slots, up to 255
```
# Hadron Quickstart
Source: https://docs.hadron.fi/sdk/quickstart
Install the Hadron package
```mdx theme={null}
npm i @hadron-fi/sdk-v2 @solana/web3.js @solana/spl-token
```
## Initializing a pool
Initialising a hadron pool gives you your own propAMM instance.
```typescript theme={null}
import { Hadron, Q32_ONE } from "@hadron-fi/sdk-v2";
const { instructions, poolAddress, seed } = Hadron.initialize(payer.publicKey, {
//Mandatory params
mintX,
mintY,
authority: payer.publicKey,
initialMidpriceQ32: Q32_ONE, // midprice = 1.0 m
//Optional params
seed: "",
tokenProgramX: "", //pick between token2022 and SPL tokens
tokenProgramY: "", //SPL 20 Default for X and Y
maxPrefabSlots: "1-16", // this decides how many strategy presets you store. Cannot be changed (2 by default)
maxCurvePoints: "", // 16 by default
});
//// Send the instructions (group into transactions as needed)
// ...
```
With default parameters the CurvePrefabs account is \~15 KB, so two allocate instructions are returned (Solana’s single-instruction realloc limit is 10 KB). Send the allocates first, then the initialize.
## **setPoolState**
Pools are initialised by default as Reject Swaps. This is done to deposit funds to prevent indexing from aggregators
```typescript theme={null}
pool.setPoolState(authority, { newState: PoolState.Paused });
//// Send the instructions
```
| **Param** | **Description** |
| :- | :- |
| `params.Initialized` | Pool is live and will be indexed by aggregators |
| `params.Paused` | All operations are paused in the pool |
| `params.RejectSwaps` | Swaps are rejected, but the user can perform all other actions |
## **Depositing Funds**
```typescript theme={null}
Build a deposit instruction.
const ix = pool.deposit(user, {
amountX: 1_000_000n,
amountY: 2_000_000n,
expiration: 418052063, //optional parameter
});
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `user` | `PublicKey` | The user’s wallet |
| `params.amountX` | `bigint` | Amount of token X to deposit (in atoms) |
| `params.amountY` | `bigint` | Amount of token Y to deposit (in atoms) |
| `params.expiration?` | `number` | Optional slot expiration |
## **Withdraw**
Build a withdraw instruction.
```typescript theme={null}
const ix = pool.withdraw(user, {
amountX: 500_000n,
amountY: 1_000_000n,
expiration: 418052063, //optional parameter
});
```
# Reading Pool State
Source: https://docs.hadron.fi/sdk/reading-pool-state
## **refetchStates**
Re-fetches all on-chain accounts (config, oracle, curve metadata, prefabs) in parallel and updates the instance in-place. Best to run first to get the latest pool data.
```typescript theme={null}
await pool.refetchStates();
```
## **getMidprice**
Current midprice decoded from the on-chain Q32 oracle value.
```typescript theme={null}
const mid = pool.getMidprice(); // e.g. 1.0023
```
**Returns**`number`
### [****](https://docs.hadron.fi/sdk/reference/pool-functions#getspreadfactor)**getSpreadFactor**
Current base-spread factor decoded from the on-chain Q32 oracle value. A factor of 0.9995 means 5 bps of spread.
```typescript theme={null}
const factor = pool.getSpreadFactor(); // e.g. 0.9995
const bps = pool.getSpreadBps(); // e.g. 5
```
**Returns**`number`
### [****](https://docs.hadron.fi/sdk/reference/pool-functions#getactivecurveslots)**getActiveCurveSlots**
Returns the slot index for each active curve.
```typescript theme={null}
const slots = pool.getActiveCurveSlots();
// { priceBid: 0, priceAsk: 0, riskBid: 0, riskAsk: 0 }
```
**Returns**`{ priceBid: number, priceAsk: number, riskBid: number, riskAsk: number }`
## **getActiveCurves**
Decodes and returns all four active curves from the appropriate prefab data.
```typescript theme={null}
const curves = pool.getActiveCurves();
// curves.priceBid.points, curves.priceAsk.points, etc.
```
**Returns**`{ priceBid: CurveSide, priceAsk: CurveSide, riskBid: CurveSide, riskAsk: CurveSide }`
Each`CurveSide`contains:
| **Field** | **Type** | **Description** |
| :- | :- | :- |
| `numPoints` | `number` | Number of curve points |
| `defaultInterpolation` | `Interpolation` | Default interpolation mode |
| `xMode` | `CurveXMode` | X-axis mode (Native or Alternate) |
| `riskMode` | `RiskMode` | Virtual or Integrated |
| `points` | `CurvePoint[]` | The curve point array |
## **getCurveSlot**
Decode a specific curve slot by type and index.
```typescript theme={null}
const curve = pool.getCurveSlot(CurveType.PriceBid, 1);
```
**Returns**`CurveSide`
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `curveType` | `CurveType` | One of `PriceBid`, `PriceAsk`, `RiskBid`, `RiskAsk` |
| `slot` | `number` | Slot index |
***
## [****](https://docs.hadron.fi/sdk/reference/pool-functions#building-instructions)
# Configuring Your Pool
Source: https://docs.hadron.fi/sdk/rust/configure-pool
All Hadron pools have price and risk curves which encode your trading strategy into the on-chain PDA. You describe a quoting function — the midprice, how price changes with trade size, how price shifts with inventory — and Hadron runs it on chain.
## Q32 Fixed-Point Format
Many curve and oracle params use Q32 (Q32.32) format. This is a fixed-point number where the value is stored as an integer scaled by 2^32.
```rust theme={null}
use hadron_sdk_v2::helpers::math::{to_q32, spread_bps_to_q32};
let midprice = to_q32(1.05); // real value → Q32
let spread = spread_bps_to_q32(5); // 5 bps spread factor
```
Use `to_q32()` for prices and price factors. Use `spread_bps_to_q32()` for spread values.
***
## updateBaseSpread
Add a flat pool-level fee to all swaps.
```rust theme={null}
use hadron_sdk_v2::types::UpdateBaseSpreadParams;
use hadron_sdk_v2::helpers::math::spread_bps_to_q32;
let ix = pool.update_base_spread(&authority, &UpdateBaseSpreadParams {
spread_factor_q32: spread_bps_to_q32(5), // 5 bps
sequence: None,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `params.spread_factor_q32` | `u64` | Spread discount factor in Q32 — use `spread_bps_to_q32(n)` for n bps |
| `params.sequence` | `Option` | Optional sequence number for ordering concurrent updates |
***
## Creating Price Curves
Write a price curve to a prefab slot. Curves define how price moves relative to the midprice as trade size increases.
* `amount_in`: Amount in atoms (smallest token unit based on token decimals). Stored as `u64`.
* `price_factor_q32`: Distance from midprice — use `to_q32(factor)` where 1.0 = midprice, 0.99 = 1% below.
```rust theme={null}
use hadron_sdk_v2::types::*;
use hadron_sdk_v2::helpers::math::to_q32;
let ix = pool.set_curve(&authority, &SetCurveParams {
side: Side::Bid,
default_interpolation: Interpolation::Linear,
points: vec![
SetCurvePointInput {
amount_in: 100_000, // for an input of 0.1 tokens (6 decimals)
price_factor_q32: to_q32(0.99), // quote at 1% below mid
interpolation: None,
params: None,
},
SetCurvePointInput {
amount_in: 500_000,
price_factor_q32: to_q32(0.95),
interpolation: None,
params: None,
},
],
slot: Some(0),
x_mode: None,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.default_interpolation` | `Interpolation` | See interpolation types below |
| `params.points` | `Vec` | Array of `{ amount_in, price_factor_q32, interpolation?, params? }` |
| `params.slot` | `Option` | Target prefab slot (default: 0) |
| `params.x_mode` | `Option` | X-axis mode: `Native` (token atoms) or `Alternate` |
**Returns** `Instruction`
### Interpolation Types
| **Type** | **Description** |
| - | - |
| `Step` | Flat price until the next point — staircase shape |
| `Linear` | Straight line between points |
| `MarginalStep` | Step applied to the marginal (last unit) rather than the whole order |
| `Hyperbolic` | Smooth curve with configurable curvature |
| `Quadratic` | Parabolic curve between points |
| `Cubic` | Cubic spline between points |
***
## switchPriceCurve
Switch between different price curves in a single instruction. Up to 16 per curve side (set at pool initialization).
```rust theme={null}
let ix = pool.switch_price_curve(&authority, &SwitchCurveParams {
sequence: 1, // must be >= pool.curve_meta.switch_sequence
side: Side::Bid,
slot: 1,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.sequence` | `u64` | Monotonic ordering guard. Must be ≥ the pool's current switch sequence (`pool.curve_meta.switch_sequence`). |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.slot` | `u8` | Slot index to activate |
Hadron protocol fees are added at the pool level to all pools and need to be factored into pools to remain competitive. For the latest fee rates go to [https://docs.hadron.fi/fees](https://docs.hadron.fi/fees)
## setRiskCurve
Write a risk curve using percent-of-vault on the x-axis. Risk curves adjust price based on your current inventory position.
```rust theme={null}
let ix = pool.set_risk_curve(&authority, &SetRiskCurveParams {
side: Side::Bid,
default_interpolation: Interpolation::Linear,
points: vec![
SetRiskCurvePointInput {
pct_base_q32: to_q32(0.25), // 25% of vault
price_factor_q32: to_q32(0.995),
interpolation: None,
params: None,
},
SetRiskCurvePointInput {
pct_base_q32: to_q32(0.75),
price_factor_q32: to_q32(1.005),
interpolation: None,
params: None,
},
],
slot: Some(0),
x_mode: None,
risk_mode: None, // default: Virtual
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.default_interpolation` | `Interpolation` | Interpolation mode |
| `params.points` | `Vec` | Points with `pct_base_q32` (0.0–1.0 as Q32), `price_factor_q32` |
| `params.slot` | `Option` | Target prefab slot |
| `params.risk_mode` | `Option` | `Virtual` (uses notional inventory) or `Integrated` (uses actual vault balance) |
***
## setRiskCurveAbsolute
Write a risk curve using absolute token amounts on the x-axis instead of percentages.
```rust theme={null}
let ix = pool.set_risk_curve_absolute(&authority, &SetRiskCurveAbsoluteParams {
side: Side::Ask,
default_interpolation: Interpolation::Step,
points: vec![
SetRiskCurveAbsolutePointInput {
vault_balance: 1_000_000_000,
price_factor_q32: to_q32(1.002),
interpolation: None,
params: None,
},
SetRiskCurveAbsolutePointInput {
vault_balance: 2_000_000_000,
price_factor_q32: to_q32(1.006),
interpolation: None,
params: None,
},
],
slot: Some(0),
risk_mode: None,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.default_interpolation` | `Interpolation` | Interpolation mode |
| `params.points` | `Vec` | Points with `vault_balance` (in atoms), `price_factor_q32` |
| `params.slot` | `Option` | Target prefab slot |
| `params.risk_mode` | `Option` | `Virtual` or `Integrated` |
***
## switchRiskCurve
Switch between different risk curves in a single instruction. Up to 16 per curve side (set at pool initialization).
```rust theme={null}
let ix = pool.switch_risk_curve(&authority, &SwitchCurveParams {
sequence: 1, // must be >= pool.curve_meta.switch_sequence
side: Side::Ask,
slot: 2,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.sequence` | `u64` | Monotonic ordering guard. Must be ≥ the pool's current switch sequence (`pool.curve_meta.switch_sequence`). |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.slot` | `u8` | Slot index to activate |
***
## submit\_curve\_updates / apply\_curve\_updates
Two-step batched curve editing for modifying individual points without rewriting an entire curve. Submit a list of edits, then apply them atomically.
```rust theme={null}
use hadron_sdk_v2::types::{CurveUpdateOp, CurveUpdateOpKind, CurveType};
// `sequence` must be >= the CurveUpdates account's current sequence (monotonic guard).
let ix1 = pool.submit_curve_updates(&authority, 1, &[
CurveUpdateOp {
curve_type: CurveType::PriceBid,
op_kind: CurveUpdateOpKind::Edit,
point_index: 0,
interpolation: Interpolation::Linear,
amount_in: 200_000,
price_factor_q32: to_q32(0.98),
params: [0; 4],
target_slot: 0, // active slot this op targets — apply rejects on mismatch
expected_x_in: 0, // expected current x_in at point_index (ignored for Add)
},
]);
let ix2 = pool.apply_curve_updates(&authority);
// Send ix1, confirm, then send ix2
```
> **Buffer behavior** Each submit replaces the queued batch (max 28 ops). Queued ops are also applied by the next swap, so validate a batch before sending it. See [Curve Management](/sdk/rust/curve-management) for the simulate-then-send flow and cost comparison with `set_curve`.
For a visual helper please visit [https://dashboard.hadron.fi/curves](https://dashboard.hadron.fi/curves)
# Crank
Source: https://docs.hadron.fi/sdk/rust/crank
Hadron pools are designed for active management. Unlike passive AMMs (like Uniswap), the pool **does not update the midprice based on reserves**. You, the operator, must push price updates from your own price engine. This is the crank.
The crank loop is the heartbeat of a live Hadron pool. A typical production crank:
1. Fetches the latest market price from your price source (CEX feed, oracle, internal model)
2. Computes the new midprice and optionally a new spread
3. Builds an `update_midprice_and_base_spread` instruction
4. Sends and confirms the transaction
5. Waits for the next tick, then repeats
```rust theme={null}
Example coming soon
```
# Curve Management
Source: https://docs.hadron.fi/sdk/rust/curve-management
Queue curve point edits with submit_curve_updates for 58 CU alongside your oracle updates, validate them in simulation, and let the next swap apply them.
`set_curve` rewrites an entire curve slot and validates every segment of it in the same transaction. For smooth curves this can cost tens of thousands of compute units, all in your update transaction.
Queued curve updates split that work in two:
* **Your transaction** only writes the edits into a small buffer. `submit_curve_updates` costs **58 CU** regardless of batch size.
* **The validation work** runs when the batch is applied, which by default is the **next swap** on the pool.
A smaller compute-unit limit means a cheaper priority fee for the same price per CU, and a transaction that lands more reliably when blocks are busy. That makes queued updates a good fit for operators who update the curve alongside every oracle tick.
Three instructions make up this flow:
| **Instruction** | **What it does** | **Accounts** |
| - | - | - |
| `submit_curve_updates` | Writes a batch of up to 28 point edits into the pool's `CurveUpdates` buffer. Nothing on the curve changes yet. | authority, curve\_updates |
| `validate_curve_updates` | Dry-runs the queued batch against the live curve, then **always reverts**. Use it only in simulation. | authority, curve\_meta, curve\_prefabs, curve\_updates |
| `apply_curve_updates` | Applies the queued batch to the curve and clears the buffer. | authority, curve\_meta, curve\_prefabs, curve\_updates |
Because the next swap applies the queued batch, an invalid batch makes **every swap on the pool fail** with that batch's error until you replace it. Always validate a batch with `validate_curve_updates` before you send it.
***
## The recommended flow
1. Build the `submit_curve_updates` instruction.
2. **Simulate** a transaction containing `submit_curve_updates` followed by `validate_curve_updates`.
* Error `73` (`ValidationSimulationSuccess`) on the validate instruction means the batch is valid.
* Any other error is the exact error the real apply would return.
3. **Remove** `validate_curve_updates` and send `submit_curve_updates`, typically in the same transaction as your `update_midprice`. The next swap applies the batch.
`validate_curve_updates` never commits state, so a transaction that contains it always fails. Include it only when simulating.
```mermaid theme={null}
flowchart LR
A[Build submit_curve_updates] --> B["simulate: submit + validate"]
B -->|"error 73"| C["send: update_midprice + submit"]
B -->|"any other error"| D[Fix the ops]
D --> A
C --> E["next swap applies the batch"]
```
***
## submit\_curve\_updates
Requires the SDK's `rpc` feature (for `Hadron::load`) plus `solana-sdk`, `solana-rpc-client`, `solana-rpc-client-api` and `solana-compute-budget-interface`.
```rust theme={null}
use hadron_sdk_v2::accounts::decode_curve_updates;
use hadron_sdk_v2::hadron::Hadron;
use hadron_sdk_v2::helpers::math::to_q32;
use hadron_sdk_v2::types::{CurveType, CurveUpdateOp, CurveUpdateOpKind, Interpolation};
let pool = Hadron::load(&rpc, &pool_address)?;
// Read the live curve so each op can pin the point it expects to edit.
let bid = pool.get_active_curves()?.price_bid;
let slot = pool.get_active_curve_slots().price_bid;
// `sequence` must be >= the buffer's current sequence.
let sequence = decode_curve_updates(&rpc.get_account_data(&pool.addresses.curve_updates)?)?.sequence;
let submit_ix = pool.submit_curve_updates(&authority.pubkey(), sequence + 1, &[
CurveUpdateOp {
curve_type: CurveType::PriceBid,
op_kind: CurveUpdateOpKind::Edit,
point_index: 3,
interpolation: Interpolation::Linear,
amount_in: bid.points[3].amount_in, // keep the same x, move the price
price_factor_q32: to_q32(0.9975),
params: [0; 4],
target_slot: slot, // the active PriceBid slot
expected_x_in: bid.points[3].amount_in, // current x_in at point_index
},
]);
```
| **Field** | **Type** | **Description** |
| - | - | - |
| `sequence` | `u64` | Must be ≥ the buffer's current sequence. Protects against out-of-order submissions. |
| `curve_type` | `CurveType` | `PriceBid`, `PriceAsk`, `RiskBid` or `RiskAsk` |
| `op_kind` | `CurveUpdateOpKind` | `Edit` overwrites the point at `point_index`. `Add` inserts before it. `Remove` deletes it. |
| `point_index` | `u8` | Index of the point to edit, insert before, or remove |
| `interpolation` | `Interpolation` | Interpolation for the edited or added point |
| `amount_in` | `u64` | The point's x value, in the same units as the curve's x mode |
| `price_factor_q32` | `u64` | New price (or risk) factor in Q32. Use `to_q32(n)`. |
| `params` | `[u8; 4]` | Interpolation parameters, for example the Hyperbolic `k` |
| `target_slot` | `u8` | The slot that was active when you built the op. The op fails with `CurveUpdateSlotMismatch` (67) if the active slot has changed. |
| `expected_x_in` | `u64` | The x value currently at `point_index`. The op fails with `CurveUpdateExpectedXMismatch` (72) if the curve has moved. Ignored for `Add`. |
Each submit **replaces** the buffer; it does not append. If you submit twice before the buffer is applied, only the second batch remains.
***
## validate\_curve\_updates
Simulate `submit_curve_updates` and `validate_curve_updates` together, so the dry-run checks the batch you are about to send:
```rust theme={null}
use hadron_sdk_v2::errors::HadronError;
use solana_sdk::instruction::InstructionError;
use solana_sdk::message::{v0, VersionedMessage};
use solana_sdk::transaction::{TransactionError, VersionedTransaction};
use solana_rpc_client_api::config::RpcSimulateTransactionConfig;
let validate_ix = pool.validate_curve_updates(&authority.pubkey());
let message = v0::Message::try_compile(
&authority.pubkey(),
&[submit_ix.clone(), validate_ix],
&[],
rpc.get_latest_blockhash()?,
)?;
let sim_tx = VersionedTransaction {
signatures: vec![Default::default()],
message: VersionedMessage::V0(message),
};
let sim = rpc.simulate_transaction_with_config(&sim_tx, RpcSimulateTransactionConfig {
sig_verify: false,
..Default::default()
})?;
match sim.value.err.map(TransactionError::from) {
Some(TransactionError::InstructionError(1, InstructionError::Custom(73))) => {
// Valid: ValidationSimulationSuccess
}
Some(TransactionError::InstructionError(_, InstructionError::Custom(code))) => {
return Err(format!("curve update rejected: {:?}", HadronError::from_code(code)).into());
}
other => return Err(format!("simulation failed: {other:?}").into()),
}
```
On failure the simulation logs name the op that failed, for example `L-06 apply[2] ct=0 kind=Edit idx=4 fail=point-op-validation`.
You can also call `validate_curve_updates` on its own to check a batch that is already queued on-chain.
***
## Send the update
**Deferred validation can halt swaps on your pool.** `submit_curve_updates` does not check your ops. They are only checked when the batch is applied, which is usually inside the next swap. If the batch is invalid, **every swap on the pool fails** until you submit a valid batch to replace it.
This is the tradeoff for keeping validation out of your update transaction. Never send a batch without first simulating it with `validate_curve_updates`. If you cannot simulate first, use `set_curve`, which rejects a bad curve in your own transaction.
After the simulation passes, remove `validate_curve_updates` and send `submit_curve_updates` with your oracle update under a tight compute-unit limit:
```rust theme={null}
use hadron_sdk_v2::types::UpdateMidpriceParams;
use solana_compute_budget_interface::ComputeBudgetInstruction;
let mid_ix = pool.update_midprice(&authority.pubkey(), &UpdateMidpriceParams {
midprice_q32: to_q32(142.35),
sequence: None,
});
let message = v0::Message::try_compile(
&authority.pubkey(),
&[
ComputeBudgetInstruction::set_compute_unit_limit(1_000),
ComputeBudgetInstruction::set_compute_unit_price(50_000),
mid_ix,
submit_ix, // validate_ix removed
],
&[],
rpc.get_latest_blockhash()?,
)?;
let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[&authority])?;
rpc.send_and_confirm_transaction(&tx)?;
```
The Hadron instructions in this transaction use under 100 CU (`update_midprice` about 37, `submit_curve_updates` 58). Each compute-budget instruction adds about 150 CU, so a limit of 1,000 leaves plenty of headroom.
Until the next swap applies the batch, the curve account still holds the old points, and off-chain quoters (including aggregators) quote from it. The swap that applies the batch executes against the new points. Send `apply_curve_updates` yourself if you need the new points live and quoted before the next trade.
***
## apply\_curve\_updates
```rust theme={null}
let ix = pool.apply_curve_updates(&authority.pubkey());
```
Applies every queued op in order and clears the buffer, exactly as the next swap would. If any op fails, the whole instruction fails and nothing changes. Applying an empty buffer is a no-op. Use it when you want the edits live immediately and are willing to pay the validation CU yourself.
***
## Batch rules
* **Ops apply in order.** Each op is checked against the curve as the earlier ops in the same batch left it, not against the final curve. A set of edits that is valid in the end can still fail midway. For example, to move two neighbouring points to the right, edit the right-hand point first. If you edit the left-hand point first, it lands past its old neighbour and the op fails with `CurvePointsNotSorted` (error `9`). Validate catches this, so reorder the ops and simulate again.
* **Indexes follow earlier ops.** `point_index` and `expected_x_in` refer to the curve after the earlier ops in the batch. After an `Add` or `Remove`, the points to its right shift by one.
* **One failure fails the batch.** No partial apply.
* **Max 28 ops** per submit.
***
## Where the compute units go
Validation cost depends on the curve's **interpolation** and on **how many segments are checked**. `set_curve` checks every segment. A queued op checks only the segments next to the point it touches (at most two). Measured on the deployed program:
| **Curve** | **`set_curve`, paid by you** | **1 queued op, paid at apply** |
| - | -: | -: |
| Flat Step, 2 points | 667 | 678 |
| MarginalStep, 5 points | 1,535 | 1,003 |
| MarginalStep, 21 points | 5,407 | 1,003 |
| Linear, 2 points | 3,151 | 3,167 |
| Linear, 5 points | 10,903 | 5,687 |
| Linear, 21 points | 52,247 | 5,687 |
With the queue, your transaction pays only the 58 CU for `submit_curve_updates`. The right-hand column is paid by whoever applies the batch: the next swap, or you if you send `apply_curve_updates`. `validate_curve_updates` costs the same as an apply, but only in simulation.
* **Updating the curve with every oracle tick:** use the queue. Your update transaction stays under 100 CU of Hadron work however long or smooth the curve is.
* **Short or cheap curves:** `set_curve` on a 2-point flat curve already costs under 700 CU, so the queue saves little.
* **Keep batches small.** The swap that applies the batch pays about 1,000–5,700 CU per op on top of its own cost. A large batch can push an aggregator's swap over the compute budget it simulated. For large reshapes, use `set_curve` or send `apply_curve_updates` yourself.
***
## Errors
| **Code** | **Name** | **Meaning** |
| - | - | - |
| 9 | `CurvePointsNotSorted` | The op would leave x values out of order |
| 61 | `ConcavityViolation` | The op would make the curve non-concave |
| 62 | `PriceFactorTooLarge` | Price factor above 1.0 (`Q32_ONE`) |
| 67 | `CurveUpdateSlotMismatch` | `target_slot` is not the active slot for that curve type |
| 72 | `CurveUpdateExpectedXMismatch` | `expected_x_in` does not match the point at `point_index` |
| 73 | `ValidationSimulationSuccess` | `validate_curve_updates` dry-run passed (intentional revert) |
See [Error Reference](/sdk/rust/error-reference) for the full list.
# Error Reference
Source: https://docs.hadron.fi/sdk/rust/error-reference
When a transaction fails, the program returns a custom error code. Match the code from your transaction logs to this table.
```rust theme={null}
use hadron_sdk_v2::HadronError;
match HadronError::from_code(code) {
Some(HadronError::StaleOracle) => { /* wait for the next oracle update */ }
Some(e) => eprintln!("{e:?}: {e}"), // variant name: message
None => eprintln!("unknown error code {code}"),
}
```
## Common errors
| Code | Name | Description |
| :- | :- | :- |
| 4 | `Unauthorized` | Signer is not the pool authority |
| 7 | `InsufficientLiquidity` | Not enough liquidity in the pool to fill the swap |
| 8 | `SlippageExceeded` | Output amount is below `minOut` |
| 15 | `ZeroSwapAmount` | Swap input amount is zero |
| 16 | `PoolPaused` | Pool is paused — no swaps or deposits allowed |
| 37 | `StaleOracle` | Oracle hasn't been updated recently enough (exceeds `deltaStaleness`) |
| 38 | `Expired` | Transaction slot deadline has passed |
## All error codes
| Code | Name | Description |
| :- | :- | :- |
| 0 | `NotSigner` | Account is not a signer |
| 1 | `InvalidOwner` | Account has wrong owner |
| 2 | `InvalidAccountData` | Account data is malformed or wrong size |
| 3 | `InvalidAddress` | Account address doesn't match expected PDA |
| 4 | `Unauthorized` | Signer is not the pool authority |
| 5 | `PoolNotInitialized` | Pool has not been initialized yet |
| 6 | `PoolAlreadyInitialized` | Pool has already been initialized |
| 7 | `InsufficientLiquidity` | Not enough liquidity to fill the swap |
| 8 | `SlippageExceeded` | Output amount is below `minOut` |
| 9 | `CurvePointsNotSorted` | Curve points must be sorted by ascending x value |
| 10 | `CurveDuplicateX` | Curve has duplicate x values |
| 11 | `CurveTooManyPoints` | Curve exceeds the max points limit |
| 12 | `CurveEmpty` | Curve has zero points |
| 13 | `InvalidMidprice` | Midprice value is invalid |
| 14 | `MathOverflow` | Arithmetic overflow during calculation |
| 15 | `ZeroSwapAmount` | Swap input amount is zero |
| 16 | `PoolPaused` | Pool is paused |
| 17 | `FeeConfigAlreadyInitialized` | Global fee config already exists |
| 18 | `FeeConfigNotInitialized` | Global fee config doesn't exist yet |
| 19 | `NotFeeAdmin` | Signer is not the fee admin |
| 20 | `InvalidFeePpm` | Fee PPM value is out of range |
| 21 | `RiskCurveXOutOfRange` | Risk curve x-value must be between 0 and Q32\_ONE |
| 22 | `SequenceTooOld` | Oracle update sequence must be >= current sequence |
| 23 | `PrefabSlotOutOfRange` | Prefab slot index exceeds pool's max slots |
| 25 | `PrefabSlotNotInitialized` | Target prefab slot has no curve data |
| 27 | `SpreadConfigAlreadyInitialized` | Spread config already exists |
| 28 | `SpreadConfigNotInitialized` | Spread config doesn't exist yet |
| 29 | `TooManySpreadTriggers` | Too many spread triggers |
| 30 | `SpreadConfigRequired` | Pool has spread config but it wasn't provided in accounts |
| 31 | `SpreadConfigInvalidOwner` | Spread config account not owned by program |
| 32 | `ConfigInvalidOwner` | Config account not owned by program |
| 33 | `MidpriceOracleInvalidOwner` | Oracle account not owned by program |
| 34 | `CurveMetaInvalidOwner` | CurveMeta account not owned by program |
| 35 | `CurvePrefabsInvalidOwner` | CurvePrefabs account not owned by program |
| 36 | `FeeConfigInvalidOwner` | FeeConfig account not owned by program |
| 37 | `StaleOracle` | Oracle hasn't been updated within `deltaStaleness` slots |
| 38 | `Expired` | Transaction slot deadline has passed |
| 39 | `NoPendingNomination` | No pending authority transfer to accept |
| 40 | `NominationExpired` | Authority nomination has expired |
| 41 | `InvalidNominee` | Signer is not the nominated authority |
| 42 | `NominateSelf` | Cannot nominate current authority as new authority |
| 43 | `InvalidNominationExpiry` | Nomination expiry slot must be in the future |
| 44 | `InvalidTokenProgram` | Must be SPL Token or Token-2022 |
| 45 | `PriceCurveXOutOfRange` | Price curve x-value out of range in percent mode |
| 46 | `CurveUpdatesBufferFull` | Curve updates buffer is full — apply before submitting more |
| 47 | `CurveUpdatesInvalidOwner` | CurveUpdates account not owned by program |
| 48 | `CurveMetaMismatch` | CurveMeta address doesn't belong to this pool |
| 50 | `InvalidRiskMode` | Risk mode must be 0 (Virtual) or 1 (Integrated) |
| 51 | `IntegratedRiskUnsupportedInterpolation` | Integrated risk mode doesn't support this interpolation type |
| 52 | `VaultsNotEmpty` | Vaults must be empty before closing pool |
| 53 | `InvalidPoolState` | Pool state must be 1 (Initialized), 2 (Paused), or 3 (RejectSwaps) and different from current |
| 54 | `MaxPrefabSlotsOutOfRange` | Max prefab slots must be 1–16 |
| 55 | `MaxCurvePointsOutOfRange` | Max curve points must be 1–128 |
| 56 | `VaultAddressMismatch` | Vault address does not match derived PDA |
| 57 | `VaultTokenAccountTooSmall` | Vault token account data too small |
| 58 | `UserTokenAccountTooSmall` | User token account data too small |
| 59 | `VaultMintMismatch` | Vault token account mint does not match config |
| 60 | `UserTokenAccountInvalid` | User token account has wrong owner or mint |
| 61 | `ConcavityViolation` | Curve interpolation params violate output concavity constraints |
| 62 | `PriceFactorTooLarge` | Price factor exceeds 1.0 (Q32\_ONE) — price curves must have factors ≤ 1.0 |
| 63 | `PoolFeeConfigAlreadyInitialized` | Pool fee config already initialized |
| 64 | `PoolFeeConfigNotInitialized` | Pool fee config not initialized |
| 66 | `AssetFeeCapExceeded` | Per-asset-pair fee table is full (max entries reached) |
| 67 | `CurveUpdateSlotMismatch` | Curve update `target_slot` does not match the active slot for the op's curve type |
| 68 | `RiskCurveMonotonicityViolation` | Risk curve is not monotone (segment shape or factor sequence) |
| 70 | `InvalidTriggerSpreadBps` | Spread trigger `spread_bps` must be non-zero |
| 71 | `PoolAlreadyClosed` | Pool is closed (terminal state) — operation not permitted |
| 72 | `CurveUpdateExpectedXMismatch` | Curve update `expected_x_in` does not match the current point at `point_index` |
| 73 | `ValidationSimulationSuccess` | `ValidateCurveUpdates` dry-run succeeded (intentional revert; surfaced via simulation) |
| 74 | `NotDepositAuthority` | Signer is not the deposit authority |
| 75 | `NotWithdrawAuthority` | Signer is not the withdraw authority |
| 76 | `RotateToZero` | Rotation target is the zero pubkey |
| 77 | `RotateNoOp` | Rotation target equals the current authority (no-op) |
| 78 | `DrawdownGuardDisabled` | Drawdown guard is disabled |
# Hadron Quickstart
Source: https://docs.hadron.fi/sdk/rust/hadron-quickstart
Install and use the Hadron Rust SDK in a few minutes.
> ## Documentation Index
> Fetch the complete documentation index at: [https://docs.hadron.fi/llms.txt](https://docs.hadron.fi/llms.txt)
## Install
```toml theme={null}
# Cargo.toml
hadron-sdk-v2 = "0.3"
solana-sdk = "3"
```
Or via cargo:
```bash theme={null}
cargo add hadron-sdk-v2
cargo add solana-sdk
```
The SDK requires `solana-sdk` for `Pubkey` and `Instruction` types.
***
## Initializing a Pool
Initialising a Hadron pool gives you your own propAMM instance.
```rust theme={null}
use hadron_sdk_v2::hadron::Hadron;
use hadron_sdk_v2::types::InitializeParams;
use hadron_sdk_v2::helpers::math::to_q32;
use hadron_sdk_v2::constants::HADRON_PROGRAM_ID;
let (instructions, pool_address, seed) = Hadron::initialize(&payer, &InitializeParams {
seed: None, // auto-generated; use Some(n) for deterministic address
mint_x,
mint_y,
authority: payer,
creator: None, // defaults to `authority` (signer of AllocateCurvePrefabs)
initial_midprice_q32: to_q32(1.0), // midprice = 1.0
lp_self_rotate: false, // false = custodial (admin can rotate LP authorities)
max_prefab_slots: None, // default: 2 — cannot be changed after init
max_curve_points: None, // default: 16 — cannot be changed after init
combined_factor_max_q32: None, // defaults to Q32_ONE (1.0)
token_program_x: None, // default: SPL Token; pass Token-2022 pubkey if needed
token_program_y: None,
}, &HADRON_PROGRAM_ID);
// Send the instructions (group into transactions as needed)
```
> **Two transactions required**
> With default parameters the `CurvePrefabs` account is \~15 KB. Two allocate instructions are returned because Solana's single-instruction realloc limit is 10 KB. Send the allocates first in one transaction, then send the initialize instruction.
| **Param** | **Type** | **Description** |
| - | - | - |
| `payer` | `&Pubkey` | Transaction fee payer and account funder |
| `params.seed` | `Option` | Pool seed. Auto-generated if `None`. Allows multiple pools per token pair. |
| `params.mint_x` | `Pubkey` | Base token mint |
| `params.mint_y` | `Pubkey` | Quote token mint |
| `params.authority` | `Pubkey` | Pool authority (manages curves, oracle, state) |
| `params.creator` | `Option` | Creator of the curve prefabs account (must sign `AllocateCurvePrefabs`). Defaults to `authority`. |
| `params.initial_midprice_q32` | `u64` | Starting midprice in Q32 format — use `to_q32(price)` |
| `params.lp_self_rotate` | `bool` | `false` = custodial (admin can rotate deposit/withdraw authorities); `true` = only the LP role-holder can. Immutable after init. |
| `params.max_prefab_slots` | `Option` | Max curve prefab slots, 1–16. **Cannot be changed after init.** Default: 2 |
| `params.max_curve_points` | `Option` | Max points per curve, 1–128. **Cannot be changed after init.** Default: 16 |
| `params.combined_factor_max_q32` | `Option` | Hard ceiling on the combined price×risk factor. Defaults to `Q32_ONE` (1.0). |
| `params.token_program_x` | `Option` | Token program for X — SPL Token or Token-2022. Default: SPL Token |
| `params.token_program_y` | `Option` | Token program for Y — SPL Token or Token-2022. Default: SPL Token |
**Returns** `(Vec, Pubkey, u64)` — instructions, pool address, seed
***
## Loading an Existing Pool
The Rust SDK is **sync-first** — you provide raw account data and it decodes everything locally. No async required.
```rust theme={null}
use hadron_sdk_v2::hadron::Hadron;
// Fetch account data however you like (RPC, Geyser, websocket, etc.)
// let config_data = rpc.get_account_data(&pool_address)?;
// let oracle_data = rpc.get_account_data(&oracle_pda)?;
// let meta_data = rpc.get_account_data(&curve_meta_pda)?;
// let prefabs_data = rpc.get_account_data(&curve_prefabs_pda)?;
// let fee_config_data = rpc.get_account_data(&fee_config_pda)?; // pool-specific or global
let pool = Hadron::from_accounts(
pool_address,
&config_data,
&oracle_data,
&meta_data,
&prefabs_data,
&fee_config_data,
)?;
```
> **RPC convenience loader**
> Enable the `rpc` feature for a one-liner: `cargo add hadron-sdk-v2 --features rpc`
> Then call `Hadron::load(&rpc_client, &pool_address)` which fetches all accounts automatically.
***
## setPoolState
Pools are initialised by default as `RejectSwaps`. This lets you deposit funds before the pool is indexed by aggregators.
```rust theme={null}
use hadron_sdk_v2::types::{SetPoolStateParams, PoolState};
let ix = pool.set_pool_state(&authority, &SetPoolStateParams {
new_state: PoolState::Initialized,
});
```
| **State** | **Description** |
| - | - |
| `Initialized` | Pool is live and will be indexed by aggregators |
| `RejectSwaps` | Swaps are rejected; deposits and withdrawals still allowed |
| `Paused` | All operations paused — no swaps, deposits, or withdrawals |
***
## Depositing Funds
Build a deposit instruction.
```rust theme={null}
use hadron_sdk_v2::types::DepositParams;
let ix = pool.deposit(&user, &DepositParams {
amount_x: 1_000_000, // in atoms (smallest token unit)
amount_y: 2_000_000,
expiration: None, // optional slot deadline
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `user` | `&Pubkey` | The user's wallet |
| `params.amount_x` | `u64` | Amount of token X to deposit (in atoms) |
| `params.amount_y` | `u64` | Amount of token Y to deposit (in atoms) |
| `params.expiration` | `Option` | Optional slot expiration |
***
## Withdraw
Build a withdraw instruction.
```rust theme={null}
use hadron_sdk_v2::types::WithdrawParams;
let ix = pool.withdraw(&user, &WithdrawParams {
amount_x: 500_000,
amount_y: 1_000_000,
expiration: None,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `user` | `&Pubkey` | The user's wallet |
| `params.amount_x` | `u64` | Amount of token X to withdraw (in atoms) |
| `params.amount_y` | `u64` | Amount of token Y to withdraw (in atoms) |
| `params.expiration` | `Option` | Optional slot expiration |
# Oracle Management
Source: https://docs.hadron.fi/sdk/rust/oracle-management
Hadron pools do not update the midprice automatically based on reserves. As an operator, you are responsible for pushing price updates from your own price engine. This is what makes Hadron a propAMM rather than a passive AMM.
***
## updateMidprice
Update your pool's midprice. Midprice can be updated when pools are in the `RejectSwaps` mode.
```rust theme={null}
use hadron_sdk_v2::types::UpdateMidpriceParams;
use hadron_sdk_v2::helpers::math::to_q32;
let ix = pool.update_midprice(&authority, &UpdateMidpriceParams {
midprice_q32: to_q32(1.05),
sequence: None,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `params.midprice_q32` | `Q32` | New midprice in Q32 format — use `to_q32(price)` |
| `params.sequence` | `Option` | Optional sequence number. Updates with a lower sequence than the current on-chain value are rejected, preventing out-of-order application when sending concurrent updates. |
Q32 (more precisely Q32.32) is a fixed-point number format where the value is stored as an integer scaled by 2^32.
In short: **stored = realValue \* 2^32**
The SDK offers a **.toQ32()** helpers to convert numbers to fixed point numbers
## updateMidpriceAndBaseSpread
Atomic update of both midprice and base spread in a single instruction. Prefer this over two separate calls when updating both together.
```rust theme={null}
use hadron_sdk_v2::types::UpdateMidpriceAndBaseSpreadParams;
use hadron_sdk_v2::helpers::math::{to_q32, spread_bps_to_q32};
let ix = pool.update_midprice_and_base_spread(&authority, &UpdateMidpriceAndBaseSpreadParams {
midprice_q32: to_q32(1.05),
spread_factor_q32: spread_bps_to_q32(5),
sequence: None,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `params.midprice_q32` | `u64` | New midprice in Q32 format |
| `params.spread_factor_q32` | `u64` | New spread factor in Q32 — use `spread_bps_to_q32(n)` for n bps |
| `params.sequence` | `Option` | Optional sequence number for ordering |
***
## updateDeltaStaleness
Delta staleness is a kill-switch. If no oracle update has been received within this many **slots**, the pool stops quoting.
```rust theme={null}
use hadron_sdk_v2::types::UpdateDeltaStalenessParams;
let ix = pool.update_delta_staleness(&authority, &UpdateDeltaStalenessParams {
delta_staleness: 150, // slots — ~60 seconds at 400ms/slot
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `params.delta_staleness` | `u8` | Number of slots without an oracle update before the pool halts quoting. At \~400ms per slot, 150 slots ≈ 60 seconds. |
> **Recommended usage** Set `delta_staleness` to a value slightly above your oracle update frequency. If your crank pushes updates every 10 slots, a staleness of 30–50 slots gives headroom for missed updates without leaving the pool quoting a stale price for too long.
# Pool Admin Control
Source: https://docs.hadron.fi/sdk/rust/pool-admin-control
## setPoolState
Pools are initialised by default as `RejectSwaps`. This lets you deposit funds before the pool is indexed by aggregators.
```rust theme={null}
use hadron_sdk_v2::types::{SetPoolStateParams, PoolState};
let ix = pool.set_pool_state(&authority, &SetPoolStateParams {
new_state: PoolState::Initialized,
});
```
What each state blocks:
| **State** | **Swaps** | **Deposits** | **Withdrawals** |
| - | - | - | - |
| `Initialized` | ✅ | ✅ | ✅ |
| `RejectSwaps` | ❌ | ✅ | ✅ |
| `Paused` | ❌ | ❌ | ❌ |
Which authority can make which transition:
| **From → To** | **Pool Authority** | **Quoting Authority** | **Pauser Authority** |
| - | - | - | - |
| `Initialized → Paused` | ✅ | ❌ | ✅ |
| `Initialized → RejectSwaps` | ✅ | ✅ | ❌ |
| `Paused → Initialized` | ✅ | ❌ | ❌ |
| `Paused → RejectSwaps` | ✅ | ❌ | ❌ |
| `RejectSwaps → Initialized` | ✅ | ❌ | ❌ |
| `RejectSwaps → Paused` | ✅ | ❌ | ✅ |
***
## nominateAuthority / acceptAuthority
Hadron pools have multiple authorities that can each be delegated to different wallets to split the surface area of control. Authority transfer is a two-step process to prevent accidental lockout.
```rust theme={null}
use hadron_sdk_v2::types::NominateAuthorityParams;
// Step 1: current authority nominates a new one
let ix1 = pool.nominate_authority(&authority, &NominateAuthorityParams {
new_authority: new_pk,
expiry_slot: current_slot + 50_000, // nomination expires after ~6 hours
});
// Step 2: new authority accepts
let ix2 = pool.accept_authority(&new_pk);
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `params.new_authority` | `Pubkey` | The wallet being nominated |
| `params.expiry_slot` | `u64` | Slot after which the nomination expires. Set this to a near-future slot — if the new authority doesn't accept in time, nominate again. |
> **Authority types** The pool has three distinct authority roles: **Pool Authority** (full admin — can do everything), **Quoting Authority** (can push oracle updates and switch pool to `RejectSwaps`), and **Pauser Authority** (can pause and unpause the pool). Each can be delegated to a different wallet using `nominate_authority` for the relevant role.
***
## closePool
Closes the pool and reclaims rent. Both vaults must be fully empty before calling this.
```rust theme={null}
let ix = pool.close_pool(&authority);
```
> **Drain vaults first** Call `withdraw` with the full vault balance for both token X and token Y before closing. The transaction will fail with error `VaultsNotEmpty` (code 52) if any balance remains.
***
## Properties
These are available on any `Hadron` instance after creation:
| **Property** | **Type** | **Description** |
| - | - | - |
| `pool.pool_address` | `Pubkey` | The pool's on-chain address |
| `pool.program_id` | `Pubkey` | Program ID |
| `pool.addresses` | `PoolAddresses` | All derived PDA addresses for this pool |
| `pool.config` | `DecodedConfig` | Decoded config account data |
| `pool.oracle` | `DecodedMidpriceOracle` | Decoded oracle data |
| `pool.curve_meta` | `DecodedCurveMeta` | Decoded curve metadata |
| `pool.curve_prefabs_data` | `Vec` | Raw curve prefabs data — decode with `get_active_curves()` |
# Reading Pool State
Source: https://docs.hadron.fi/sdk/rust/reading-pool-state
Call `refetch_states` first to ensure all decoded values on the instance reflect the latest on-chain data before reading or building instructions.
## refetch\_states (rpc feature)
Re-fetches all on-chain accounts (config, oracle, curve metadata, prefabs) and updates the instance in-place.
```rust theme={null}
// Requires the `rpc` feature
pool.refetch_states(&rpc_client)?;
```
If you are managing account data yourself (Geyser, websocket), reconstruct the pool with `Hadron::from_accounts` using the latest data instead.
***
## getMidprice
Current midprice decoded from the on-chain Q32 oracle value.
```rust theme={null}
let mid = pool.get_midprice(); // e.g. 1.0023
```
**Returns** `f64`
***
## getSpreadFactor / getSpreadBps
Current base spread decoded from the on-chain Q32 oracle value. A factor of `0.9995` means 5 bps of spread.
```rust theme={null}
let factor = pool.get_spread_factor(); // e.g. 0.9995
let bps = pool.get_spread_bps(); // e.g. 5.0
```
**Returns** `f64`
***
## getActiveCurveSlots
Returns the slot index for each currently active curve.
```rust theme={null}
let slots = pool.get_active_curve_slots();
// ActiveCurveSlots { price_bid: 0, price_ask: 0, risk_bid: 0, risk_ask: 0 }
```
**Returns** `ActiveCurveSlots`
***
## getActiveCurves
Decodes and returns all four active curves from the appropriate prefab data.
```rust theme={null}
let curves = pool.get_active_curves()?;
// curves.price_bid.points, curves.price_ask.points, etc.
```
**Returns** `Result`
Each `CurveSide` contains:
| **Field** | **Type** | **Description** |
| - | - | - |
| `num_points` | `u8` | Number of curve points |
| `default_interpolation` | `Interpolation` | Default interpolation mode |
| `x_mode` | `CurveXMode` | `Native` (token atoms) or `Alternate` |
| `risk_mode` | `RiskMode` | `Virtual` or `Integrated` |
| `points` | `Vec` | The decoded curve point array |
***
## getCurveSlot
Decode a specific curve slot by type and index.
```rust theme={null}
use hadron_sdk_v2::types::CurveType;
let curve = pool.get_curve_slot(CurveType::PriceBid, 1)?;
```
**Returns** `Result`
| **Param** | **Type** | **Description** |
| - | - | - |
| `curve_type` | `CurveType` | One of `PriceBid`, `PriceAsk`, `RiskBid`, `RiskAsk` |
| `slot` | `u8` | Slot index |
# Pool Functions
Source: https://docs.hadron.fi/sdk/rust/reference/pool-functions
Complete guide to creating and using Hadron in Rust.
## Initializing a new pool
### Hadron::initialize
Builds all the instructions needed to create a new on-chain pool: account allocation + initialization. Returns the instructions, derived pool address, and seed.
The allocate instruction(s) must be confirmed **before** sending the initialize instruction (they can be in separate transactions, or together if they fit).
```rust theme={null}
use hadron_sdk_v2::hadron::Hadron;
use hadron_sdk_v2::types::InitializeParams;
use hadron_sdk_v2::helpers::math::to_q32;
use hadron_sdk_v2::constants::HADRON_PROGRAM_ID;
let (instructions, pool_address, seed) = Hadron::initialize(&payer, &InitializeParams {
seed: None, // auto-generated
mint_x,
mint_y,
authority: payer,
creator: None, // defaults to `authority`
initial_midprice_q32: to_q32(1.0), // midprice = 1.0
lp_self_rotate: false, // false = custodial (admin can rotate LP authorities)
max_prefab_slots: None, // default: 2
max_curve_points: None, // default: 16
combined_factor_max_q32: None, // defaults to Q32_ONE (1.0)
token_program_x: None, // default: SPL Token
token_program_y: None, // default: SPL Token
}, &HADRON_PROGRAM_ID);
// Send the instructions (group into transactions as needed)
```
| Param | Type | Description |
| - | - | - |
| `payer` | `&Pubkey` | Transaction fee payer and account funder |
| `params.seed` | `Option` | Pool seed. Auto-generated if `None`. Allows multiple pools per token pair. |
| `params.mint_x` | `Pubkey` | Base token mint |
| `params.mint_y` | `Pubkey` | Quote token mint |
| `params.authority` | `Pubkey` | Pool authority (manages curves, oracle, state) |
| `params.creator` | `Option` | Creator of the curve prefabs account (must sign `AllocateCurvePrefabs`). Defaults to `authority`. |
| `params.initial_midprice_q32` | `u64` | Starting midprice in Q32 fixed-point format |
| `params.lp_self_rotate` | `bool` | `false` = custodial (admin can rotate deposit/withdraw authorities); `true` = only the LP role-holder can. Immutable after init. |
| `params.token_program_x` | `Option` | Token program for X (default: SPL Token) |
| `params.token_program_y` | `Option` | Token program for Y (default: SPL Token) |
| `params.max_prefab_slots` | `Option` | Max curve prefab slots (default: 2) |
| `params.max_curve_points` | `Option` | Max points per curve (default: 16) |
| `params.combined_factor_max_q32` | `Option` | Hard ceiling on the combined price×risk factor. Defaults to `Q32_ONE` (1.0). |
**Returns** `(Vec, Pubkey, u64)` — instructions, pool address, seed
With default parameters the CurvePrefabs account is \~15 KB, so two allocate instructions are returned (Solana's single-instruction realloc limit is 10 KB). Send the allocates first, then the initialize.
***
## Loading an existing pool
### Hadron::from\_accounts
Create a `Hadron` instance from raw account data bytes. This is sync — no RPC calls. You fetch the accounts however you like (RPC, Geyser, etc.) and pass the raw data in.
```rust theme={null}
use hadron_sdk_v2::hadron::Hadron;
let pool = Hadron::from_accounts(
pool_address,
&config_data,
&oracle_data,
&curve_meta_data,
&curve_prefabs_data,
&fee_config_data,
)?;
```
| Param | Type | Description |
| - | - | - |
| `pool_address` | `Pubkey` | The pool's on-chain address |
| `config_data` | `&[u8]` | Raw config account data |
| `oracle_data` | `&[u8]` | Raw midprice oracle account data |
| `curve_meta_data` | `&[u8]` | Raw curve metadata account data |
| `curve_prefabs_data` | `&[u8]` | Raw curve prefabs account data |
| `fee_config_data` | `&[u8]` | Raw fee config account data (pool-specific when `has_pool_fee`, else global) |
**Returns** `Result`
Enable the `rpc` feature (`cargo add hadron-sdk-v2 --features rpc`) for a convenience
`Hadron::load(&rpc_client, &pool_address)` method that fetches accounts over RPC.
***
## Reading pool state
### get\_midprice
Current midprice decoded from the on-chain Q32 oracle value.
```rust theme={null}
let mid = pool.get_midprice(); // e.g. 1.0023
```
**Returns** `f64`
### get\_spread\_factor / get\_spread\_bps
Current spread factor and basis-point equivalent.
```rust theme={null}
let factor = pool.get_spread_factor(); // e.g. 0.9995
let bps = pool.get_spread_bps(); // e.g. 5.0
```
**Returns** `f64`
### get\_active\_curve\_slots
Returns the slot index for each active curve.
```rust theme={null}
let slots = pool.get_active_curve_slots();
// ActiveCurveSlots { price_bid: 0, price_ask: 0, risk_bid: 0, risk_ask: 0 }
```
**Returns** `ActiveCurveSlots`
### get\_active\_curves
Decodes and returns all four active curves from prefab data.
```rust theme={null}
let curves = pool.get_active_curves()?;
// curves.price_bid.points, curves.price_ask.points, etc.
```
**Returns** `Result`
Each `CurveSide` contains:
| Field | Type | Description |
| - | - | - |
| `num_points` | `u8` | Number of curve points |
| `default_interpolation` | `Interpolation` | Default interpolation mode |
| `x_mode` | `CurveXMode` | X-axis mode (Native or Alternate) |
| `risk_mode` | `RiskMode` | Virtual or Integrated |
| `points` | `Vec` | The curve point array |
### get\_curve\_slot
Decode a specific curve slot by type and index.
```rust theme={null}
use hadron_sdk_v2::types::CurveType;
let curve = pool.get_curve_slot(CurveType::PriceBid, 1)?;
```
| Param | Type | Description |
| - | - | - |
| `curve_type` | `CurveType` | One of `PriceBid`, `PriceAsk`, `RiskBid`, `RiskAsk` |
| `slot` | `u8` | Slot index |
**Returns** `Result`
***
## Building instructions
All methods below return an `Instruction`. They do **not** send a transaction — add the instruction to a `Transaction` and submit it yourself.
### swap
Build a swap instruction.
```rust theme={null}
use hadron_sdk_v2::types::SwapParams;
let ix = pool.swap(&user, &SwapParams {
is_x: true, // true = sell X for Y, false = sell Y for X
amount_in: 1_000_000,
min_out: 0, // set > 0 for slippage protection
fee_recipient,
expiration: None,
});
```
| Param | Type | Description |
| - | - | - |
| `user` | `&Pubkey` | The user's wallet |
| `params.is_x` | `bool` | Direction: `true` = X→Y, `false` = Y→X |
| `params.amount_in` | `u64` | Input amount in token atoms |
| `params.min_out` | `u64` | Minimum output (slippage protection) |
| `params.fee_recipient` | `Pubkey` | Address to receive fees |
| `params.expiration` | `Option` | Optional slot expiration |
### deposit
Build a deposit instruction.
```rust theme={null}
use hadron_sdk_v2::types::DepositParams;
let ix = pool.deposit(&user, &DepositParams {
amount_x: 1_000_000,
amount_y: 2_000_000,
expiration: None,
});
```
| Param | Type | Description |
| - | - | - |
| `user` | `&Pubkey` | The user's wallet |
| `params.amount_x` | `u64` | Amount of token X to deposit (in atoms) |
| `params.amount_y` | `u64` | Amount of token Y to deposit (in atoms) |
| `params.expiration` | `Option` | Optional slot expiration |
### withdraw
Build a withdraw instruction.
```rust theme={null}
use hadron_sdk_v2::types::WithdrawParams;
let ix = pool.withdraw(&user, &WithdrawParams {
amount_x: 500_000,
amount_y: 1_000_000,
expiration: None,
});
```
| Param | Type | Description |
| - | - | - |
| `user` | `&Pubkey` | The user's wallet |
| `params.amount_x` | `u64` | Amount of token X to withdraw (in atoms) |
| `params.amount_y` | `u64` | Amount of token Y to withdraw (in atoms) |
| `params.expiration` | `Option` | Optional slot expiration |
***
## Curve management
### set\_curve
Write a price curve to a prefab slot.
```rust theme={null}
use hadron_sdk_v2::types::*;
use hadron_sdk_v2::helpers::math::to_q32;
let ix = pool.set_curve(&authority, &SetCurveParams {
side: Side::Bid,
default_interpolation: Interpolation::Linear,
points: vec![
SetCurvePointInput {
amount_in: 100_000,
price_factor_q32: to_q32(0.99),
interpolation: None,
params: None,
},
SetCurvePointInput {
amount_in: 500_000,
price_factor_q32: to_q32(0.95),
interpolation: None,
params: None,
},
],
slot: Some(0),
x_mode: None,
});
```
| Param | Type | Description |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.default_interpolation` | `Interpolation` | `Step`, `Linear`, `MarginalStep`, `Hyperbolic`, `Quadratic`, `Cubic` |
| `params.points` | `Vec` | Array of curve points with `amount_in`, `price_factor_q32`, optional `interpolation` and `params` |
| `params.slot` | `Option` | Target prefab slot (default: 0) |
| `params.x_mode` | `Option` | X-axis mode |
### set\_risk\_curve
Write a risk curve using percent-of-vault x-axis.
| Param | Type | Description |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.default_interpolation` | `Interpolation` | Interpolation mode |
| `params.points` | `Vec` | Points with `pct_base_q32`, `price_factor_q32`, etc. |
| `params.slot` | `Option` | Target prefab slot |
| `params.x_mode` | `Option` | X-axis mode |
| `params.risk_mode` | `Option` | `Virtual` or `Integrated` |
### set\_risk\_curve\_absolute
Write a risk curve using absolute token amounts on the x-axis.
| Param | Type | Description |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.default_interpolation` | `Interpolation` | Interpolation mode |
| `params.points` | `Vec` | Points with `vault_balance`, `price_factor_q32`, etc. |
| `params.slot` | `Option` | Target prefab slot |
| `params.risk_mode` | `Option` | `Virtual` or `Integrated` |
### switch\_price\_curve / switch\_risk\_curve
Activate a previously written curve slot (hot path — no curve data uploaded).
```rust theme={null}
let ix1 = pool.switch_price_curve(&authority, &SwitchCurveParams { sequence: 1, side: Side::Bid, slot: 1 });
let ix2 = pool.switch_risk_curve(&authority, &SwitchCurveParams { sequence: 1, side: Side::Ask, slot: 2 });
```
| Param | Type | Description |
| - | - | - |
| `authority` | `&Pubkey` | Pool authority |
| `params.sequence` | `u64` | Monotonic ordering guard. Must be ≥ the pool's current switch sequence (`pool.curve_meta.switch_sequence`). |
| `params.side` | `Side` | `Bid` or `Ask` |
| `params.slot` | `u8` | Slot to activate |
### submit\_curve\_updates / apply\_curve\_updates
Two-step batched curve editing. Submit a list of point edits, then apply them.
```rust theme={null}
// `sequence` must be >= the CurveUpdates account's current sequence (monotonic guard).
let ix1 = pool.submit_curve_updates(&authority, 1, &[
CurveUpdateOp {
curve_type: CurveType::PriceBid,
op_kind: CurveUpdateOpKind::Edit,
point_index: 0,
interpolation: Interpolation::Linear,
amount_in: 200_000,
price_factor_q32: to_q32(0.98),
params: [0; 4],
target_slot: 0, // active slot this op targets — apply rejects on mismatch
expected_x_in: 0, // expected current x_in at point_index (ignored for Add)
},
]);
let ix2 = pool.apply_curve_updates(&authority);
```
***
## Oracle management
### update\_midprice
```rust theme={null}
use hadron_sdk_v2::types::UpdateMidpriceParams;
let ix = pool.update_midprice(&authority, &UpdateMidpriceParams {
midprice_q32: to_q32(1.05),
sequence: None,
});
```
| Param | Type | Description |
| - | - | - |
| `params.midprice_q32` | `u64` | New midprice in Q32 format |
| `params.sequence` | `Option` | Optional sequence number for ordering |
### update\_base\_spread
```rust theme={null}
use hadron_sdk_v2::types::UpdateBaseSpreadParams;
let ix = pool.update_base_spread(&authority, &UpdateBaseSpreadParams {
spread_factor_q32: spread_bps_to_q32(5), // 5 bps
sequence: None,
});
```
| Param | Type | Description |
| - | - | - |
| `params.spread_factor_q32` | `u64` | Spread discount factor in Q32 (e.g. `spread_bps_to_q32(5)` = 5 bps) |
| `params.sequence` | `Option` | Optional sequence number |
### update\_midprice\_and\_base\_spread
Atomic update of both values in a single instruction.
```rust theme={null}
use hadron_sdk_v2::types::UpdateMidpriceAndBaseSpreadParams;
let ix = pool.update_midprice_and_base_spread(&authority, &UpdateMidpriceAndBaseSpreadParams {
midprice_q32: to_q32(1.05),
spread_factor_q32: spread_bps_to_q32(5),
sequence: None,
});
```
***
## Admin
### nominate\_authority / accept\_authority
Two-step authority transfer.
```rust theme={null}
use hadron_sdk_v2::types::NominateAuthorityParams;
let ix1 = pool.nominate_authority(&authority, &NominateAuthorityParams {
new_authority: new_pk,
expiry_slot: 300_000_000,
});
// ... new_authority signs:
let ix2 = pool.accept_authority(&new_pk);
```
### set\_pool\_state
```rust theme={null}
use hadron_sdk_v2::types::{SetPoolStateParams, PoolState};
let ix = pool.set_pool_state(&authority, &SetPoolStateParams {
new_state: PoolState::Paused,
});
```
States: `Uninitialized`, `Initialized`, `Paused`, `RejectSwaps`, `Closed` (set via `close_pool`). `set_pool_state` only accepts `Initialized`, `Paused`, or `RejectSwaps`.
### update\_delta\_staleness
```rust theme={null}
use hadron_sdk_v2::types::UpdateDeltaStalenessParams;
let ix = pool.update_delta_staleness(&authority, &UpdateDeltaStalenessParams {
delta_staleness: 150,
});
```
### close\_pool
Closes the pool and reclaims rent. Requires authority and all vaults to be empty.
```rust theme={null}
let ix = pool.close_pool(&authority);
```
***
## Properties
These are available on any `Hadron` instance:
| Property | Type | Description |
| - | - | - |
| `pool.program_id` | `Pubkey` | Program ID |
| `pool.pool_address` | `Pubkey` | The pool's on-chain address |
| `pool.addresses` | `PoolAddresses` | All derived PDA addresses for this pool |
| `pool.config` | `DecodedConfig` | Decoded config account data |
| `pool.oracle` | `DecodedMidpriceOracle` | Decoded oracle data |
| `pool.curve_meta` | `DecodedCurveMeta` | Decoded curve metadata |
| `pool.curve_prefabs_data` | `Vec` | Raw curve prefabs data (decode with `get_active_curves()`) |
# Segmenting Flow
Source: https://docs.hadron.fi/sdk/rust/segmenting-flow
Hadron lets you add extra spread for specific addresses or programs, letting you distinguish toxic flow from retail flow.
**Examples:**
* **Reduce cyclic arbitrage:** Add spread for trades involving competing venue programs or specific bot addresses that buy from your pool and sell to a competing AMM.
* **Be more competitive with retail traders:** Keep your base spread tight for retail flow (e.g. txns carrying the Phantom/Solflare fee accounts or the Jupiter Ultra fee accounts) and add spread only on the toxic/arb flow you target with triggers.
***
## updateSpreadConfig
Replace the pool's spread trigger list in one call.
> **Full replacement** This is a full replacement of all existing triggers. Always pass the complete final list you want on-chain — not just the new entries.
```rust theme={null}
use hadron_sdk_v2::types::{UpdateSpreadConfigParams, SpreadTriggerInput};
let ix = pool.update_spread_config(&admin, &UpdateSpreadConfigParams {
triggers: vec![
SpreadTriggerInput {
account: competing_amm_pool,
program_id: competing_amm_program_id, // trigger fires when this program touches the account
spread_bps: 25, // add 25 bps to penalise arb flow
},
SpreadTriggerInput {
account: some_account,
program_id: some_program_id,
spread_bps: 10,
},
],
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `admin` | `&Pubkey` | Spread config admin signer |
| `params.triggers` | `Vec` | Full list of spread triggers to store (replaces previous list) |
| `params.triggers[].account` | `Pubkey` | Account key to match against the instruction context |
| `params.triggers[].program_id` | `Pubkey` | Program that must use `account` for the trigger to fire. Replaces v1's `match_mode` — triggers are scoped to a specific calling program. |
| `params.triggers[].spread_bps` | `u16` | Extra spread in basis points to add when matched. Must be non-zero. |
# Swapping Through Hadron
Source: https://docs.hadron.fi/sdk/rust/swapping-through-hadron
Build a swap instruction.
```rust theme={null}
use hadron_sdk_v2::types::SwapParams;
let ix = pool.swap(&user, &SwapParams {
is_x: true, // true = sell X for Y, false = sell Y for X
amount_in: 1_000_000, // in atoms
min_out: 950_000, // minimum acceptable output — set this to protect against slippage
fee_recipient,
expiration: None,
});
```
| **Param** | **Type** | **Description** |
| - | - | - |
| `user` | `&Pubkey` | The user's wallet |
| `params.is_x` | `bool` | Direction: `true` = X→Y, `false` = Y→X |
| `params.amount_in` | `u64` | Input amount in token atoms |
| `params.min_out` | `u64` | Minimum output amount. The transaction fails with `SlippageExceeded` (error 8) if the pool cannot fill at this level or better. Set to `0` only in tests. |
| `params.fee_recipient` | `Pubkey` | Address that receives the operator's fee portion of the spread |
| `params.expiration` | `Option` | Optional slot deadline — transaction fails with `Expired` (error 38) after this slot |
> **Multi-pool routing** Hadron does not currently support native matching across multiple propAMMs for the same asset. For multi-pool routing, use one of the supported aggregators or contact the Hadron team.
# Segmenting Flow
Source: https://docs.hadron.fi/sdk/segmenting-flow
Hadron allows you to segment flow i.e add extra spread for certain addresses or programs to distinguish toxic flow from retail flow.
**Examples:**
* **Reduce cyclic arbitrage:** Reduce arbitrage from cyclic trading e.g. bots that buy from your pool and sell to a competing AMM pool. You can do this by adding a spread for trades involving a competing venue or specific addresses.
* **Be more competitive with retail traders:** Keep your base spread tight for retail flow (e.g. txns carrying the Phantom/Solflare fee account or the Jupiter Ultra fee accounts) and add spread only on the toxic/arb flow you target with triggers.
## **updateSpreadConfig**
Replace the pool’s spread trigger list in one call.
This is a **full replacement** of existing triggers, so pass the complete final triggers array you want on-chain.
```typescript theme={null}
pool.updateSpreadConfig(admin, {
triggers: [
{
account: competingAmmPool,
programId: competingAmmProgramId, // trigger fires when this program touches the account
spreadBps: 25,
},
{
account: someAccount,
programId: someProgramId,
spreadBps: 10,
},
],
});
```
| Param | Type | Description |
| :- | :- | :- |
| admin | PublicKey | Spread config admin signer. |
| params | UpdateSpreadConfigParams | Update payload. |
| params.triggers | SpreadTriggerInput\[] | Full list of spread triggers to store (replaces previous list). |
| params.triggers\[].account | PublicKey | Account key to match against the instruction context. |
| params.triggers\[].programId | PublicKey | Program that must use `account` for the trigger to fire. Replaces v1's `matchMode` — triggers are now scoped to a specific calling program. |
| params.triggers\[].spreadBps | number | Extra spread in basis points to apply when matched. Must be non-zero (encoded on-chain as `u16`). |
# Swapping Through Hadron Pools
Source: https://docs.hadron.fi/sdk/swaps-ts
Build a swap instruction.
```typescript theme={null}
const ix = pool.swap(user, {
isX: true, // true = sell token X for Y, false = sell Y for X
amountIn: 1_000_000n,
minOut: 0n,
feeRecipient: feePk,
});
```
| **Param** | **Type** | **Description** |
| :- | :- | :- |
| `user` | `PublicKey` | The user’s wallet |
| `params.isX` | `boolean` | Direction: `true` = X→Y, `false` = Y→X |
| `params.amountIn` | `bigint` | Input amount in token atoms |
| `params.minOut` | `bigint` | Minimum output (slippage protection) |
| `params.feeRecipient` | `PublicKey` | Address to receive fees |
| `params.expiration?` | `number` | Optional slot expiration |
Hadron does not currently support custom matching across multiple propAMMs for the same asset natively. For multi-prop matching, please use one of our supported aggregators or reach out to us to discuss more.
# Prefabs
Source: https://docs.hadron.fi/toxic-cliw-copied-1
Pre-built curves for advanced makers
Prefabs allow operators to save multiple curves in the contract, allowing them to switch between strategies by only updating a single parameter in the instruction. This allows for switching between vastly different (or similar) quoting functions and risk curves in under 40 CUs. Makers can store up to 16 prefabs for each curve (64 total) on Hadron, set at pool initialization (2 by default).
## How Prefabs Work:
Concepts:
* **Slot (0 to maxPrefabSlots−1, up to 0–15):** Which prefab slot to write to (SetCurve / SetRiskCurve) or to activate (SwitchPriceCurve / SwitchRiskCurve).
* **Side:** `bid` (0) or `ask` (1). Price and risk curves have one active slot per side
* **Active slot:** The slot index currently used for quoting. Swap reads the active curve from prefabs by this index.
## Example: Switching between multiple price and risk curves
Store two ask curves in different slots (e.g. slot 0 = tight spread, slot 1 = wide spread). Then switch the active ask curve by sending a single **SwitchPriceCurve** instruction with `side: Side.Ask`, `slot: 0` or `1`.
* *1. Set an ask curve into slot 0 with (e.g. tight spread)*
```typescript theme={null}
// `pool` is a loaded Hadron instance; `authority` is the pool authority (see SDK ▸ Quickstart)
const setAskSlot0Ix = pool.setCurve(authority, {
side: Side.Ask,
defaultInterpolation: Interpolation.Linear,
slot: 0,
points: [
{ amountIn: 0n, priceFactor: 1.0010, interpolation: Interpolation.Step },
{ amountIn: 1000n, priceFactor: 1.0020, interpolation: Interpolation.Linear },
{ amountIn: 2000n, priceFactor: 1.0050, interpolation: Interpolation.Linear },
{ amountIn: 5000n, priceFactor: 1.0100, interpolation: Interpolation.Step },
{ amountIn: 10000n, priceFactor: 1.0150, interpolation: Interpolation.Step },
],
});
```
* *2. Set a different ask curve into slot 1 (e.g. wide spread)*
```typescript theme={null}
// Same structure, wider spreads (e.g. 50, 100, 200, 400, 600 bps)
// Slot 1 = "wide" strategy
const setAskSlot1Ix = pool.setCurve(authority, {
side: Side.Ask,
defaultInterpolation: Interpolation.Linear,
slot: 1,
points: [
{ amountIn: 0n, priceFactor: 1.0050, interpolation: Interpolation.Step },
{ amountIn: 1000n, priceFactor: 1.0100, interpolation: Interpolation.Linear },
{ amountIn: 2000n, priceFactor: 1.0200, interpolation: Interpolation.Linear },
{ amountIn: 5000n, priceFactor: 1.0400, interpolation: Interpolation.Step },
{ amountIn: 10000n, priceFactor: 1.0600, interpolation: Interpolation.Step },
],
});
```
* *3. Switch the active ask curve to slot 0 or 1 (one parameter, \~38 CUs)*\*
* No curve data is sent in the switch instruction—only `sequence`, `side`, and `slot`. The program updates the active slot index
```typescript theme={null}
// Switch active ask price curve to slot 1 (~38 CUs)
const switchPriceAskIx = pool.switchPriceCurve(authority, {
sequence: 1n, // must be >= pool.curveMeta.switchSequence
side: Side.Ask,
slot: 1,
});
```