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 API or the 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 Bordrless 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:
| Template | Each creator fee | 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, // 0, 50, 100 or 200: what the companion runs on
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 creator fee above 0 for the companion to run.
- Holder rewards
- On whenever
holdersBpsis 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. The API answers 400companion_rules. - 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 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 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: 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 creator 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 {
BRIDGED_SOL_MINT, companion, companionAddress, companionVested, decodeCompanion, decodeHolding, decodeLaunch,
fetchAccount, holdingAddress, 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));
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#
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.
Lottery coins#
A companion may run a lottery (companion v2, live on mainnet; the launch page’s Lottery choice under “where the creator fee goes”). Part of every creator fee goes into a pot the companion holds; each round (an hour to 30 days), 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. The pot is capped at 10 SOL until the hook is audited, and Bordrless can stop a game: 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);
}
Jackpot and streak coins#
Two more games (phase 2, live on mainnet) run on a game hook written in Studio and deployed under Studio’s key, so a coin needs its own hook first; neither uses randomness. Jackpot: every buy of at least the hook’s MIN_TOKENS on the coin’s own pool restarts a TIMER_SECS countdown; when it runs out, the last buyer is paid prizeBps of the pot in SOL if they still hold everything they bought (any sale or transfer forfeits). Buys count on the bonding curve only. Streak: each epoch of the hook’s EPOCH_SECS, prizeBps of the pot is shared among the holders of at least MIN_WEIGHT who held through it without sending a token, in proportion to what they held; send anything and you lose this epoch’s and last epoch’s share. The hook’s constants are read from its source; the launch request names the rest: companion: { split, game: { kind: 'jackpot' | 'streak', potBps, minPotLamports, prizeBps, claimWindowSecs (streak) } } with a config naming the hook (ConfigInspection.game shows what it reads; JACKPOT_DEFAULTS, STREAK_DEFAULTS and gameArgsProblem in the SDK). GET companion/:mint?wallet= answers GameStatus.jackpot (the countdown, the last buyer, the open and settled rounds) or GameStatus.streak (the epoch, the wallet’s weight and share, the claim epoch); a streak holder claims with POST game/claim/prepare, and the keeper claims for everyone above a small share. The pot is capped at 10 SOL until the hook is audited, and Bordrless can stop a game: its pot is then bought back and burned. The rules in full: docs/games.md “Phase 2”.