CAsoon

SDK

Build on the standard

The TypeScript SDK builds every instruction, decodes every account and event, and resolves a hook's accounts. The hook interface is open: anyone can write one.

Install#

shellnpm
npm install https://mizoapp.xyz/sdk.tgz

Version 0.5.0 of both, on npm; the IDLs ship inside the package. New to the standard? Start with why this standard.

For your coding agent#

prompt124 lines · for Claude Code, Cursor or any coding agent
You are helping me build on a token standard of its own on Solana: not Token-2022, not SPL.
Tokens live in its token program, which calls a mint's hook before and after every transfer, mint
and burn and applies what the hook answers; its DEX does the same with pool hooks on every swap.

INSTALL (published on npm; the IDLs ship inside the package):
  npm install https://mizoapp.xyz/sdk.tgz

IMPORTS (real export names):
  import { token, swap, bridge, launch, kit } from 'mizo-sdk';                       // instruction builders
  import { holdingAddress, poolAddress, launchAddress, registryAddress } from 'mizo-sdk'; // PDAs
  import { decodeMint, decodeHolding, decodePool, decodeLaunch, fetchAccount } from 'mizo-sdk'; // decoders
  import { eventsOf, typedEvent, explainFailure } from 'mizo-sdk';                     // events, errors
  import { fetchTokenHook, tokenHookOf, resolveHookAccounts, kitTokenHook } from 'mizo-sdk'; // hook accounts
  import { buildV0Transaction, protocolLookupTable } from 'mizo-sdk';                 // v0 transactions
  import { buildCreateConfig, inspectConfig, taxHook, TAX_HOOK_FLAGS } from 'mizo-sdk'; // build your own
  import { companion, companionAddress, companionCreatorAddress, decodeCompanion, companionVested,
           COMPANION_TEMPLATES, COMPANION_DEFAULTS, companionReady } from 'mizo-sdk';     // companions
  import { PROGRAM_IDS, TOKEN_HOOK_FLAGS, POOL_HOOK_FLAGS } from 'mizo-sdk/shared';

PROGRAM IDS (one set: the same on mainnet, devnet and localnet):
  token     2XoEWp8cF3kRXg74eVwPAyTFhVCAztn3V88komxAvr22
  DEX       GyzKSnnEu2uN5bBRecE4XYY2enbfR2D2MtxnbJPGy7hk
  bridge    CtLkuFVitoXHTa86Hfp8KmfSDfqJaMYFWr6EGmQVsKb7
  launchpad 1jcBymHxBjniZDhNPy51Vgm5Nz7pLUdxa9UBHc4TavC
  kit       14RJQXPdJfkehit6ezktjd3xujamf8nVSKw2shKamaEH
  tax_hook  8tjnVSreJGBRQFyDBf1SyyhBgLsdBxa2rHYh9sbxFyX7
  companion 6ZUM1gWBH9hBBNoJoaVAGwSftyZ6CUda6vUZTW9MsJuo
CLUSTER: the programs are live on mainnet; the RPC from SOLANA_RPC_URL (any Solana RPC, e.g. Helius).

