Launch with a companion
A launch whose creator is a program, the companion: it pays the launcher their creator fee and spends the companion fee, bought back and burned, shared with holders, traded by Snowball, drawn by the Jackpot, or spent by a script you wrote. Launch with one through the API or the builders; anyone can crank one.
What a companion is#
The companion creates the launch and signs as its creator, from the companion address, PDA(["creator", mint]) under the companion program, so the launch’s whole 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 Bordrless stopped cranking, any holder could. The mechanism in full: Companions.
It is the second of the two layers a builder works in. A hook runs inside the trade and writes into the holdings; a companion never does: Bordrless’s companion program holds the pot and either runs one of its own actions (the split, Snowball, the Jackpot) or calls a script you wrote with the launch’s accounts, which reads what it likes and answers, and the program executes the answer inside hard bounds (custom companions). That program is never upgraded for a builder’s program, only for a new kind of action.
The split, and the templates#
A companion splits every 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); a script or a game adds the pot’s part. Any mix works. On chain the launch has one creatorFeeBps, the whole fee; the launch page shows it as two rows, the Creator fee (the launcher’s part, beneficiaryBps) and the Companion fee (the rest, which the companion spends), together at most 5% of a trade (each row 0, 0.5%, 1% or 2% (the Companion fee from 0.5%); a Workshop config’s fee is any quarter percent up to 5%). The templates are three preset splits, accepted by name too:
| Template | Each fee claim | First buy | Holder rewards |
|---|---|---|---|
buysItself | 100% bought back and burned | None: no dev at all | Optional |
rugProofDev | 50% to holders, 50% to the launcher | Held, vesting to the launcher | Required |
buybackAndReward | 50% bought back and burned, 50% to holders | Held, vesting to the launcher | Required |
The splits are COMPANION_TEMPLATES in the SDK (COMPANION_SPLITS in shared). The API adds COMPANION_DEFAULTS: a 0.5% bounty, at most 1 SOL a buyback, at least 60 s apart, and a vest of vestDays (the site offers 7, 30, 90; the API takes 0 to 365, 30 by default).
Launch with one: the API#
The launch page’s own path: POST /api/launch/prepare with companion: { split: { buybackBps, holdersBps, beneficiaryBps }, vestDays? } (or { template, vestDays? }). The answer has one more transaction first, “Set up the companion”, signed by the mint too.
import { Keypair, Transaction, VersionedTransaction } from '@solana/web3.js';
import { NO_RULES, type LaunchPrepareRequest, type LaunchPrepareResponse, type Submission, type UploadResponse } from '@bordrless/shared';
const API = 'https://bordrless.app/api';
const post = async <T,>(path: string, body: unknown): Promise<T> => {
const res = await fetch(`${API}/${path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) throw new Error(await res.text()); // a 400 says why, with a code: companion_rules, max_wallet…
return (await res.json()) as T;
};
const mint = Keypair.generate(); // save it to disk now: see "Keep the mint key" below
// 1. The image and details, pinned to IPFS.
const form = new FormData();
form.append('image', new Blob([png], { type: 'image/png' }), 'logo.png');
for (const [k, v] of Object.entries({ creator: wallet.publicKey.toBase58(), mint: mint.publicKey.toBase58(), name: 'Back', symbol: 'BACK', description: 'Buys itself back.' })) form.append(k, v);
const { uploadId } = (await (await fetch(`${API}/upload`, { method: 'POST', body: form })).json()) as UploadResponse;
// 2. The launch, with a companion: half bought back and burned, half to holders (so holder rewards on).
const request: LaunchPrepareRequest & { mint: string } = {
creator: wallet.publicKey.toBase58(),
mint: mint.publicKey.toBase58(),
uploadId,
creatorFeeBps: 100, // the whole fee: 0, 50, 100 or 200, or with a companion the two rows' sum; this split makes it all companion fee
devBuyLamports: '500000000', // held by the companion, vesting to you
rules: { ...NO_RULES, holderFeeBuyBps: 100, holderFeeSellBps: 100 },
companion: { split: { buybackBps: 5_000, holdersBps: 5_000, beneficiaryBps: 0 }, vestDays: 30 }, // or { template: 'buybackAndReward' }
};
const prepared = await post<LaunchPrepareResponse>('launch/prepare', request);
// prepared.transactions: "Set up the companion", "Launch BACK", "Buy BACK", in stage order.
// 3. Sign each with the wallet, then the mint where extraSigners names it; send them and follow.
const transactions = prepared.transactions.map((p) => {
const signers = p.extraSigners.includes('mint') ? [wallet, mint] : [wallet];
const bytes = Buffer.from(p.transaction, 'base64');
const tx = p.version === 'legacy' ? Transaction.from(bytes) : VersionedTransaction.deserialize(bytes);
if (tx instanceof Transaction) tx.partialSign(...signers);
else tx.sign(signers);
return { transaction: Buffer.from(tx.serialize()).toString('base64'), stage: p.stage, label: p.label };
});
let submission = await post<Submission>('tx/submit', { transactions, intentId: prepared.intentId });
while (submission.state === 'pending') {
await sleep(1_000);
submission = (await (await fetch(`${API}/tx/submission/${submission.id}`)).json()) as Submission;
}
// 'confirmed': launched. Anything else: keep the mint key.
The rules#
- No creator lock
creatorLockDaysis 0: the companion vests the first buy instead.- The split
- Three shares summing to 10,000 bps, each a whole number, and a
creatorFeeBpsabove 0 for the companion to run. - Holder rewards
- On whenever
holdersBpsis above 0 (rugProofDev,buybackAndReward): the holders’ share goes through the kit. - A custom hook
- Since the companion program’s 2026-10-12 upgrade a companion of any kind may launch a token with a custom hook, from a plain config (no author share:
AuthorShareUnsupported; a listed config is refused) and with no holders’ share (one token hook per mint: the kit carries holder rewards). Every step that moves the token carries the hook’s slice (LaunchKeys.customHook). The API answers 400companion_rulesto a listed config or a holders’ share beside one. - First buy
- Within what max wallet lets one wallet hold at the opening (
devBuyMaxLamports; 400max_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. The API can’t prepare that mint again: take the SOL back, then launch with a fresh mint.
import { type CompanionRefundResponse, type CompanionStatus } from '@bordrless/shared';
import { companion } from '@bordrless/sdk';
// The setup landed, the launch didn't: its SOL goes back to the wallet that set it up.
const status = (await (await fetch(`${API}/companion/${mint.publicKey.toBase58()}`)).json()) as CompanionStatus;
if (status.state === 'set-up') { // status.refundable: the lamports it returns
const { transaction } = await post<CompanionRefundResponse>('companion/refund/prepare', { wallet: wallet.publicKey.toBase58(), mint: mint.publicKey.toBase58() });
// sign it with the wallet, then the mint, as above, and send it to tx/submit
}
// The same with the SDK: withdraw before the launch, signed by the mint. Anyone may send it; it pays the beneficiary.
const refund = companion.refund(wallet.publicKey, mint.publicKey, wallet.publicKey);
Launch with one: the builders#
What the API builds, by hand: create (signed by the payer and the mint), then launch wrapping createLaunch with the companion address, companionCreatorAddress(mint), as the creator (the launcher and the mint sign), then optionally devBuy. The API sets virtualQuote from the SOL price; by hand, pick it within the config’s minVirtualQuote and maxVirtualQuote. The split may be any that sums to 10,000 bps, as through the API.
import { Keypair } from '@solana/web3.js';
import { NO_RULES } from '@bordrless/shared';
import {
COMPANION_DEFAULTS, COMPANION_TEMPLATES, LAUNCH_CONFIG, buildV0Transaction, companion, companionCreatorAddress,
companionReady, decodeLaunchConfig, fetchAccount, launch, launchKeys, launchRulesFromInput,
} from '@bordrless/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 companion address's own rent
});
// 2. launch: create_launch with the companion 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: Bordrless 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 fees in, the bounty paid, the rest split into the pending buyback, holders’ and launcher’s parts. Bordrless’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.
import type { TransactionInstruction } from '@solana/web3.js';
import { rewardsOn } from '@bordrless/shared';
import {
companion, companionAddress, companionVested, decodeCompanion, decodeLaunch, fetchAccount, fetchLaunchCreatorFees,
launchAddress, launchKeysOf, launchRulesInputOf,
} from '@bordrless/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));
// The launch's unclaimed fees: its lamports above rent (an older coin's: its bridged-SOL holding).
const fees = (await fetchLaunchCreatorFees(connection, mint))?.fees ?? 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#
import { companionAddress, companionVested, decodeCompanion } from '@bordrless/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
// Or the API: GET /api/launches/:mint answers LaunchDetail.companion (CompanionSummary), figures as strings.
Custom companions: your script#
A custom companion (on the launch page, a Workshop companion under Companion fee) is a program of yours, the script, that the companion program calls to spend the pot the companion fee fills. companion_init runs once at the setup, before the launch, with [creator (signer), mint, state, system program] and its init data (at most 512 bytes; a Workshop script’s comes from its keeper.json, never from the launcher); companion_step runs from a minute after the launch, at most once an interval (10 s to a day), with [creator (signer), mint, launch, companion, pool, state] and then whatever accounts the sender passed, none of them signing. The state is yours, made by the companion at scriptStateAddress(mint) (up to 10,240 bytes, zeroed, owned by your program). Your script reads the hook’s data in the holdings, the pool and its state, and acts as the companion address: it may trade on the launch pool (launch.swap with the companion address as trader and recipient), buy back, burn. After every call the companion program checks the result and refuses the whole step otherwise: at most the pot spent (ScriptOverspent), the dev bag untouched (ScriptTookDevBag), the companion address’s rent kept (ScriptTookLamports; on a native coin the pot is its lamports above rent), custody intact (ScriptBrokeCustody), not early (ScriptNotDue), and only code that is approved or immutable (NotApproved). The template and the brief: Build with your agent.
- The Workshop
- Submit the source with its
keeper.json; Bordrless checks it, deploys it under its key and approves its code on chain (Approval, bound to the executable hash: an upgrade voids it). Launchers pick it on the launch page under Companion fee, with Bordrless’s reading of it, and you earn half of the launcher’s part of the creator fee each claim takes on a launch someone else makes with it, paid by the companion program out of that part (never from the pot), or nothing, as you chose at the submission. - Self-serve
- Deploy it yourself and make it immutable: the companion program runs an immutable script with no approval. Launch with it through the SDK, or the API with
companion.script.program. It is launched unreviewed, with no author’s share, and the site names it a custom hook (unreviewed): not read by Bordrless, unless you submit its source with its address for the reading. - Hidden
- A launch whose creator fee goes to a program of your own, not the companion program, is allowed by the chain, and hidden on the site: nothing on the launchpad may present a pot that Bordrless’s program does not hold.
import { Keypair } from '@solana/web3.js';
import { COMPANION_DEFAULTS, companion, companionAddress, decodeCompanion, decodeScript, fetchAccount, inspectScriptProgram, launchKeysOf, scriptAddress, scriptClaimAuthor, scriptDue, scriptInitDataBytes } from '@bordrless/sdk';
// 1. What the companion program will say of your script: immutable (self-serve), approved (the Workshop), or why not.
const status = await inspectScriptProgram(connection, script); // lane: 'workshop' | 'self-serve' | null, problems: [...]
// 2. The setup, before the launch, both signed by me and the mint: the companion (a placeholder split), then its script
// (the real split with the pot's part: the four add up to 10,000). The script's companion_init runs here.
const mint = Keypair.generate(); // keep it until the launch confirms
const split = { buybackBps: 0, holdersBps: 0, beneficiaryBps: 3_000 };
const setup = [
companion.create(me, me, mint.publicKey, { split: { buybackBps: 0, holdersBps: 0, beneficiaryBps: 10_000 }, ...COMPANION_DEFAULTS, fund }),
companion.createScript(me, mint.publicKey, { split, potBps: 7_000, program: script, interval: 60, stateLen: 128, initData: scriptInitDataBytes('0102') }),
];
// then companion.launchScript(...) (companion.launch's arguments, the script's address appended: needed while native launches are on,
// else MissingAccount); the API does all three with companion: { split, script: { ... } }.
// 3. Crank it, as anyone may: the fees in (an author's half of the launcher's part paid out of that part, when there
// is one; never from the pot), then a step once due.
const c = (await fetchAccount(connection, companionAddress(mint.publicKey), decodeCompanion))!;
const s = (await fetchAccount(connection, scriptAddress(mint.publicKey), decodeScript))!;
const claim = companion.claimFeesScript(me, mint.publicKey, scriptClaimAuthor(s, c));
if (scriptDue(c, s, now)) {
const keys = launchKeysOf(l); // the launch, decoded; with its hook's accounts for a pair
const step = companion.scriptStep(me, keys, s.program, extras); // extras: what your companion_step documents after its six accounts
}
Through the API: POST launch/prepare with companion: { split, script: { potBps, program, interval, initData, stateLen } } (the pot’s part with the split’s three adds up to 10,000; scriptProblem in shared says what would be refused; 400 script_not_approved for a program that is neither approved nor immutable). The token page reads LaunchDetail.companion.script, a ScriptSummary: the pot, the steps, the next step, the author, the Workshop item, and reviewed (false for an immutable script launched unreviewed).
Which holders it pays: keeper.json#
Bordrless’s keeper sends your script’s steps. A script that pays holders needs their holdings among a step’s accounts, and only the sender of a step chooses those, so your script says in data which ones to bring: keeper.json, beside src/ (where Cargo.toml would sit). It is submitted with the source, checked against its schema and approved with the build, and the Workshop’s page says it in words. Without one, a step brings the pool’s swap accounts alone, so nobody gets paid.
{
"version": 1,
"holdings": {
"minBalance": "1",
"maxBalance": "19999999999999",
"hookData": {
"offset": 1,
"width": 4,
"compare": "ne",
"against": { "epoch": { "secs": 300, "fieldUnitSecs": null } }
},
"payTo": "owner",
"perStep": { "withSwapAccounts": 3, "withoutSwapAccounts": null }
},
"swapAccounts": true,
"hookAccounts": false,
"mintWritable": false,
"payouts": false,
"roundSecs": 300
}
holdings- Which holdings a step brings: the least and the most balance that count (base units, or a field of your script’s state), a field of each holding’s 64 bytes of hook data compared with a constant, the pool’s price or the current epoch, where each is paid (
payTo:owner, the owner’s wallet, paid in lamports;ownerQuoteHolding, its bridged-SOL holding, for coins from before 15 Oct 2026 only; ornone), and how many a step brings (perStep, 1 to 16). swapAccounts- Each step also carries the launch pool’s swap accounts, which trading the pot needs.
hookAccounts- A step without the swap accounts carries the token’s hook accounts, for moving the token inside a step.
mintWritable- Default false. Every step passes the coin’s mint writable, so the script can burn what it bought: any burn needs it (with the swap accounts, their mint is made writable). Without it a burn fails the whole step. A burn never reaches the dev bag.
payouts- The companion’s payout accounts come first, for a script made with
createScriptV2whose hook records payouts. wallets- Up to 12 wallets each step brings for wallet answers (wallet payouts).
roundSecs- The round the keeper’s log counts by.
stepSecs- How often a step runs, 10 s to a day: a launch with your script takes it from here, never from the launcher. Without it, a thirtieth of a fixed roundSecs (Whale Bait’s 300 s: every 10 s), else a minute.
payoutTerms- The payout terms a launch makes the script with (
stepBps,holdingBps,windowBps,windowSecs, ascreateScriptV2takes them). Without it a Workshop script launches with open terms: a step may pay the whole pot. Either way the launch’s beneficiary or the script approver can change them after the launch (companion.setPayoutTerms). launcherShareBps- The launcher’s share of each trade’s fee on every launch with your script, fixed: 0 to 9,500 bps of the whole, the rest the companion fee. Without it the launcher sets the two rows (a pair: its config’s split, half each unless you set it).
Anyone can send a step with any accounts, so your script still checks every account it is handed: a holding of this mint, its owner, its balance within your bounds, its stamp, a payee that is that owner’s. checkKeeperJson(text) in the SDK says what the Workshop would refuse, and its words for the rest; KEEPER_EXAMPLE is the template’s.
A launch with a Workshop script asks the launcher for nothing: companion_init gets keeper.json’s initData (hex; empty when it says none), the interval is its stepSecs, and the state account is its stateLen bytes (1,024 when it says none).
Payees and wallet payouts#
Payees. Up to 10 wallets can share the launcher’s part of the creator fee (beneficiaryBps), each a fixed share of it in basis points, 10,000 in all at most; the launcher keeps the rest. They are set once, before the launch, with companion.setPayees(payer, mint, [{ wallet, bps }]), the mint signing as for create. Each wallet is named once, and none is the launcher, the companion or its companion address (BadPayees). Every withdraw then pays them first: companion.withdrawWithPayees(sender, mint, beneficiary, wallets), which Bordrless’s crank sends; a wallet that can’t take its share yet is owed it, never skipped. Read them with decodePayees at payeesAddress(mint); Companion.payees says a companion has them. Payees share the fee part only: the first buy still vests to the launcher.
Wallet payouts. A script made with createScriptV2 has payout terms: the most a step pays of the pot, the most one holding or wallet gets of a step, the most a window pays. A Workshop script launched from the site gets open terms (a step may pay the whole pot) unless its keeper.json sets payoutTerms; the launch’s beneficiary or the script approver can change them after the launch with companion.setPayoutTerms(authority, mint, terms). If its launcher sent companion.allowWalletPayouts(mint) before the launch (the mint signing), the script may also answer encodeWalletPayoutAnswer([{ to, lamports, tag }]): the companion pays each wallet SOL from the pot within those same terms, each wallet passed writable among the step’s accounts, and never one of the launch’s own accounts. Buyers can read it before they trade: byte 100 of the terms at payoutTermsAddress(mint) (PayoutTerms.reserved[0]), and the token page says it in words. The launcher’s own wallet can be paid, so say in your script’s name what it pays.
Through the API: companion: { split, payees: [{ wallet, bps }] } for any companion (the setup carries set_payees, in a transaction of its own, “Set up the payees”, beside a custom hook or Lossback), and script: { ..., payout: { stepBps, holdingBps, windowBps, windowSecs } }, allowWalletPayouts: true for a script that pays wallets. payeesProblem and walletPayoutsProblem in shared say what would be refused (400 companion_rules). The token page reads LaunchDetail.companion.payees and script.walletPayouts.
Pairs: a hook and its script#
A pair is a hook on the trades and a companion on the pot, submitted to the Workshop as one item (both sources, one approval bound to both builds, one deploy, one config). The hook can take a cut, keep a score or fund the pot; the companion pays from the pot, filled by the companion fee, by the hook (its SOL cut lands whole in the pot), or both (Whale Bait: the hook cuts whale trades in tokens into the companion address’s holding, the script sells them into the pot and pays the holders under 20M tokens). On chain it is the two above: a plain config on the hook (never listed: a companion refuses a config’s author share, so the pair’s builder is paid by the companion program alone, half of the launcher’s part of the creator fee) and companion.createScript in the same setup, the launch wrapping launch.createLaunch from that config with the hook’s accounts in companion.launch. Which companion a config requires (ConfigInspection.requiredCompanion: the pair’s script, a game’s kind, or none) is the API’s record until the on-chain LaunchConfig field lands; the launch is refused without it (companion_required) or from any other config (config_required), and the launch page locks the hook to it. The pair’s config fixes how its fee splits between the launcher’s creator fee and the companion fee (half each unless its builder sets it; keeper.json’s launcherShareBps wins), and the launcher can’t adjust the fee or the split. The four patterns, with what each ships and what its card reads: build with your agent.
A pair may also pay through the companion program (Lossback’s path): the script is made with companion.createScriptV2, so it has payout terms, and its step answers a PayoutAnswer naming holdings among its accounts; the companion checks every entry against the terms, pays each owner SOL from the pot and has the hook record it through the hook’s own record_payout, which only the companion’s payout authority may sign (the hook needs WRITES_HOOK_DATA). One bad entry fails the whole step.
import type { PublicKey } from '@solana/web3.js';
import { COMPANION_DEFAULTS, PAYOUT_LIMITS, companion, decodePayoutTerms, encodePayoutAnswer, launchKeysOf, payoutTermsAddress } from '@bordrless/sdk';
// 1. The setup: the script made with create_script_v2, so it has payout terms: the most a step pays of the pot,
// the most one holding (or wallet) gets of a step's budget, the most a window pays of the pot, and the window.
// These are yours to choose; a site launch makes them open (10_000 each, a 10 s window) unless keeper.json's
// payoutTerms sets them, and the launch's beneficiary or the script approver may replace them after the launch:
// companion.setPayoutTerms(me, mint.publicKey, terms).
const terms = { stepBps: 1_000, holdingBps: 10_000, windowBps: 2_500, windowSecs: 86_400 };
const setup = [
companion.create(me, me, mint.publicKey, { split: { buybackBps: 0, holdersBps: 0, beneficiaryBps: 10_000 }, ...COMPANION_DEFAULTS, fund }),
companion.createScriptV2(me, mint.publicKey, { split, potBps, program: script, interval: 15, stateLen: 128, initData: new Uint8Array() }, terms),
// companion.allowWalletPayouts(mint.publicKey): the script may then answer wallets too (encodeWalletPayoutAnswer).
];
// 2. A step that answers payouts carries the companion's payout accounts first, then each holding (writable) with its
// owner (writable) after it: what keeper.json { "payouts": true, "payTo": "owner" } has Bordrless's keeper bring.
const w = (pubkey: PublicKey) => ({ pubkey, isSigner: false, isWritable: true });
const extras = [...companion.payoutAccounts(mint.publicKey, hook), ...candidates.flatMap((c) => [w(c.holding), w(c.owner)])];
const step = companion.scriptStep(me, launchKeysOf(l), script, extras);
// 3. What the script's companion_step returns (set_return_data in Rust): version 1, up to PAYOUT_LIMITS.maxPayoutsPerStep
// entries, each the index of a holding among the extras, lamports and a tag the hook records.
const answer = encodePayoutAnswer({ version: 1, entries: [{ account: 7, lamports: 50_000_000n, tag: 12 }] });
const read = decodePayoutTerms((await connection.getAccountInfo(payoutTermsAddress(mint.publicKey)))!.data); // paidTotal, walletsAllowed, …
Approvals: read one#
Bordrless’s approval of a program’s code is an account at approvalAddress(program): the kind (a token hook, or a companion script), the executable hash and the ProgramData slot it was approved at, revoked or not, and the author. It holds while the program runs that code. decodeApproval reads it; inspectScriptProgram(connection, program) reads it with the program and says what the companion would do with it: lane is 'workshop' (approved and current), 'self-serve' (immutable) or null, with the reason in problems.
Jackpot coins: the lottery#
A companion may run a lottery (companion v2, live on mainnet; the launch page’s Jackpot). Part of every fee claim, the companion fee, goes into a pot the companion holds; each round (5 minutes to 30 days for the programs; the site offers 15 minutes and up), every holder’s tokens held since the round began are their tickets; once it ends, anyone starts a draw, randomness by ORAO VRF, and the pot pays the holder of the drawn ticket in SOL. The coin’s token hook is Bordrless’s lottery hook (LOTTERY_HOOK_PROGRAM), which keeps the tickets and takes no cut; a lottery coin has no other token rules. That hook has an audited status on mainnet, so its pots are not capped and it cannot be blocked; a game on any other hook is capped at 10 SOL until audited, and Bordrless can stop it: its pot is then bought back and burned, and nobody is paid. The mechanism in full, with every rule and failure mode: docs/games.md.
import { LOTTERY_DEFAULTS, NO_RULES, type GameEnterResponse, type GameStatus, type LaunchPrepareRequest } from '@bordrless/shared';
import { LOTTERY_HOOK_PROGRAM, companion, decodeGame, fetchAccount, fetchSeedSlot, gameAddress, lotteryHook, roundOf } from '@bordrless/sdk';
// 1. The launch, through the API: the same as a companion launch, with the lottery named. No token rules (the
// coin runs the lottery hook), a creator fee the lottery is open at, no holders' part: the pot takes its place.
const request: LaunchPrepareRequest & { mint: string } = {
creator: wallet.publicKey.toBase58(), mint: mint.publicKey.toBase58(), uploadId, creatorFeeBps: 100, rules: NO_RULES,
companion: { split: { buybackBps: 3_000, holdersBps: 0, beneficiaryBps: 0 }, game: LOTTERY_DEFAULTS },
// LOTTERY_DEFAULTS: 6-hour rounds, 70% of the fee to the pot, drawn from 0.5 SOL paying the whole pot, 8 claims of 10 min.
};
// prepared.transactions: "Set up the lottery" (create, prepare, create_game; the mint signs), "Launch", "Buy".
// 400 companion_rules says what the program would refuse; 503 lottery_off: no lottery config at that fee yet.
// 2. The token page's figures: GET /api/companion/:mint?wallet=<holder> -> CompanionStatus.game (GameStatus):
// the pot, the round and its end, the tickets, the holder's tickets and odds, the draws (each with its ORAO request).
const status = (await (await fetch(`${API}/companion/${mint.publicKey.toBase58()}?wallet=${holder}`)).json()) as { game: GameStatus | null };
// 3. A holder's own entry (their tokens registered as this round's tickets; a trade this round does it too):
const { transactions } = await post<GameEnterResponse>('game/enter/prepare', { wallet: holder, mint: mint.publicKey.toBase58() });
// or with the builders: lotteryHook.enter(mint, holder), sent by anyone.
// 4. Crank a draw (anyone; docs/games.md "A keeper's loop"): once the round ends, draw from the newest slot hash,
// sent at once without preflight (it must land within 3 slots: StaleSeed otherwise, send again), then reveal
// ORAO's answer and claim the prize for the winner found among the holdings.
const g = (await fetchAccount(connection, gameAddress(mint.publicKey), decodeGame))!;
if (g.status === 'idle') {
const at = await fetchSeedSlot(connection);
const draw = companion.draw(me.publicKey, mint.publicKey, LOTTERY_HOOK_PROGRAM, roundOf(now, g.roundSecs) - 1, at, oraoTreasury, g.paidSeed);
}
The companion program also takes two more game kinds, jackpot (the last buyer before a timer) and streak (holders who held through an epoch), on a game hook written on the bordrless-game crate (createGameV2, settle, closeEpoch, claimShare in the SDK). Bordrless offers no hook for them and the site does not present them.