I. Why a new standard#
Transfer hooks are played out. Token-2022 made them safe by making them powerless: over a transfer, a hook has one bit of say, to let it stand or make it fail.
What a Token-2022 transfer hook is#
Checked against Token-2022 v11.0.0 (the build on mainnet) and Solana’s own documentation. These are the only statements this site makes about transfer hooks; the numbers point to the sources at the end of this section.
Deliberate
It runs only on transfers. Minting, burning and withdrawing withheld fees never call it, and a transfer to the same account skips it. 14
It runs after the balances have changed. It gets the transfer’s accounts read-only, and no account reaches it as a signer, so it can only sign for its own PDAs. 124
It cannot change the amount, take part of it, burn it or send it elsewhere. Over the transfer itself it can only let it stand or make the whole transaction fail. It can write to its own accounts and call other programs, for example to charge a separate fee in another token from an account the sender approved in advance. 12
It is not told why a transfer happened: no buy or sell flag, no price, no counterpart asset. It can guess, by comparing addresses with pool vaults it was configured with, or by reading the top-level instructions. A venue it does not know about looks like a gift. 12
Remembering anything per holder takes a separate account per holder, owned by the hook, which someone has to create and fund before it is used, and which every transfer has to carry. 12
The only fee Token-2022 takes from the tokens moved is the separate transfer-fee extension: one rate per mint, charged on every transfer alike (buys, sells and gifts), withheld in the recipient’s account and collected later. 3
Venues treat hooks as a risk. Meteora’s DBC revokes a token’s hook when it graduates, and DAMM v2 accepts a hook mint permissionlessly only once its hook is revoked. Orca lists hook mints case by case, and its criteria forbid hooks that charge fees or move tokens. Raydium’s CPMM and CLMM refuse hook mints unless an admin registers them. 5678
What a Bordrless hook is#
It keeps the safety rule that matters: it never gets the user’s signature. What it wants done to an operation, it answers, and the token program or the DEX applies the answer. Uniswap v4’s hook idea, on Solana, at the token level.
- Runs Token hooks run on transfers, mints and burns. Pool hooks run on pool creation, liquidity and swaps.
- Sees A pool hook sees the whole trade: direction, input, output, reserves, both mints, the trader and the wallet that receives.
- Answers A
before_*callback can take up to three cuts from the amount, each to an account it names. A pool hook can also burn part of it and set the swap’s LP fee. - Keeps A token hook keeps 64 bytes of state for each holder inside the holding itself. The token program writes it from the hook’s answer. No extra account per holder.
- Bounds The token program never applies more than the amount being moved, and the DEX checks every account a hook names.
Runs on
- Bordrless hook
- Token hooks: transfers, mints and burns. Pool hooks: pool creation, liquidity and every swap.
When
- Bordrless hook
- Before and after the operation. What a before callback answers is applied with it.
Sees
- Token-2022 transfer hook
- The transfer, read-only. Not why it happens: no buy or sell flag, no price, no pool. It can only guess, from addresses it was configured with or the transaction’s instructions. 12
- Bordrless hook
- Owners, balances, supply, who signed and whether as a delegate. A pool hook sees the whole trade: direction, amounts in and out, reserves, both tokens, the trader and the wallet that receives.
Signatures
- Bordrless hook
- Never the user’s signature either. The calling program signs every call, so a hook knows who is calling.
Over the amount
- Token-2022 transfer hook
- Let it stand, or make the whole transaction fail. 1
- Bordrless hook
- Up to three cuts from the amount, each to an account it names. Never more than the amount being moved.
Burn
- Token-2022 transfer hook
- It can’t change an amount or burn. 1
- Bordrless hook
- A pool hook can burn part of a swap.
A swap’s fee
- Token-2022 transfer hook
- It can’t set a pool’s fee.
- Bordrless hook
- before_swap can set that swap’s LP fee.
State per holder
A fee from the tokens moved
- Token-2022 transfer hook
- Only the transfer-fee extension: one rate per mint, on every transfer alike. A hook can charge a separate fee in another token from an account the sender approved in advance; that is a side charge, not a cut of the transfer. 13
- Bordrless hook
- What the hook answers, as cuts from the amount, per operation: a trade can pay and a gift between wallets need not.
Refuse a transfer
- Token-2022 transfer hook
- Yes.
- Bordrless hook
- Yes.
| Aspect | Token-2022 transfer hook | Bordrless hook |
|---|---|---|
| Runs on | Transfers only. Mints and burns never call it. 14 | Token hooks: transfers, mints and burns. Pool hooks: pool creation, liquidity and every swap. |
| When | After the balances have changed. 14 | Before and after the operation. What a before callback answers is applied with it. |
| Sees | The transfer, read-only. Not why it happens: no buy or sell flag, no price, no pool. It can only guess, from addresses it was configured with or the transaction’s instructions. 12 | Owners, balances, supply, who signed and whether as a delegate. A pool hook sees the whole trade: direction, amounts in and out, reserves, both tokens, the trader and the wallet that receives. |
| Signatures | No account reaches it as a signer. 14 | Never the user’s signature either. The calling program signs every call, so a hook knows who is calling. |
| Over the amount | Let it stand, or make the whole transaction fail. 1 | Up to three cuts from the amount, each to an account it names. Never more than the amount being moved. |
| Burn | It can’t change an amount or burn. 1 | A pool hook can burn part of a swap. |
| A swap’s fee | It can’t set a pool’s fee. | before_swap can set that swap’s LP fee. |
| State per holder | An extra account per holder, created and funded before use, carried by every transfer. 12 | 64 bytes inside every holding, written by the token program from the hook’s answer. |
| A fee from the tokens moved | Only the transfer-fee extension: one rate per mint, on every transfer alike. A hook can charge a separate fee in another token from an account the sender approved in advance; that is a side charge, not a cut of the transfer. 13 | What the hook answers, as cuts from the amount, per operation: a trade can pay and a gift between wallets need not. |
| Refuse a transfer | Yes. | Yes. |
Where a transfer hook can do one of these at all, it needs an account the trader approved in advance, an extra account per holder, or a guess about which transfers are trades. Here each is part of the operation.
Sources#
- 1Solana docs: Transfer Hook extensionsolana.com/docs/tokens/extensions/transfer-hook
- 2Solana docs: Transfer Hook integrationsolana.com/docs/tokens/extensions/transfer-hook-integration
- 3Solana docs: Transfer Fees extensionsolana.com/docs/tokens/extensions/transfer-fees
- 4Token-2022 program v11.0.0github.com/solana-program/token-2022 · program/src/processor.rs
- 5Orca: Token extensionsdocs.orca.so/developers/architecture/token-extensions
- 6Raydium: Token-2022 supportdocs.raydium.io/reference/token-2022-support
- 7Meteora: DAMM v2 Token-2022 supportdocs.meteora.ag/core-products/damm-v2/token-2022-support
- 8Meteora: DBC transfer hook poolsdocs.meteora.ag/core-products/dbc/transfer-hook-pools
II. The token standard#
Tokens live in a program built for hooks. A mint names its hook and the callbacks it subscribes to; every holding keeps room for the hook's state.
A mint is an account of the token program (bordrless_token): decimals, supply, an optional maximum supply, four revocable authorities (mint, freeze, hook, metadata), the hook program and its flags, and the name, symbol and URI on chain. A holding is one owner’s token account for one mint, always the PDA of the pair, which anyone can create for anyone. Every holding keeps 64 bytes for the mint’s hook (section IV).
holding = PDA(["holding", mint, owner], token_program)
signer = PDA(["hook-authority", hook_program], token_program) // signs every call to that hook
create_mint create_holding transfer mint_to burn
approve revoke set_frozen close_holding write_hook_data
set_authority set_hook update_metadata
transfer(amount): authority (owner or delegate), source, destination, mint,
hook_program?, hook_signer?, then the hook's extra accounts
Transfers, mints and burns call the mint’s hook before and after the operation, for the callbacks its flags subscribe to (section III). Events are emitted by self-CPI and carry post-balances, so an indexer keeps exact holder balances without reading accounts. Hook data is not in events: an indexer that mirrors it reads the holding.
Coming in
III. The hook protocol#
A hook is an ordinary program whose instructions are named after the callbacks. The token program calls token hooks, the DEX calls pool hooks; both sign every call and both apply what the hook answers.
The calling program invokes a callback by CPI with an Anchor-style discriminator and Borsh arguments, signing with its ["hook-authority", hook_program] PDA, which is always the first account. The signer is one per hook program: a callback receives it as a signer and could pass it on in a CPI of its own, so a hook accepts only the signer made for it, and a signer another hook passes on vouches for nothing.
Never the user’s
Callbacks#
| Hook | Callbacks | Called by | Accounts, before the hook’s own |
|---|---|---|---|
| Token hook | before_transferafter_transferbefore_mintafter_mintbefore_burnafter_burn | The token program | hook_signer, mint, source, destination, authority |
| Pool hook | before_initializeafter_initializebefore_add_liquidityafter_add_liquiditybefore_remove_liquidityafter_remove_liquiditybefore_swapafter_swap | The DEX | hook_signer, pool, base_mint, quote_mint, actor |
For a mint or a burn, the mint stands in for the side the operation lacks. Pool callbacks also carry the caller’s opaque hook data (Uniswap v4’s hookData), up to 256 bytes.
Flags#
A mint subscribes its hook with Mint.hook_flags, a pool with Pool.hook_flags. A callback whose flag is off is never called, and an answer is read only when a flag allows one for that callback.
Token hook flags Mint.hook_flags · u16, 8 bits used
BEFORE_TRANSFER1calls before_transferAFTER_TRANSFER2calls after_transferBEFORE_MINT4calls before_mintAFTER_MINT8calls after_mintBEFORE_BURN16calls before_burnAFTER_BURN32calls after_burnTRANSFER_RETURNS_DELTA64before_transfer may answer up to three cutsWRITES_HOOK_DATA128before callbacks may answer hook data; write_hook_data is allowed
Pool hook flags Pool.hook_flags · u16, 11 bits used
BEFORE_INITIALIZE1calls before_initializeAFTER_INITIALIZE2calls after_initializeBEFORE_ADD_LIQUIDITY4calls before_add_liquidityAFTER_ADD_LIQUIDITY8calls after_add_liquidityBEFORE_REMOVE_LIQUIDITY16calls before_remove_liquidityAFTER_REMOVE_LIQUIDITY32calls after_remove_liquidityBEFORE_SWAP64calls before_swapAFTER_SWAP128calls after_swapBEFORE_SWAP_RETURNS_DELTA256before_swap may answer cuts and a burnAFTER_SWAP_RETURNS_DELTA512after_swap may answer cuts and a burnBEFORE_SWAP_OVERRIDES_FEE1,024before_swap may set this swap’s LP fee
A kit mint with holder rewards or the early-buyer lock
145
BEFORE_TRANSFER | BEFORE_BURN | WRITES_HOOK_DATA
A kit mint with only max wallet or the creator wallet lock
1
BEFORE_TRANSFER
A launch pool (the launchpad’s pool hook)
1,985
BEFORE_INITIALIZE | BEFORE_SWAP | AFTER_SWAP | BEFORE_SWAP_RETURNS_DELTA | AFTER_SWAP_RETURNS_DELTA | BEFORE_SWAP_OVERRIDES_FEE
Arguments#
Before or after the operation, a callback is told everything the calling program knows about it. Balances and hook data are those before the operation in the before phase and after it in the after phase.
pub struct TokenHookArgs {
pub op: TokenOp, // Transfer, Mint or Burn
pub phase: Phase, // Before or After
pub mint: Pubkey,
pub source: Pubkey, // the mint, for a mint
pub destination: Pubkey, // the mint, for a burn
pub source_owner: Pubkey,
pub destination_owner: Pubkey,
pub authority: Pubkey, // who signed
pub authority_is_delegate: bool,
pub amount: u64,
pub delta: u64, // what the cuts took (known after)
pub source_balance: u64,
pub destination_balance: u64,
pub decimals: u8,
pub supply: u64,
pub source_hook_data: [u8; 64], // the holdings' 64 bytes
pub destination_hook_data: [u8; 64],
}
pub struct PoolHookArgs {
pub op: PoolOp, // Initialize, AddLiquidity, RemoveLiquidity or Swap
pub phase: Phase,
pub pool: Pubkey,
pub base_mint: Pubkey,
pub quote_mint: Pubkey,
pub actor: Pubkey, // the trader or liquidity provider
pub recipient: Pubkey, // the owner of the holding that receives
pub direction: u8, // 0 a sell (base in), 1 a buy (quote in)
pub amount_in: u64,
pub amount_out: u64, // known after
pub base_reserve: u64,
pub quote_reserve: u64,
pub virtual_base: u64,
pub virtual_quote: u64,
pub lp_fee_bps: u16, // after a swap: the fee applied
pub protocol_fee_bps: u16,
pub swap_count: u64,
pub created_at: i64,
pub lp_amount: u64,
pub hook_data: Vec<u8>, // from the caller, up to 256 bytes
}
Answers#
A before_* callback, and a pool’s after_swap, may answer in its return data. Which fields it may fill depends on the callback and the flags; a field it may not fill must be empty, zero or None, or the whole answer is refused (UnsupportedHookReturn).
pub const MAX_DELTAS: usize = 3;
pub const HOOK_DATA_LEN: usize = 64;
pub struct Delta {
pub amount: u64, // above zero
pub account: u8, // index in the callback's accounts; must be an extra
}
pub struct HookReturn {
pub deltas: Vec<Delta>, // at most MAX_DELTAS
pub burn: u64, // pool swap callbacks only
pub lp_fee_bps: Option<u16>, // before_swap only
pub source_hook_data: Option<[u8; HOOK_DATA_LEN]>, // token callbacks only
pub destination_hook_data: Option<[u8; HOOK_DATA_LEN]>,
}
| Field | Callback | Needs |
|---|---|---|
deltas (up to 3) | token before_transfer | TRANSFER_RETURNS_DELTA |
deltas, burn | pool before_swap | BEFORE_SWAP_RETURNS_DELTA |
deltas, burn | pool after_swap | AFTER_SWAP_RETURNS_DELTA |
lp_fee_bps | pool before_swap | BEFORE_SWAP_OVERRIDES_FEE |
source_hook_data | token before_transfer, before_burn | WRITES_HOOK_DATA |
destination_hook_data | token before_transfer, before_mint | WRITES_HOOK_DATA |
After-phase token callbacks, and initialize and liquidity callbacks, answer nothing. The calling program, after decoding:
- reads an answer only when the flags allow one for that callback, and only when the return data is the hook program’s own;
- takes at most three cuts, each above zero, no account named twice, the sums in checked arithmetic (an overflow is
DeltaTooLarge); - on a transfer: the cuts add up to at most the amount, each to a writable holding of the mint that is not frozen and is neither the source nor the destination. The source loses the amount; the destination gains the amount less the cuts. A token callback never answers a burn;
- on a swap: the cuts and the burn stay below the side’s amount, each cut to a writable holding of that side’s mint, never a vault or one of the trader’s holdings; a burn needs the mint passed writable (
MintNotWritable), andmin_amount_outis checked against what the recipient’s holding actually gained.
Cuts to holders
Extra accounts#
A hook that needs accounts of its own publishes them at PDA(["bordrless-hook-accounts", mint-or-pool], hook_program): a list of literal keys, or PDAs whose seeds are literals or keys already in the callback’s list (the fixed prefix first, then the extras in order). Clients resolve the list and append the accounts; the calling programs pass them through untouched.
Funded first
A swap, in order#
The protocol fee is always taken in the pool’s quote token, on both sides; the LP fee stays on the input side. A pool hook’s answer is taken at two points, drawn on ink below: from the input before the curve, and from the output after it.
Each pool copies how Bordrless is paid from the DEX config when it is created and keeps it for life (fee_model). An ordinary pool, one anyone opens, pays a flat 1% of the quote. A launch pool, a curve the launchpad creates as its own hook (only the launchpad’s: a curve any other program opens as its hook pays the flat rate), pays no flat rate: Bordrless takes 25% of what the pool’s hooks cut on each swap, the creator fee, holder rewards and any cut a creator’s own token hook takes, measured by the DEX as what left the trade and reached someone, in the quote. A base-side cut is valued at the swap’s own price. Burns and the LP fee are not shared, and a launch whose rules collect nothing pays nothing: there is no floor.
A buy quote in, base out
- The
before_swapanswer, taken from the input: each cut a transfer from the trader’s input holding to the named holding, then the burn. - The rest reaches the quote vault. The LP fee and the protocol fee are charged on it, and the curve runs on what remains.
- The
after_swapanswer, taken from the curve’s output: each cut from the output vault (the pool signs), then the burn. - The rest goes to the recipient’s holding.
A sell base in, quote out
- The
before_swapanswer, taken from the input. On a launch pool: the burn. - The rest reaches the base vault. The LP fee is charged on it, and the curve gives the output in the quote token.
- The protocol fee is taken from that output, kept apart from the reserves.
- The
after_swapanswer, taken from what is left. On a launch pool: the creator and holder fees. - The rest goes to the recipient’s holding.
IV. 64 bytes in every holding#
Every holding carries 64 bytes that belong to its mint's hook. The token program writes them from the hook's answer, with the balances, in the same instruction: no extra account per holder, nothing to create or fund first.
Holding.hook_data: [u8; 64] sits after frozen. Only the mint’s hook program can change it, in two ways:
- by answering
source_hook_dataordestination_hook_datafrombefore_transfer,before_mint(the destination only) orbefore_burn(the source only), when the mint hasWRITES_HOOK_DATA; the token program writes it with the balances, after applying the operation; - by calling
write_hook_data(data), signed by its own["hook-authority"]PDA at the canonical bump, for a mint whose hook it is and which has the flag. It calls no hook. The kit’s claim uses it.
close_holding refuses a holding whose data is not all zero when the mint has a hook with WRITES_HOOK_DATA (HookDataNotEmpty): a hook’s record of what a holder is owed must not vanish with the account. Without a hook, or without the flag, nobody can ever clear the data, so the holding closes regardless.
The hook’s own
- snapshot
- bytes 0–15 · u128
- Holder rewards: acc_per_share at the holder’s last settle
- owed
- bytes 16–23 · u64
- Holder rewards: rewards earned and not claimed, in lamports
- early_locked
- bytes 24–31 · u64
- Early-buyer lock: tokens bought in the early window
- reserved
- bytes 32–63
- zero
Little-endian. All zero for a holding the kit has never written, and written back as all zero whenever nothing is left to keep, so the holding can close.
V. The kit#
bordrless_kit is a token hook with four fixed, bounded modules. Only the launchpad installs it, and each launch picks the modules its token has.
Who can install it#
KitConfig, at["kit", mint], is created only byinit, which must be signed byPDA(["kit-caller", mint], LAUNCH_ID). The kit knowsLAUNCH_IDas a constant; that PDA signs nothing else and holds nothing.- A kit launch’s mint is created with
hook_program = KIT_IDandhook_authority = None, so which program runs is fixed from the token’s first instruction. The kit’s own code can change while it has an upgrade authority (section XI). - Anyone can create a mint that names the kit as its hook. Without a KitConfig, which only a launch can create, every callback for that mint fails, so such a mint can never move.
Modules#
| Bit | Module | Parameters | What it does |
|---|---|---|---|
| 1 | Holder rewards | the reward vault and the accounting of section VI; the fee itself is set in the pool hook | A share of each trade, paid to holders in SOL; claim any time. |
| 2 | Max wallet | max_wallet_amount, fixed at init | No holder above the cap until graduation. Never blocks a sell into the launch pool. |
| 4 | Creator wallet lock | creator_unlock_at | The creator’s wallet can’t sell or send until then, whoever signs. |
| 8 | Early-buyer lock | early_window_end, early_unlock_at | Tokens bought from the pool in the window can’t be sold or sent until the unlock. |
The mint’s flags are BEFORE_TRANSFER, plus BEFORE_BURN | WRITES_HOOK_DATA when holder rewards or the early-buyer lock is on. A launch with none of these modules gets no hook at all; a burn alone runs in the pool hook.
Owners#
- Excluded: the pool and the launch. Their holdings are not settled, not capped and not counted in
eligible, and they cannot claim. Every other owner is a holder. - Refused destinations (
DestinationNotAllowed): the launch, the kit config, the default key, and the ids of the token, DEX, bridge, launch and kit programs. - With holder rewards on, no program can hold the token: a transfer to an owner that is not excluded must go to an address on the ed25519 curve. No other pool, vault, escrow or multisig can hold it, so every pool trade happens on the launch pool and pays its rules. Trades that never hold the token in a program (two wallets swapping with each other, an order book moving tokens as a delegate) are not prevented and pay no launch fees; max wallet and the locks still apply to them.
- With holder rewards off, anyone can open another pool for the token. Burn, the creator fee and the buy and sell rates apply only on the launch pool, and max wallet caps another pool’s vault like any wallet until graduation.
The callbacks#
- Where the token may go: a refused destination, or, with holder rewards, an owner that is neither excluded nor on the curve.
DestinationNotAllowed - Sync: what reached the reward vault since the last callback is divided among the eligible supply (holder rewards).
- Creator wallet lock: before the unlock, the creator’s wallet sends nothing, whoever signs.
CreatorLocked - Early-buyer lock: before the unlock, a holder cannot go below the tokens it bought in the window.
EarlyLocked - Settle the source at its balance before the transfer (holder rewards).
- Settle the destination at its balance before the transfer.
- Max wallet: until graduation, no holder above the cap.
MaxWalletExceeded - Early-buyer bookkeeping: a buy from the pool inside the window adds to the buyer’s locked tokens; after the unlock they are written as 0.
- The eligible count: from excluded to holder adds the amount, from holder to excluded takes it away.
- Answer the 64 bytes of each holder side that changed. No cuts, no burn: what leaves is what arrives.
before_burn runs the sync, the early-buyer check, the settle and the eligible count on the source alone, and answers the source’s bytes. The creator wallet lock does not apply to burns: a burn extracts nothing.
What follows
Instructions#
init- Called by the launch program inside create_launch, signed by the kit-caller PDA, the creator paying. Checks the hard bounds again, creates KitConfig and, with holder rewards, the reward vault; eligible starts at 0 because the whole supply sits in the launch reserve.
graduate- Called by the launch at graduation, signed by the kit-caller PDA. Sets graduated, which lifts max wallet, and refuses a second time.
claim- Signed by the holder. Pays what it is owed in bridged SOL (section VI). Refuses the pool and the launch.
share(amount)- Signed by anyone: bridged SOL that every holder receives in proportion to what they hold, released over an hour. At least 0.001 SOL, and refused while nobody is eligible.
Runtime#
- Callbacks make no CPIs and emit no events. While a callback runs, the token program is on the stack, so the kit could not call it for any mint.
- Heights: swap, then token, then kit is 3; a router on top makes 4. With no events in callbacks, a routed swap never reaches the limit of 5.
- Known addresses are compared with constants instead of derived on chain: the token program’s signer for the kit is
C2Y3B3hZTesJQqLYrZ7qoaZUoRmwYWh5Qh3MuFxruouE, the DEX’s signer for the launchpad6Ztfr97cUewdViXDXdZUsQq4pz7MYdygvK1WijALjZ5q.
A verified kit token#
The site and the indexer treat a token as a Bordrless launch with kit rules only when a Launch exists at ["launch", mint], the mint’s hook program is the kit with no hook authority and no mint authority, its flags match the config’s modules, and the KitConfig at ["kit", mint] names that launch and its pool. The indexer lists launches only from LaunchCreated, so look-alike mints never reach the board.
VI. Worked example: holder rewards#
A share of each trade, paid to holders in SOL, claimable any time. A transfer hook could only do this as a separate pre-approved charge, by guessing which transfers are trades, with an extra account per holder. The kit does it with the 64 bytes and one number per token.
A fee on every transfer is the one thing Token-2022 already does, with its transfer-fee extension, so it makes a poor example. Holder rewards are the kit’s real work. The launch pool’s hook takes the holder fee from each trade, in bridged SOL, and pays it into the reward vault, a holding of the token’s KitConfig. The kit divides whatever arrives among the eligible supply.
The accounting#
KitConfig keeps acc_per_share: the lamports earned so far per eligible base unit, scaled by 1012. Each holder’s 64 bytes keep a snapshot of it and what the holder is owed. Every callback first syncs, dividing what arrived since the last one, then settles each holder it touches at the balance it had before the operation.
pub fn sync(&mut self, vault_amount: u64, now: i64) -> Result<()> {
let released = self.release(now)?; // the share stream; paused while nobody is eligible
let total = vault_amount + self.total_claimed;
let fresh = total - self.seen + released; // what arrived since the last sync
self.seen = total;
if self.divides() { // eligible > 0 and eligible >= min_eligible
let pot = fresh + self.held;
self.held = 0;
if pot > 0 {
let scaled = u128::from(pot) * SCALE + self.rem; // SCALE = 10^12
let inc = scaled / u128::from(self.eligible);
self.rem = scaled - inc * u128::from(self.eligible); // the exact remainder, kept
self.acc_per_share += inc;
self.total_distributed += pot;
}
} else {
self.held += fresh; // waits until holders hold enough
}
Ok(())
}
pub fn settle(data: &mut HolderData, balance: u64, balance_after: u64, acc_per_share: u128) -> Result<()> {
let earned = u128::from(balance) * (acc_per_share - data.snapshot) / SCALE; // rounded down
data.owed += u64::try_from(earned)?;
data.snapshot = if balance_after == 0 { 0 } else { acc_per_share };
Ok(())
}
While holders hold less than min_eligible, a thousandth of the supply, nothing is divided: what arrives waits in held, and the pool hook takes no holder fee at all. Rewards accrue with every trade, not by the second, and they stay with the wallet that earned them.
Exact
Claims#
claim syncs, settles the holder at its balance and pays min(owed, reward vault) to the holder’s bridged-SOL holding. It writes bytes 0 to 23 through write_hook_data and keeps bytes 24 to 63 exactly as read, so a claim never unlocks an early buyer. A wallet that sold everything keeps what it earned: what it is owed stays in its holding until it claims, and the holding cannot close until then. The site claims for up to about four tokens in one transaction and unwraps the SOL.
Share with holders#
share(amount) lets anyone send bridged SOL that every holder receives in proportion to what they hold. It streams: a share joins the stream at its own rate, its amount over 3,600 seconds, and is out within the hour, so buying just before a share and selling after it gains nothing. While nobody is eligible the stream pauses, and resumes with the same time left.
Why the vault always covers it#
After every instruction the reward vault holds at least what every holder could claim, plus what is held, plus what is still streaming. Four facts carry it:
- eligible is the sum of the balances that will be settled: every balance change of the token passes through a kit callback, minting is impossible (no mint authority), and excluded owners are never settled and cannot claim;
- every balance change of a holder is settled first, at the balance before the change;
- rounding always favours the vault: the division floors with its remainder kept, owed floors, stream releases floor;
- shared lamports count as seen when they arrive and reach holders only as they are released.
The off-chain mirror#
What the site shows as claimable is a pure function of account state, read with one call for KitConfig, the reward vault and the holding:
released = what the stream releases by now // 0 while nobody is eligible
pending = vault.amount + total_claimed - seen + released
acc = acc_per_share
if eligible > 0 and eligible >= min_eligible and pending + held > 0:
acc = acc_per_share + ((pending + held) * SCALE + rem) / eligible
claimable = 0 for the pool and the launch,
else owed + balance * (acc - snapshot) / SCALE // floors
a claim pays min(claimable, vault.amount)
VII. Launch rules#
What a launch can switch on, each under one name everywhere. Rules are fixed at launch: the creator can't change these.
Sniper feepool hook · v1
The first 30 seconds pay up to 80% LP fee, falling to 0.3%; the pool keeps it.
A transfer hook?NoIt can’t set a pool’s fee
Creator feepool hook · v1
The creator gets up to 2% of each trade on the launch pool, in SOL.
A transfer hook?Only with workaroundsOnly as a separate charge from an account the trader approved in advance, and by guessing which transfers are trades
Trades pay, transfers don’tpool hook · v1
Fees apply to trades on the launch pool; sending tokens to a friend costs nothing.
A transfer hook?Only with workaroundsOnly by guessing which transfers are trades, as a separate pre-approved charge; the fee extension charges every transfer alike
Holder rewardspool hook (fee), kit (accounting) · v2
A share of each trade, paid to holders in SOL; claim any time.
A transfer hook?Only with workaroundsOnly as a separate pre-approved charge, by guessing which transfers are trades, with an extra account per holder; it can take no part of the trade itself
Share with holderskit · v2
Anyone can send SOL that every holder receives pro rata, released over an hour.
A transfer hook?Only with workaroundsOnly with an extra account per holder, created and funded before use
Burnpool hook · v2
A share of each buy and sell on the launch pool is destroyed.
A transfer hook?NoIt can’t change an amount or burn
Buy and sell ratespool hook · v2
Different rates for buys and sells (for example, only sells pay holders).
A transfer hook?Only with workaroundsOnly by guessing which transfers are buys and sells, and only for a separate charge
Early-buyer lockkit, state in each holding · v2
Tokens bought in the first seconds can’t be sold or sent until a set time.
A transfer hook?Only with workaroundsOnly with an extra account per holder and by guessing which transfers are buys
Max walletkit · v2
No wallet can hold more than N% until graduation. Never blocks a sell on the launch pool.
A transfer hook?YesIt can refuse a transfer
Creator wallet lockkit · v2
The creator’s wallet can’t sell, send or add liquidity until a set time.
A transfer hook?YesIt can refuse a transfer
| Rule | What it does | Runs in | Since | A transfer hook? |
|---|---|---|---|---|
| Sniper fee | The first 30 seconds pay up to 80% LP fee, falling to 0.3%; the pool keeps it. | pool hook | v1 | NoIt can’t set a pool’s fee |
| Creator fee | The creator gets up to 2% of each trade on the launch pool, in SOL. | pool hook | v1 | Only with workaroundsOnly as a separate charge from an account the trader approved in advance, and by guessing which transfers are trades |
| Trades pay, transfers don’t | Fees apply to trades on the launch pool; sending tokens to a friend costs nothing. | pool hook | v1 | Only with workaroundsOnly by guessing which transfers are trades, as a separate pre-approved charge; the fee extension charges every transfer alike |
| Holder rewards | A share of each trade, paid to holders in SOL; claim any time. | pool hook (fee), kit (accounting) | v2 | Only with workaroundsOnly as a separate pre-approved charge, by guessing which transfers are trades, with an extra account per holder; it can take no part of the trade itself |
| Share with holders | Anyone can send SOL that every holder receives pro rata, released over an hour. | kit | v2 | Only with workaroundsOnly with an extra account per holder, created and funded before use |
| Burn | A share of each buy and sell on the launch pool is destroyed. | pool hook | v2 | NoIt can’t change an amount or burn |
| Buy and sell rates | Different rates for buys and sells (for example, only sells pay holders). | pool hook | v2 | Only with workaroundsOnly by guessing which transfers are buys and sells, and only for a separate charge |
| Early-buyer lock | Tokens bought in the first seconds can’t be sold or sent until a set time. | kit, state in each holding | v2 | Only with workaroundsOnly with an extra account per holder and by guessing which transfers are buys |
| Max wallet | No wallet can hold more than N% until graduation. Never blocks a sell on the launch pool. | kit | v2 | YesIt can refuse a transfer |
| Creator wallet lock | The creator’s wallet can’t sell, send or add liquidity until a set time. | kit | v2 | YesIt can refuse a transfer |
Where a transfer hook can do one of these at all, it needs an account the trader approved in advance, an extra account per holder, or a guess about which transfers are trades. Here each is part of the trade.
The last two are safety rails, said honestly: a transfer hook could do these too; here they’re standard, bounded on chain and fixed at launch.
Status
Choices and presets#
The launch form offers these choices, under “Token rules (fixed at launch)”, and starts on Plain.
| Rule | Choices | Default |
|---|---|---|
| Holder rewards | off, 0.5%, 1%, 2%; buys and sells together, or apart under Custom | 1% both sides |
| Creator fee | 0, 0.5%, 1%, 2% | 0.5% with holder rewards on, else 1% |
| Burn | off, 0.25%, 0.5%, 1%; both sides, or apart under Custom | off |
| Max wallet | off, 1%, 2%, 3%, 5%, until graduation | 2% |
| Creator wallet lock | off, 7, 30, 90 days | 30 days |
| Early-buyer lock | off; first 30 s, 60 s or 5 min; locked until 15 min, 1 h or 24 h after launch | off |
| Preset | Rules |
|---|---|
| Plain | No rules; creator fee 1% |
| Diamond hands | Early-buyer lock (first 5 min, until 24 h after launch), holder rewards 1% both sides, max wallet 1%, creator fee 0.5%, creator wallet lock 90 days |
| Burn | Holder rewards 0.5% both sides, burn 0.5% both sides, creator fee 0.5% |
| Paid to hold | Holder rewards 2% on sells only, creator fee 0.5%, creator wallet lock 30 days |
| Half-Life | An exit fee that halves every 6 hours held: 20% to sell at once, 0% after 2 days, burned. Buys are free. |
| Custom | Mix the rules by hand; holder rewards and burn can differ on buys and sells. |
| Build your own | Write a hook with the SDK, make a launch config, paste its key. |
Half-Life#
Half-Life is Bordrless’s own token hook, and the clearest example of what a hook here can do that a Token-2022 transfer hook can’t. Sell or send a Half-Life token the moment you get it and 20% of what you move is burned. The fee halves every six hours you hold (10% at 6 h, 5% at 12 h, 1.25% at a day) and is gone after two days. Buys never pay it. Pick “Half-Life” on the launch page; it launches from Bordrless’s config ABz5Je9FznnotUQxxaj28vn18t1Wv9SsDzEfDxGLRJY.
- Every holding remembers when its tokens arrived, in the 64 bytes of hook data inside the holding. Tokens sent to another wallet keep their age, so moving them resets nothing; buying more averages the age by weight.
- The fee is taken from the tokens being moved, before they move, into the token’s furnace. Anyone can burn the furnace, so early sellers shrink the supply for everyone who stays.
- A transfer hook can’t take part of a transfer, can’t keep state inside a holding without an extra account per holder, and Token-2022’s own transfer fee is one rate for everyone.
- The launch prepares the hook for the mint, launches, and lights the furnace: three transactions in one approval. On sells, Bordrless’s share is 25% of the fee’s value in SOL, as for every launch rule. The program is open source and a verified build: programs/half_life.
Build your own#
A launch can run a token hook you wrote instead of the kit. The rules travel in a LaunchConfig: an account anyone makes once with the SDK (buildCreateConfig) and reuses, holding the rules, the creator fee, the hook program and its flags, and a label. Its key is what the launch page takes under “Build your own”: the page reads it, shows every rule and the hook with who can upgrade it, checks it against the bounds, and launches with it. The mint is created with that hook and no hook authority.
- Burn and the creator fee are pool-hook rules and may go with a custom hook; holder rewards, max wallet and the locks are the kit’s, so a config with a hook can’t have them (one token hook per mint).
- The hook must be prepared for the mint before the launch: its registry at
["bordrless-hook-accounts", mint]for the mint address the page shows (tax_hook.prepareis the worked example). The registry must not depend on who sends or receives. - Nobody vets the code. Such a token is “Custom hook, unverified” on every surface, with the program, its upgrade authority or “fixed”, and the one warning: written by the creator, not Bordrless; it can refuse transfers or take part of them. A hook that refuses sells is a honeypot the DEX stays sound through: the swap fails whole, nothing moves.
Bounds#
create_launch refuses anything outside the launch config’s bounds, and no config can raise a bound past its ceiling. The kit checks its own hard bounds again in init.
| Rule | Policy | Ceiling |
|---|---|---|
| Holder rewards, per side | up to 2% | 5% |
| Burn, per side | up to 1% | 5% |
| Creator fee, holder rewards and burn, per side | up to 3% together | 10% |
| Max wallet | 1% to 5% | below 100% |
| Creator wallet lock | up to 90 days | 365 days |
| Early-buyer window | up to 5 min | 1 h |
| Early-buyer unlock, after launch | up to 7 days | 30 days |
One fee per side#
The site shows one compounded figure per side, in the order the DEX charges them, and names who receives each part:
buy = 1 - (1 - (creator + holders)(1 + s)) (1 - lp) (1 - burn_buy)
sell = 1 - (1 - burn_sell) (1 - lp) (1 - (creator + holders)(1 + s))
lp is the current LP fee (the sniper fee while it is elevated, 0.3% after) and holders is 0 while nobody is eligible. Bordrless takes 25% of what a launch’s rules collect: a quarter of the creator fee and holder rewards (and of what a creator’s own hook takes), in SOL, on buys and on sells, on the curve and after graduation. A launch whose rules collect nothing pays Bordrless nothing; burns and the pool fee are not shared. Plain (creator 1%) is 1.5437% per side, printed to a tenth and rounded up: Buy 1.6% · Sell 1.6%; Diamond hands (creator 0.5%, holders 1%) Buy 2.2% · Sell 2.2%. Price impact is the curve’s movement only; fees are their own lines.
VIII. The launchpad hook#
The launchpad is itself a pool hook. It runs the sniper fee, the creator fee, the holder fee and the burn on every swap of a launch pool.
Every launch mints 1,000,000,000 tokens; 75% are sold on the curve, and it graduates at a fixed SOL raise. On each swap of its pool:
- Before every swap it sets the LP fee: 0.3%, or during the first 30 seconds a fee that starts at 80% and falls linearly to 0.3%. The pool keeps it.
- A buy: before the curve, the creator fee and, while holders hold enough, the holder fee, from the SOL in, as cuts to the launch’s SOL holding and to the holder vault; after the curve, the burn from the tokens out.
- A sell: before the curve, the burn from the tokens in; after it, the creator and holder fees from the SOL out. The DEX then holds its quarter of those two fees back from the delivery.
- The creator’s own first buy, into the creator’s wallet inside the sniper window, pays the normal LP fee instead of the sniper fee, once. A bot that buys first no longer takes that exemption from the creator.
index 5 launch writable
index 6 launch SOL holding writable creator fees are paid here
index 7 holder vault writable holder fees are paid here
index 8 kit config read for the holder-fee threshold
Graduation is a separate, permissionless instruction. It tops the pool up from the reserve so the price does not move when the virtual reserves are dropped, burns the rest, and the DEX mints the first LP to the launch, which has no instruction to spend it. For a launch with kit rules it then calls the kit’s graduate, which lifts max wallet. Solana forbids indirect reentrancy, which is also why graduation is its own instruction rather than part of after_swap.
IX. The bridge#
The way into the standard and back out: SOL, SPL tokens and Token-2022 tokens, one for one.
The bridge wraps SPL Token and Token-2022 mints and native SOL into mints of the standard, one for one, and unwraps them. The original sits in a vault owned by the wrapper PDA. A mint nobody has bridged yet is registered by the first wallet that bridges it. Bridged SOL is what every launch is quoted in; the site wraps and unwraps it inside a trade, so a trader only ever sees SOL.
X. Programs and who can upgrade them#
Five programs make the standard. Each one's address, and who can upgrade it, read from the chain.
The addresses are the same on localnet and devnet; mainnet gets its own keys at deploy time. An upgrade authority is read from each program’s ProgramData account; a dash means it has not been read yet, not that there is none.
Hook signers
6EsVn9…KWW1MV, the DEX signs the launchpad’s with FbMnAz…bh958 (section V).XI. Upgrade policy#
Who can change the code, and what happens to that power before mainnet. A launch's rules are fixed at launch: the creator can't change them. The programs can still be upgraded until they are audited, and this page says so.
- The token, DEX, launch, kit and bridge programs keep their upgrade authority, the deployer key, on localnet and devnet. Section X shows who holds each one.
- Before the first mainnet launch, each authority moves to a multisig behind a public timelock.
- The kit’s upgrade authority is revoked once the kit is audited.
- The DEX’s pause flag, which stops every swap, moves to the same multisig.
- Every program’s IDL is published on chain at deploy, so explorers decode its accounts, KitConfig included.
That is why the site says a launch’s rules are “fixed at launch: the creator can’t change these” and claims no more. Nothing a creator signs can change a launch’s rules: they are written into its KitConfig and its Launch at creation, and no instruction rewrites them. But while a program keeps an upgrade authority, its code can change.
Specification: docs/hooks-v2.md, revised 7 Oct 2026. The source, the hook interface crate and the tests are in the repository.