HOOKS: a hook is an ordinary Solana program written against the mizo-hook crate (download: /downloads/mizo-hook.zip).
A token hook implements before_/after_ transfer, mint, burn; a pool hook before_/after_ initialize, add_liquidity,
remove_liquidity, swap. A before_* callback (and a pool's after_swap) returns, in its return data:
  pub struct HookReturn {
      pub deltas: Vec<Delta>,                   // up to 3 cuts { amount: u64, account: u8 } 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; 64]>,   // token callbacks only: the holding's 64 bytes of hook state
      pub destination_hook_data: Option<[u8; 64]>,
  }
A mint's hook_flags (TOKEN_HOOK_FLAGS) say which callbacks run and what they may answer; Pool.hook_flags likewise.
REGISTRY: extra accounts a hook needs are published at registryAddress(hook_program, mint-or-pool)
as a HookAccountList of fixed keys and PDAs; the SDK resolves it (fetchTokenHook) and appends them after the 5 fixed
accounts (hook signer, mint, source, destination, authority).
RULE: a hook never gets the user's signature. It answers, and the token program (or the DEX) checks and applies the
answer, never more than the amount being moved, signing every call with PDA(["hook-authority", hook_program]).

PUT A HOOK ON A MINT: token.createMint(payer, mint, { ..., hookProgram, hookFlags, hookAuthority }) names it;
token.setHook changes it while a hook authority is held; token.setAuthority(authority, mint, 'hook', null) locks it.
Every transfer of a hooked mint takes the hook's accounts: token.transfer(auth, src, dst, mint, amount, hook).

LAUNCH IT ON THE LAUNCHPAD (build your own): the rules travel in a LaunchConfig account made once with
buildCreateConfig(creator, { rules, creatorFeeBps, customHook, customHookFlags, label }) from 'mizo-sdk'
(send its instruction signed by the creator and its keypair; its address is pasted into the launch page under
"Custom config"). The launch creates the mint, so prepare the hook for the mint address the page shows first:
its registry at registryAddress(hook, mint) (taxHook.prepare is the worked example). A config
with a hook may have burn and a creator fee, not holder rewards, max wallet or the locks (one token hook per
mint). inspectConfig(connection, config, mint) runs the page's checks. The protocol takes 25% of what a launch's
rules collect on each swap (creator fee, holder rewards, your hook's cuts), in SOL; a config can't change that.
The site labels such a token "Custom hook, unverified".

UPGRADE AUTHORITY (required to launch from a config): the hook program MUST be immutable, or upgradeable only by
the protocol (CS1NRyXNCPxEUP4CRoa26cHQSeSJCxXh5SPijwFhDW6W or 5xsibKwtiN6ruxsYrEyWVpV3KcwuzSPbQd1n28a7spEd). The launch program's create_config refuses any other hook
(HookUpgradeable). Right after deploying your own hook, make it immutable (signed by its current upgrade authority):
  solana program set-upgrade-authority <PROGRAM_ID> --final
A config made before that rule whose hook someone else can upgrade can't launch either: the launch page says
"This config’s hook can be upgraded by someone outside the protocol, so it can’t launch here.". Check it
before making the config: `solana program show <PROGRAM_ID>` shows the Authority (inspectTokenHook and
inspectConfig say so too). Do not keep the
authority yourself.

COMPANIONS (live on mainnet): a launch whose creator is a program. The companion creates the launch and signs as
its creator, companionCreatorAddress(mint) = PDA(["creator", mint], companion), so every creator fee lands with it
and only its code spends it, by a split fixed at create. Its account: companionAddress(mint) = PDA(["companion", mint]).
THE SPLIT of every creator fee claim: { buybackBps, holdersBps, beneficiaryBps }, whole basis points summing to 10,000,
any mix (bought back and burned / streamed to holders through the kit / paid to the launcher in SOL). The launch page
calls it "where the creator fee goes": You (no companion), Buy back & burn, Holders, or a Split.
TEMPLATES (COMPANION_TEMPLATES; preset splits, accepted by name too):
  buysItself       buyback+burn 100% / holders 0% / launcher 0%; no dev, no first buy
  rugProofDev      buyback+burn 0% / holders 50% / launcher 50%; first buy vests to the launcher; needs holder rewards on
  buybackAndReward buyback+burn 50% / holders 50% / launcher 0%; first buy vests to the launcher; needs holder rewards on
LAUNCH WITH ONE: with the SDK builders below (WITH THE BUILDERS): set up the companion, launch, first buy,
each signed by the wallet and the mint keypair.
RULES: the shares sum to 10,000; a creator fee above 0; no creator
wallet lock (creatorLockDays 0: the companion vests the first buy instead); holder rewards on when holdersBps > 0 (and
then the launching wallet must be a plain wallet); no launch config and no custom hook (inline rules only: the
companion program refuses a custom hook, CustomHookUnsupported). The first buy is capped by max wallet; a split that buys back everything has no dev, so send no first buy. The
beneficiary is the launching wallet.
KEEP THE MINT KEYPAIR (write it to disk before signing) until the launch transaction confirms. If "Set up the
companion" landed and the launch didn't, only that key can launch it or take its SOL back: companion.refund(sender, mint, beneficiary), the mint
signing, takes its SOL back; then launch with a fresh mint.
WITH THE BUILDERS: companion.create(payer, beneficiary, mint, { split (any, or COMPANION_TEMPLATES.<template>),
...COMPANION_DEFAULTS, fund }) signed by payer and mint (fund = the LaunchConfig's launchFeeLamports + 50_000_000
rent + 890_880); then companion.launch(launcher, mint, launch.createLaunch(companionCreatorAddress(mint), mint,
config.treasury, config.quoteMint, config.lpFeeBps, args), args), signed by the launcher and the mint, as a v0
transaction with a lookup table holding PROTOCOL_LOOKUP_TABLE in order (the protocol keeps one on mainnet:
4vxVcYLdkqT1rMfGHjAu4kMa9XSEhUdQU5ThVfU5grGQ; companionReady(table) must be true; it
doesn't fit without); then optionally companion.devBuy(beneficiary, launchKeys(...), lamports, minOut) within 10 minutes.
CRANK ANY COMPANION (permissionless; each step checks on chain that it is due and pays its sender bountyBps of what
it moves, 0.5% by default (COMPANION_DEFAULTS), at most 1%): companion.claimFees(cranker, mint) (creator fees in,
split), companion.buyback(cranker, keys, rewards) (buys on the pool and burns; at most maxBuyback and 1% of the
pool's SOL, buybackInterval apart, waits while the price is over 3% above its reference), companion.share(cranker,
mint) (holders' part into the kit), companion.withdraw(sender, mint, beneficiary) (launcher's part, in SOL),
companion.release(cranker, keys, rewards, beneficiary) (vested first buy). keys = launchKeysOf(decodeLaunch(...)),
rewards = rewardsOn(launchRulesInputOf(launch.rules)). One step a transaction, claimFees first.
READ ONE: decodeCompanion(data) gives split, pendingBuyback, pendingHolders, pendingBeneficiary, lastBuybackAt,
burnedTotal, sharedTotal, paidBeneficiaryTotal, devTokens, devReleased; companionVested(c, now) - c.devReleased is
what release sends now.
With the builders any split summing to 10,000 bps works (the token page then
names no template). A companion program of your own isn't possible yet: Studio can't write one.

STATUS: the programs are live on mainnet and keep an upgrade authority until audited; the SDK is on npm (0.5.0).

What’s in it#

tsmizo-sdk · one import for everything
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
  companion, decodeCompanion,                // companions: launch one, crank one, read one
} from 'mizo-sdk';
The SDK's modules
ModuleGives
instructions.tsBuilders for every program: token, swap, bridge, launch, kit, tax_hook, in each program’s account order.
addresses.tsProgram ids and every PDA; the tests hold each fixed one to the value compiled into the programs.
accounts.tsTyped accounts: mints, holdings, pools, launches, the kit’s config. Integers as bigint.
hooks.tsA hook’s registry: decode, encode, and resolve it for one operation, as the Rust does.
events.tsEvents from a transaction’s inner instructions, in execution order.
errors.tsProgram errors in words, keyed by the program that failed.
transactions.tsv0 transactions with the protocol lookup table: create_launch with kit rules only fits with it (1,042 bytes, 1,366 without).
inspect.tsA LaunchConfig read and checked as create_launch checks it, and buildCreateConfig to make one.
companion.tsCompanions: create, launch, the dev buy, every permissionless step, the refund, and the account decoded.
coders.tsBorsh coders from the programs’ IDLs: no layout is written by hand.

Status#

Package
mizo-sdk 0.5.0, on npm.
Programs
Live on mainnet, one set of ids on every cluster; upgradeable until audited (who holds each key).
Your hook
Goes on a mint you create, or launches from a config once nobody outside the protocol can upgrade it (deploying it). Its code isn’t reviewed: the site says so wherever the token shows.
Companions
Live on mainnet: launch with one, or crank any (companions).
Tested
Every builder against the IDLs, v0 sizes to the byte, layouts, events and errors through the coders, and the hook crate’s checks.

SDK

Write a hook

A hook is an ordinary Solana program. The hook crate is the whole interface: the callbacks, their arguments, the answer and the registry.

Callbacks#

Implement the ones you need, each as an instruction of your program named after it. The caller invokes it by CPI with Anchor’s discriminator for that name and Borsh arguments (TokenHookArgs, PoolHookArgs).

Token hook
before_transferafter_transferbefore_mintafter_mintbefore_burnafter_burn
Pool hook
before_initializeafter_initializebefore_add_liquidityafter_add_liquiditybefore_remove_liquidityafter_remove_liquiditybefore_swapafter_swap

The first account is the caller’s signer, PDA(["hook-authority", your_program], caller). Accept only that one (hook_signer in the crate gives it): a signer another hook received and passes on vouches for nothing.

The answer#

A before_* callback, and a pool’s after_swap, returns a HookReturn (Ok(HookReturn { .. }) from an Anchor instruction). The caller checks it (read_answer) and applies it: at most three cuts, never more than the amount, each to a writable holding your hook named among its extras.

rustmizo-hook/src/lib.rs · what a callback answers
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 say which token callbacks run and what they may answer; a pool’s, the same for pool callbacks. A field a callback may not fill refuses the whole answer (UnsupportedHookReturn).

rust
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
}
mizo-hook/src/lib.rs

Extra accounts#

Five fixed accounts come first. Anything else your hook needs goes in a registry at registryAddress(your_program, mint-or-pool): fixed keys, and PDAs whose seeds may be literals, accounts already in the list, or the owners of the two holdings. write_registry creates it; the SDK resolves it (fetchTokenHook, resolveHookAccounts).

rustmizo-hook/src/lib.rs · the registry
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 and locking it#

A mint names its hook at creation (hook_program, hook_flags in create_mint). While it keeps a hook authority, set_hook changes either; revoking that authority with set_authority fixes which program runs, as every kit launch is made.

Deploying it#

Deploy it like any Solana program. To launch it from a config, make it immutable: the launch program refuses a config naming a hook that anyone can still upgrade.

shellAfter deploying, signed by the hook's current upgrade authority
solana program set-upgrade-authority <PROGRAM_ID> --final

Example: tax_hook#

programs/tax_hook is a token hook with one callback. install records the fee and the cap, publishes the registry and sets the mint’s hook to itself; prepare does the same for a mint that does not exist yet, so a launch can create it with the hook. before_transfer answers one cut to the collector and refuses a transfer that would leave a wallet over the cap. The collector is exempt, and the fee is 0 until its holding exists.

rustprograms/tax_hook/src/lib.rs · before_transfer, abridged
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() })
}

