I. What a Bordrless token is#
For holders and traders first: what you hold, what a hook can do to it, and the rule that never changes.
A token of its own
A Bordrless token lives in the Bordrless token program, not in SPL or Token-2022. Your balance is a holding: one account per wallet for each token, which anyone can create for anyone.
Hooks that can do more
The token’s hook runs before and after every transfer, mint and burn; a pool’s hook runs on every swap on the Bordrless DEX. A Token-2022 transfer hook can only approve or block; a Bordrless hook answers, and the program applies the answer: cuts of the amount to accounts it names, a burn on a swap, that swap’s fee, and 64 bytes of its own state kept in every holding. The program runs the hooks itself, which is why they can do more.
The same safety rule
A hook never gets your signature. It answers, and the token program or the DEX checks the answer and applies it, never more than the amount being moved, and signs every call, so a hook knows who is calling.
Where they trade
Launched tokens trade on the Bordrless DEX and on this site. Wallets don’t show them; your Portfolio here does.
In and out, one for one
The bridge brings SOL, SPL tokens and Token-2022 tokens into the standard and back out, one for one. Bridged SOL is what every launch is quoted in.
The whole standard
II. Build on it: what the SDK gives#
One package, nine modules, every layout from the programs’ own IDLs.
@bordrless/sdk lives at packages/sdk. Its tests hold every instruction builder to the IDLs (the number of accounts, each one’s signer and writable flags, every fixed address, the discriminator) and every PDA to the values compiled into the programs; its v0 builder reproduces, to the byte, the transaction sizes the programs’ runtime test measured. With it you can launch a token, trade it, bridge SOL and SPL or Token-2022 tokens in and out, put a hook on a mint you create, and read everything the programs emit.
import {
token, swap, bridge, launch, kit, // instruction builders, one object per program
holdingAddress, poolAddress, launchAddress, // every PDA
decodeMint, decodeHolding, decodePool, // typed accounts from the IDL coders
eventsOf, typedEvent, // events from a transaction's inner instructions
fetchTokenHook, kitTokenHook, // a hook's accounts, resolved for one operation
buildV0Transaction, protocolLookupTable, // v0 transactions with the protocol table
explainFailure, // a failed transaction, in words
} from '@bordrless/sdk';
- addresses.tsProgram ids and every PDA the programs use. The seeds mirror the Rust constants; the tests derive every fixed address again and hold it to the values compiled into the programs.
- accounts.tsTyped views of the programs’ accounts, decoded with the IDL coders: mints, holdings, pools, launches, the kit’s config. Integers come back as bigint, times as numbers, byte arrays as Uint8Array.
- coders.tsBorsh coders built from the programs’ IDLs (idl/*.json, generated by anchor idl build). Instruction data, account data and events go through these, so the SDK never hand-writes a layout.
- events.tsThe programs’ events, read from a transaction’s inner instructions (a self-CPI) and decoded with the IDL coders, numbered in execution order: transfers with their cuts, swaps, launches, claims.
- errors.tsProgram errors in words a trader can act on, keyed by the program that failed: read from the logs, since custom codes overlap between programs and a failure inside a CPI surfaces on every program above it.
- hooks.tsHook accounts. Decodes and encodes a hook’s registry, resolves it against a callback’s prefix exactly as the Rust resolve does, and assembles what instructions take: the two hook slots of a token instruction and each hooked mint’s slice of a DEX instruction.
- instructions.tsInstruction builders for every program (token, swap, bridge, launch, kit, tax_hook), in the account order of each program’s Accounts struct, with the event authority appended as #[event_cpi] does.
- inspect.tsBuild your own: a LaunchConfig read and checked as create_launch checks it, a hook program and whether it is prepared for a mint, who can upgrade it, and buildCreateConfig, which makes the config and the key to paste.
- transactions.tsv0 transactions with the protocol lookup table: the 18 fixed addresses no top-level instruction invokes. create_launch with kit rules only fits with the table (1,042 bytes with it, 1,366 without).
III. Write your own hook#
A hook is an ordinary Solana program. The crate bordrless-hook is the whole interface: the callbacks, their arguments, the answer and the registry.
Callbacks#
A token hook can implement before_transfer, after_transfer, before_mint, after_mint, before_burn and after_burn; a pool hook before_initialize, after_initialize, before_add_liquidity, after_add_liquidity, before_remove_liquidity, after_remove_liquidity, before_swap and after_swap. Each is an instruction of your program named after the callback, invoked by CPI with Anchor’s discriminator for that name and Borsh arguments (TokenHookArgs: the operation, the phase, the mint, both holdings and their owners, who signed and whether as a delegate, the amount, both balances, the supply, both holdings’ hook data; PoolHookArgs: the pool, both mints, the trader and the recipient, the direction, amounts in and out, the reserves, the fees, and the caller’s opaque hook_data).
The first account
PDA(["hook-authority", your_program], caller), always the first account. The signer is one per hook program: your hook accepts only the one made for it (hook_signer in the crate gives it), so a signer another hook received and passes on vouches for nothing.The answer#
A before_* callback, and a pool’s after_swap, answers with a HookReturn in its return data (Ok(HookReturn { .. }) from an Anchor instruction). The calling program checks it (read_answer) and applies it: at most three cuts, each above zero, no account named twice, the sums in checked arithmetic, never more than the amount being moved, each cut to a writable holding your hook named among its extras.
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>, // up to MAX_DELTAS (3) cuts from the amount
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 (64 bytes)
pub destination_hook_data: Option<[u8; HOOK_DATA_LEN]>,
}
Flags#
A mint’s flags (Mint.hook_flags) say which token callbacks run and what they may answer; a pool’s (Pool.hook_flags) the same for pool callbacks. A field a callback may not fill refuses the whole answer (UnsupportedHookReturn).
pub mod token_flags {
pub const BEFORE_TRANSFER: u16 = 1 << 0; pub const AFTER_TRANSFER: u16 = 1 << 1;
pub const BEFORE_MINT: u16 = 1 << 2; pub const AFTER_MINT: u16 = 1 << 3;
pub const BEFORE_BURN: u16 = 1 << 4; pub const AFTER_BURN: u16 = 1 << 5;
pub const TRANSFER_RETURNS_DELTA: u16 = 1 << 6; // before_transfer may answer cuts
pub const WRITES_HOOK_DATA: u16 = 1 << 7; // before_* may answer the 64 bytes
}
pub mod pool_flags {
pub const BEFORE_INITIALIZE: u16 = 1 << 0; pub const AFTER_INITIALIZE: u16 = 1 << 1;
pub const BEFORE_ADD_LIQUIDITY: u16 = 1 << 2; pub const AFTER_ADD_LIQUIDITY: u16 = 1 << 3;
pub const BEFORE_REMOVE_LIQUIDITY: u16 = 1 << 4; pub const AFTER_REMOVE_LIQUIDITY: u16 = 1 << 5;
pub const BEFORE_SWAP: u16 = 1 << 6; pub const AFTER_SWAP: u16 = 1 << 7;
pub const BEFORE_SWAP_RETURNS_DELTA: u16 = 1 << 8; // cuts and a burn from the input
pub const AFTER_SWAP_RETURNS_DELTA: u16 = 1 << 9; // cuts and a burn from the output
pub const BEFORE_SWAP_OVERRIDES_FEE: u16 = 1 << 10; // the swap's LP fee
}
Extra accounts#
Every callback gets five fixed accounts first: the signer, the mint, source, destination and authority for a token hook; the signer, the pool, both mints and the actor for a pool hook. Anything else your hook needs goes in a registry it publishes at PDA(["bordrless-hook-accounts", mint-or-pool], your_program): a HookAccountList of fixed keys and PDAs whose seeds may be literals, accounts already in the callback’s list, or the owners of the two holdings. The SDK resolves it (fetchTokenHook, resolveHookAccounts) and the calling programs pass the accounts through untouched. write_registry in the crate creates the account.
pub enum Seed { Literal(Vec<u8>), Account(u8), SourceOwner, DestinationOwner }
pub enum AccountSource { Key(Pubkey), Pda { program: Pubkey, seeds: Vec<Seed> } }
pub struct ExtraAccount { pub writable: bool, pub source: AccountSource }
pub struct HookAccountList { pub version: u8, pub accounts: Vec<ExtraAccount> }
// tax_hook publishes two: its config (a PDA on the mint, account 1 of the prefix), then the collector.
let list = HookAccountList::new(vec![
ExtraAccount { writable: true, source: AccountSource::Pda { program: crate::ID, seeds: vec![Seed::Literal(b"tax".to_vec()), Seed::Account(1)] } },
ExtraAccount { writable: true, source: AccountSource::Key(collector_holding) },
]);
Naming the hook, and locking it#
A mint names its hook when it is created: hook_program and hook_flags are arguments of create_mint. While the mint keeps a hook authority, that authority can change the hook or its flags with set_hook; revoking the hook authority with set_authority locks it, so which program runs is fixed from then on. A kit launch is made that way: hook_program = KIT_ID and no hook authority from the first instruction.
IV. Put it on a mint, or launch it#
Create the mint through the SDK with your hook named, set it up, lock it; every transfer then carries the hook’s accounts.
import { Keypair } from '@solana/web3.js';
import { TOKEN_HOOK_FLAGS } from '@bordrless/shared';
import { TAX_HOOK_PROGRAM, fetchTokenHook, token } from '@bordrless/sdk';
const mint = Keypair.generate();
const flags = TOKEN_HOOK_FLAGS.BEFORE_TRANSFER | TOKEN_HOOK_FLAGS.TRANSFER_RETURNS_DELTA;
const create = token.createMint(creator, mint.publicKey, {
decimals: 6,
name: 'My token', symbol: 'MINE', uri: 'ipfs://…',
maxSupply: 1_000_000_000_000_000n,
mintAuthority: creator,
freezeAuthority: null,
hookProgram: TAX_HOOK_PROGRAM, // the mint names its hook…
hookFlags: flags, // …and the callbacks it subscribes to
hookAuthority: creator, // kept while the hook is set up
metadataAuthority: creator,
});
// Change the hook or its flags while the hook authority is held…
const change = token.setHook(creator, mint.publicKey, TAX_HOOK_PROGRAM, flags);
// …then lock it: with no hook authority, which program runs is fixed.
const lock = token.setAuthority(creator, mint.publicKey, 'hook', null);
// A transfer of a hooked mint carries the hook's accounts, resolved for that transfer.
const hook = await fetchTokenHook(connection, { mint: mint.publicKey, source, destination, authority });
const send = token.transfer(authority, source, destination, mint.publicKey, 1_000_000n, hook);
Every transfer, mint and burn of a hooked mint carries the hook program, the token program’s signer for it and the hook’s extras: fetchTokenHook reads the mint and the registry and resolves them for the operation, and the builders take the result. A pool for the token is created through the SDK too (swap.createPool), and the DEX calls the token’s hook on every swap of it; swap.swapWithHooks takes each side’s hook.
Transactions
buildV0Transaction(payer, instructions, blockhash, [protocolLookupTable(key)]): a v0 message that loads the 18 fixed addresses from the protocol table. explainFailure(logs) names the program that failed and its error, in words.Build your own: launch it on the launchpad#
A launch on the site can run your hook instead of the kit. The rules travel in a LaunchConfig, an account you make once and reuse; its key is what the launch page takes under “Build your own”. The launch creates the mint, so the hook must be prepared for the mint address the page shows before you launch: its registry at ["bordrless-hook-accounts", mint], plus whatever your hook keeps per mint. The site labels the token “Custom hook, unverified”: nobody vets the code.
import { buildCreateConfig, inspectConfig, taxHook, TAX_HOOK_FLAGS, TAX_HOOK_PROGRAM, NO_LAUNCH_RULES } from '@bordrless/sdk';
// 1. Prepare the hook for the mint the launch page shows (tax_hook's own instruction; write one like it).
const prepare = taxHook.prepare(me, mint, collector, 100, 0); // 1% of every transfer to collector
// 2. A config: burn and the creator fee go with a custom hook; holder rewards, max wallet and the locks do not.
const made = buildCreateConfig(me, {
rules: { ...NO_LAUNCH_RULES, burnBuyBps: 25, burnSellBps: 25 },
creatorFeeBps: 100,
customHook: TAX_HOOK_PROGRAM,
customHookFlags: TAX_HOOK_FLAGS,
label: 'taxed',
});
// send made.instruction signed by me and made.keypair; paste made.address into the launch page.
// 3. The same checks the page makes: the bounds now, the hook, its registry for the mint.
const seen = await inspectConfig(connection, made.address, mint); // problems: [], registryReady: true
Bordrless takes 25% of what a launch’s rules collect on each swap, the creator fee, holder rewards and any cut your hook takes, in SOL; a config can’t change that. The registry must not depend on who sends or receives: the launch passes one slice of accounts for every transfer, mint and burn of the token.
V. Two examples: tax_hook and the kit#
A small one and the full one, both in the repository.
tax_hook: a transfer fee with a wallet cap#
programs/tax_hook is a token hook with one callback. Its install records the fee and the cap, publishes the registry above, and sets the mint’s hook to itself (the mint’s hook authority signs); its prepare does the same for a mint that does not exist yet, so a launch can create the mint with the hook. Its before_transfer answers one cut to the collector, taken by the token program from what the destination receives, and refuses a transfer that would leave a wallet over the cap. The collector is exempt on both sides, and the fee is 0 until the collector’s holding exists, so a launch’s own deposit is free.
pub const FLAGS: u16 = token_flags::BEFORE_TRANSFER | token_flags::TRANSFER_RETURNS_DELTA;
pub const COLLECTOR_INDEX: u8 = TOKEN_PREFIX_ACCOUNTS as u8 + 1; // prefix of 5, the config, the collector
pub fn before_transfer(ctx: Context<BeforeTransfer>, args: TokenHookArgs) -> Result<HookReturn> {
let tax = &mut ctx.accounts.tax;
require_keys_eq!(args.mint, tax.mint, TaxError::WrongMint);
let exempt = args.source_owner == tax.collector_owner
|| args.destination_owner == tax.collector_owner;
let delta = if exempt {
0
} else {
fee_amount(args.amount, tax.fee_bps).ok_or(TaxError::Overflow)?.min(args.amount)
};
if tax.max_wallet_bps > 0 && tax.max_wallet_bps < 10_000
&& args.destination_owner != tax.collector_owner
{
let cap = u128::from(args.supply) * u128::from(tax.max_wallet_bps) / u128::from(BPS);
let after = u128::from(args.destination_balance) + u128::from(args.amount - delta);
require!(after <= cap, TaxError::WalletTooLarge);
}
// A delta must be above zero, so a free transfer answers none.
let deltas = if delta > 0 {
vec![Delta { amount: delta, account: COLLECTOR_INDEX }]
} else {
vec![]
};
Ok(HookReturn { deltas, ..HookReturn::default() })
}
The kit: the launch rules#
programs/bordrless_kit is the full worked example: a token hook with four modules (holder rewards, burn, max wallet, the locks) whose 64 bytes do the holder accounting, installed by the launchpad, which is itself a pool hook (programs/bordrless_launch: the sniper fee, the creator fee, the holder fee and the burn on every swap of a launch pool). The SDK’s kitTokenHook, kitHookSlice and launch.swap show how a client carries a hook with several extras. The docs walk through both (the kit, the launchpad hook).
VI. What is there, and what is not#
Said plainly, so a builder knows where they stand.
Where it stands
On npm as @bordrless/sdk and @bordrless/shared (0.1.0), from the SDK repository. The IDLs ship inside the package.
A hook of your own launches from a config made with the SDK (“Build your own” on the launch page), or goes on a mint created through the SDK directly. Nobody vets its code: the site says so wherever the token shows.
The programs run on localnet and devnet until they are deployed to mainnet, and keep an upgrade authority until they are audited; the docs show who holds it.
What is there, and tested:
packages/sdk/src/instructions.test.ts: every builder against the IDLs, the remaining-account rules and the registries.packages/sdk/src/transactions.test.ts: the v0 builder reproduces the sizes the programs’ runtime test measured, to the byte.packages/sdk/src/coders.test.ts: the account layouts and events through the IDL coders, and errors explained by the program that failed.crates/bordrless-hook/src/lib.rs: the answer’s checks, the flags and the registry, with their tests.
The SDK: bordrless-sdk, run its tests with pnpm test. The programs and the hook interface (crates/bordrless-hook): bordrless-programs. The standard in full is in the docs.