01 Routing boundary
Pump and PumpSwap do not call external programs before or after a swap. There is no Uniswap-v4-style callback inside the venue. Pump Hooks therefore runs hooks at its own router boundary: a trader calls route_curve or route_amm to buy or sell, the router runs before-callbacks, performs the trade by CPI into Pump (bonding curve) or PumpSwap (after migration), observes the executed amounts and runs after-callbacks — all in one atomic transaction.
A hooked market is a MarketConfig PDA at ["market", mint, namespace] for an existing Pump token. Many namespaces can hook the same token; the creator's market is marked creator-owned when the namespace is the Pump creator.
02 Hook ABI
Callback wire values are frozen. Reserved values are never reused or reordered; V1 subscribes to the six trade callbacks only.
| # | Callback | Semantics |
|---|---|---|
| 0 | BeforeBuy | Routed buy, before the Pump CPI. Can deny. |
| 1 | AfterBuy | Routed buy, after the Pump CPI. Sees executed amounts; can still fail the transaction. |
| 2 | BeforeSell | Routed sell, before the Pump CPI. Can deny. |
| 3 | AfterSell | Routed sell, after the Pump CPI. |
| 4 | BeforeSwap | Either side, before the Pump CPI. Can deny. |
| 5 | AfterSwap | Either side, after the Pump CPI. |
| 6 | BeforeLiquidity | Reserved (wire value 6). |
| 7 | AfterLiquidity | Reserved (wire value 7). |
| 8 | BeforeFeeAction | Reserved (wire value 8). |
| 9 | AfterFeeAction | Reserved (wire value 9). |
| 10 | OnGraduation | Reserved (wire value 10). |
| 11 | OnStrategySignal | Reserved (wire value 11). |
Each slot holds 48 bytes of config and 48 bytes of state. Modules implement one interface and return an 8-byte decision:
pub trait PumpHook {
const MODULE_ID: u16;
fn validate_config(config: &[u8; 48]) -> Result<(), HookError>;
fn init_state(_config: &[u8; 48], _state: &mut [u8; 48], _now: i64) {}
fn on_callback(env: &mut HookEnv<'_>) -> Result<HookDecision, HookError>;
}
// 8 bytes, deterministic
pub struct HookDecision { pub allow: u8, pub _pad: u8, pub reason_code: u16, pub flags: u32 }Capabilities are granted at install and enforced by the executor, not trusted from the module:
| READ_MARKET | Read market config, venue reserves and the trade context |
| VETO_ROUTED_SWAP | Deny a routed trade in a before-callback (reverts the transaction) |
| WRITE_HOOK_STATE | Persist its 48-byte slot state on the market |
| WRITE_USER_STATE | Update the trading wallet's per-market state |
| AWARD_POINTS | Credit XP / points in the wallet's per-market ledger |
| TREASURY_SIGNAL | Emit treasury allocation intents for the strategy vault |
| STRATEGY_EXECUTION | Bound or request vault strategy execution (TWAP, orders) |
| ORACLE_READ | Read an external oracle account (reserved; not granted in V1) |
| EXTERNAL_CPI | Call an external hook program (reserved; no permissionless CPI hooks in V1) |
| EMIT_SIGNAL | Emit or consume signed strategy signals |
V1 ships 9 first-party modules (see Hooks). External programs as hooks (CPI hooks) are specified but not enabled in V1.
03 Execution order
- Validate the market, venue accounts and kill switch. Paused markets and disabled modules fail closed.
- Run before-callbacks in ascending priority. The first DENY reverts the whole transaction with
HookDenied(6018). - CPI into Pump or PumpSwap with the trader's own signature and slippage limit.
- Measure executed amounts from balance deltas, not from inputs.
- Run after-callbacks and active policies; commit state only for modules holding the write capability.
- Emit
RoutedSwapEvent,HookExecutionEventandPolicyExecutionEvents, decodable by the SDK, CLI, explorer and indexer.
Limits: 6 hook slots, 4 runtime hooks, 4 policies per market. Measured overhead on top of a direct PumpSwap buy is about 21–32k CU for 0–4 hooks (see docs/benchmarks.md).
04 Policy VM
Policies are WHEN trigger IF conditions THEN actions programs evaluated by a bounded interpreter: at most 8 conditions, 4 actions and 4 registers; no loops, no external calls, at most 10000 points per action. DENY is valid only on Before triggers and reports error 9000 + reason. Vault actions only record an intent; no vault is deployed in V1. Build policies in the Composer.
05 SENTIENT
The on-chain half of an autonomous risk engine. An off-chain engine signs a StrategySignal with Ed25519; the program checks the market-configured signer, nonce, freshness and TTL, then moves the market between NORMAL, CAUTIOUS, DEFENSIVE and CIRCUIT_BREAKER. Signals can only restrict routed trades: the engine never holds user funds, never signs for users and never trades. Every regime expires.
06 SDK
@pump-hooks/sdk builds every instruction, decodes every account and event, and validates hook configs and policies with the same rules as the program.
import { Connection, PublicKey } from "@solana/web3.js";
import { PumpHooksClient, PUMP_ALT, ModuleId, CONFIG_PRESETS } from "@pump-hooks/sdk";
const client = new PumpHooksClient(new Connection("https://api.devnet.solana.com"), {
lookupTables: [PUMP_ALT.devnet],
});
// 1. Detect an existing Pump token: curve or PumpSwap, Token / Token-2022, reserves.
const token = await client.detect(new PublicKey(MINT));
// 2. Preview create + install + a routed buy as one simulated transaction.
const preview = await client.previewStack({
mint: token.mint,
authority: wallet,
hooks: [
{ module: ModuleId.VolumeGuard, config: CONFIG_PRESETS[ModuleId.VolumeGuard] },
{ module: ModuleId.HolderXp, config: CONFIG_PRESETS[ModuleId.HolderXp] },
],
quoteIn: 100_000_000n, // 0.1 SOL
});
const result = await client.simulate(preview, wallet);
result.trace?.entries; // [{ callback, moduleId, allowed, reasonCode }, ...]
// 3. Route a buy through an existing hooked market.
const market = await client.getMarket(MARKET);
const tx = await client.buy({ market, user: wallet, quoteIn: 100_000_000n });
// 4. Decode any confirmed transaction.
const trace = await client.getExecutionTrace(signature);07 CLI
The CLI never reads your Solana CLI config; pass --url and --keypair explicitly. Write commands accept --dry-run.
pump-hooks inspect <mint> --url https://api.devnet.solana.com pump-hooks market create <mint> --keypair ./authority.json pump-hooks install <market> volume-guard --config maxBuyQuote=5,windowSecs=3600 --dry-run pump-hooks policy create <market> --spec policy.json pump-hooks simulate <market> --buy 0.1 pump-hooks tx <signature>
08 Security model
- No token delegate and no persistent custody: users sign each routed trade; the router moves only what that trade moves.
- Hooks cannot transfer user assets; they can deny, record, and update their own state and per-wallet ledgers.
- Fail-closed: any hook error, bad config or unexpected venue state reverts the transaction.
- Fees Pump Hooks does not control are never redirected. Treasury Router only describes how explicitly deposited funds would be split; the vault that would enforce it is not deployed.
- A global kill switch can pause routing or disable individual modules; it cannot move funds.
- Status: not audited. Not deployed on mainnet. Read
docs/threat-model-pump-router.mdbefore using real funds.