Example: the kit#

programs/kit is the full example: a token hook whose 64 bytes do the holder accounting, installed by the launchpad, itself a pool hook (programs/launch). 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.

SDK

Put it on a mint

Name your hook on a mint you create, or launch it on the launchpad from a config.

On a mint you create#

tstoken.createMint, token.setHook, token.setAuthority, token.transfer
import { Keypair } from '@solana/web3.js';
import { TOKEN_HOOK_FLAGS } from 'mizo-sdk/shared';
import { TAX_HOOK_PROGRAM, fetchTokenHook, token } from 'mizo-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, its signer and the extras: fetchTokenHook resolves them and the builders take the result. A pool for the token is swap.createPool; swap.swapWithHooks takes each side’s hook.

Transactions#

buildV0Transaction(payer, instructions, blockhash, [protocolLookupTable(key)]) builds a v0 message that loads the fixed addresses from the protocol table (its first 18 never change; later ones are only appended). explainFailure(logs) names the program that failed and its error, in words.

On the launchpad#

The rules travel in a LaunchConfig, made once and reused; paste its key under “Custom config” on the launch page. The hook must be immutable, or upgradeable only by the protocol (make it immutable after deploying); the launch program refuses the config otherwise. The launch creates the mint, so prepare your hook for the mint address the page shows first. Its code isn’t reviewed: the site labels the token “Custom hook, unverified”.

tsbuildCreateConfig, inspectConfig, taxHook.prepare
import { buildCreateConfig, inspectConfig, taxHook, TAX_HOOK_FLAGS, TAX_HOOK_PROGRAM, NO_LAUNCH_RULES } from 'mizo-sdk';

// 0. Your own hook: after deploying it, make it immutable (tax_hook is the protocol's, so it may stay upgradeable):
//    solana program set-upgrade-authority <PROGRAM_ID> --final

// 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 and who can upgrade it, its registry for the mint.
const seen = await inspectConfig(connection, made.address, mint);   // problems: [], hookAccepted: true, registryReady: true

The protocol 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.

SDK

Launch with a companion

A launch whose creator is a program: its creator fees are bought back and burned, shared with holders or paid to the launcher by code. Launch with one through the SDK builders; anyone can crank one.

What a companion is#

The companion creates the launch and signs as its creator, from PDA(["creator", mint]) under the companion program, so every creator fee lands with it and only its code spends it, by a split fixed at create. Every step after that is permissionless and pays its sender a bounty: if the protocol stopped cranking, any holder could. The mechanism in full: Companions.

The split, and the templates#

A companion splits every creator fee claim three ways, in whole basis points summing to 10,000: buybackBps (bought back on the token’s pool and burned), holdersBps (streamed to holders through the kit) and beneficiaryBps (paid to the launcher in SOL). Any mix works; the launch page calls it “where the creator fee goes”: You (no companion), Buy back & burn, Holders, or a Split. The templates are three preset splits, accepted by name too:

Companion templates
TemplateEach creator feeFirst buyHolder rewards
buysItself100% bought back and burnedNone: no dev at allOptional
rugProofDev50% to holders, 50% to the launcherHeld, vesting to the launcherRequired
buybackAndReward50% bought back and burned, 50% to holdersHeld, vesting to the launcherRequired

The splits are COMPANION_TEMPLATES in the SDK (COMPANION_SPLITS in shared). The SDK's COMPANION_DEFAULTS: a 0.5% bounty, at most 1 SOL a buyback, at least 60 s apart, and a vest of vestDays (30 days by default).

The rules#

No creator lock
creatorLockDays is 0: the companion vests the first buy instead.
The split
Three shares summing to 10,000 bps, each a whole number, and a creator fee above 0 for the companion to run.
Holder rewards
On whenever holdersBps is above 0 (rugProofDev, buybackAndReward): the holders’ share goes through the kit.
No custom hook
Inline rules only (DIY or a kit preset): no launch config, listed or not, and no custom hook: the companion program refuses one (CustomHookUnsupported), though the transaction would fit.
First buy
Within what max wallet lets one wallet hold at the opening (devBuyMaxLamports; 400 max_wallet). A split that buys back everything (buysItself) has no dev: send none.
Beneficiary
The launching wallet: paid its part in SOL and the first buy as it vests. With holder rewards on it must be a wallet, not a program.

Keep the mint key#

Keep the mint keypair from before signing until the launch confirms. Once “Set up the companion” lands, only that key can launch it or take its SOL back. Take the SOL back with companion.refund, then launch with a fresh mint.

Launch with one: the builders#

What to build, by hand: create (signed by the payer and the mint), then launch wrapping createLaunch with companionCreatorAddress(mint) as the creator (the launcher and the mint sign), then optionally devBuy. Set virtualQuote from the SOL price, within the config’s minVirtualQuote and maxVirtualQuote. The split may be any that sums to 10,000 bps.

tscompanion.create, companion.launch, companion.devBuy
import { Keypair } from '@solana/web3.js';
import { NO_RULES } from 'mizo-sdk/shared';
import {
  COMPANION_DEFAULTS, COMPANION_TEMPLATES, LAUNCH_CONFIG, buildV0Transaction, companion, companionCreatorAddress,
  companionReady, decodeLaunchConfig, fetchAccount, launch, launchKeys, launchRulesFromInput,
} from 'mizo-sdk';

const mint = Keypair.generate();                                  // keep it until the launch confirms
const config = (await fetchAccount(connection, LAUNCH_CONFIG, decodeLaunchConfig))!;
const rules = launchRulesFromInput({ ...NO_RULES, holderFeeBuyBps: 100, holderFeeSellBps: 100 });

// 1. create: the companion and its split, fixed for good. The mint signs: nobody else can make it.
const setUp = companion.create(me.publicKey, me.publicKey, mint.publicKey, {   // payer, beneficiary, mint
  split: COMPANION_TEMPLATES.buybackAndReward,
  ...COMPANION_DEFAULTS,                                          // 0.5% bounty, ≤ 1 SOL a buyback, 60 s apart, 30-day vest
  fund: config.launchFeeLamports + 50_000_000n + 890_880n,        // the launch's fee and rent, the creator address's own rent
});

// 2. launch: create_launch with the companion's creator address as the creator, sent through the companion.
const args = { name: 'Back', symbol: 'BACK', uri: 'ipfs://…', creatorFeeBps: 100, virtualQuote: config.minVirtualQuote, rules };
const create = launch.createLaunch(companionCreatorAddress(mint.publicKey), mint.publicKey, config.treasury, config.quoteMint, config.lpFeeBps, args);
const launchIx = companion.launch(me.publicKey, mint.publicKey, create, args);

// It fits only as v0 with a table holding PROTOCOL_LOOKUP_TABLE in order: the protocol keeps one on mainnet at
// 4vxVcYLdkqT1rMfGHjAu4kMa9XSEhUdQU5ThVfU5grGQ (or make your own with createProtocolLookupTable).
const table = (await connection.getAddressLookupTable(TABLE)).value!;
if (!companionReady(table)) throw new Error('the table lacks the companion addresses');
const tx = buildV0Transaction(me.publicKey, [launchIx], blockhash, [table]);
tx.sign([me, mint]);                                              // 1,071 bytes with kit rules

// 3. dev_buy (optional; its own transaction, within 10 minutes): the companion holds it, vesting to the beneficiary.
const keys = launchKeys(mint.publicKey, config.quoteMint, config.lpFeeBps, rules);
const firstBuy = companion.devBuy(me.publicKey, keys, 500_000_000n, minOut);

Crank one#

Anyone may send every step, for any companion, and earns its bounty. Each checks on chain that it is due and fails on its own otherwise. Each fits in one transaction with or without the protocol table (1,214 bytes at most, with a compute-unit limit).

claimFees
The launch’s creator fees in, the bounty paid, the rest split into the pending buyback, holders’ and launcher’s parts. The protocol’s crank waits for 0.02 SOL so the bounty covers the fee.
buyback
Buys on the token’s own pool and burns it: at most maxBuyback and 1% of the pool’s SOL, buybackInterval apart, not in the launch’s first minute; it waits while the price runs over 3% above its reference.
share
The holders’ part into the kit’s reward pool, once holders hold enough for the kit to take it.
withdraw
The launcher’s part, paid in SOL to the beneficiary.
release
The vested part of the first buy, to the beneficiary, once the early-buyer lock has ended.
tscompanion.claimFees, buyback, share, withdraw, release
import type { TransactionInstruction } from '@solana/web3.js';
import { rewardsOn } from 'mizo-sdk/shared';
import {
  BRIDGED_SOL_MINT, companion, companionAddress, companionVested, decodeCompanion, decodeHolding, decodeLaunch,
  fetchAccount, holdingAddress, launchAddress, launchKeysOf, launchRulesInputOf,
} from 'mizo-sdk';

const c = (await fetchAccount(connection, companionAddress(mint), decodeCompanion))!;
const l = (await fetchAccount(connection, launchAddress(mint), decodeLaunch))!;
const keys = launchKeysOf(l);
const rewards = rewardsOn(launchRulesInputOf(l.rules));
const fees = (await fetchAccount(connection, holdingAddress(BRIDGED_SOL_MINT, launchAddress(mint)), decodeHolding))?.amount ?? 0n;
const now = Math.floor(Date.now() / 1000);

// What is due; each checks again on chain and pays its sender c.bountyBps of what it moves.
const steps: TransactionInstruction[] = [];
if (fees > 0n) steps.push(companion.claimFees(me.publicKey, mint));
if (c.pendingBuyback > 0n && now >= c.lastBuybackAt + c.buybackInterval) steps.push(companion.buyback(me.publicKey, keys, rewards));
if (c.pendingHolders > 0n) steps.push(companion.share(me.publicKey, mint));
if (c.pendingBeneficiary > 0n) steps.push(companion.withdraw(me.publicKey, mint, c.beneficiary));
if (companionVested(c, now) > c.devReleased) steps.push(companion.release(me.publicKey, keys, rewards, c.beneficiary));
// One step a transaction, in this order (a claim fills what the others spend), each with ~600,000 compute units.

Read one#

tsdecodeCompanion, companionVested
import { companionAddress, companionVested, decodeCompanion } from 'mizo-sdk';

const c = decodeCompanion((await connection.getAccountInfo(companionAddress(mint)))!.data);
c.split;                                          // { buybackBps, holdersBps, beneficiaryBps }
c.claimedTotal; c.burnedTotal; c.sharedTotal;     // lamports claimed, tokens burned, lamports shared
c.paidBeneficiaryTotal; c.bountiesTotal;
companionVested(c, now) - c.devReleased;          // what release would send now