# How LOOT works > Launch a coin on pump.fun and pair it to a brand. 80% of trading fees fund rewards for holders. 20% buys and burns $LOOT. Holders redeem what they earn as that brand's gift card. ## 1. Overview Trading a coin on pump.fun earns creator fees. Once enough has built up, they are claimed and split the same way every time. Creator fees are claimed in one transaction. 80% funds rewards for holders, paid by the coin's launch mode, and holders redeem that balance as a gift card for the paired brand. 20% buys and burns $LOOT. - **Holders:** 80%, funds rewards for holders - **Buy & burn:** 20%, buys and burns $LOOT - **Claimed:** Once unclaimed fees reach 0.1 SOL - **Other fees:** None ## 2. Launch modes Create a coin, choose a brand, and launch on pump.fun. You pick how holders get paid, and how much they have to hold. Those choices are locked at launch. ### Dividends Rewards are paid on every claim. Your share is your share of the tokens that qualify. No draw. Hold more, earn more. ### Giveaway Rewards accumulate for 10 minutes. Eligible holders are given tickets. Hold more, get more tickets, and a higher chance to win. You can only win once. Each giveaway prize is the cheapest US gift card for that brand, plus 10%. A $550 pot and a $10 card means 50 winners, each with enough to redeem one card. If the pot is too small for one prize, it rolls to the next draw. - **Who qualifies.** Hold at least the minimum and you qualify. Hold less and you don't, in both modes. - **Draw times.** Giveaways draw at :00, :10, :20, :30, :40, and :50 past every hour, UTC. - **Nobody qualifies.** Dividends record the claim without paying holders. Giveaways keep the fees and try again at the next draw. ### Minimum holding You pick one at launch. It never changes. Supply is 1B tokens. Giveaways use whole tokens as tickets. Dividends use your token balance. The bonding curve and the Pump pool never count. - **100K tokens** (0.01% of supply). - **1M tokens** (0.1% of supply). - **10M tokens** (1% of supply). ## 3. Balances and redeeming - Earnings are credited to a balance for each wallet and brand. - Coins paired to the same brand add to the same balance. - To redeem, choose a gift card amount on the brand page. The balance is debited and the gift card is issued. - If your region isn't listed, transfer the balance to any other brand available in your region. If none are, cash it out as SOL. - There is a 20% penalty on SOL claims, so the gift card is the better deal. - If a gift card fails to issue or has an error, the spent amount is credited back. ## 4. Listing requirements - It trades against SOL. - The paired brand is in the LOOT registry. - Its creator fees were locked to the 80/20 split at launch. # Build on LOOT > One instruction, link_coin, pairs a pump.fun coin to a brand and locks its fee split, payout, and holder floor. Holder credit and redemption happen off-chain. ## 1. Launch Send `create_v2` followed by `link_coin`. An optional dev buy goes between them. `link_coin` checks that the product id is in the registry and that the bonding-curve creator is still the signer, then locks the fee split and stores the payout and floor. `launch.ts` ```ts import { AccountRole, address, appendTransactionMessageInstructions, createSolanaRpc, createTransactionMessage, generateKeyPairSigner, getAddressEncoder, getProgramDerivedAddress, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, type Address, type Instruction, type TransactionSigner, } from "@solana/kit"; const LOOT = address("LootzMHHCubYFZEgg7kjbyfdsEFBxfPkrUPNzuoExCk"); const PUMP = address("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); const PUMP_FEE = address("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"); const PAMM = address("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); const GLOBAL = address("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); const MINT_AUTHORITY = address("TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"); const EVENT_AUTHORITY = address("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"); const MAYHEM_PROGRAM = address("MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e"); const MAYHEM_GLOBAL = address("13ec7XdrjF3h3YcqBTFDSReRcUFwbCnJaAQspM4j6DDJ"); const MAYHEM_SOL_VAULT = address("BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s"); const SYSTEM = address("11111111111111111111111111111111"); const TOKEN = address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); const TOKEN_2022 = address("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); const ATA_PROGRAM = address("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); const WSOL = address("So11111111111111111111111111111111111111112"); const text = new TextEncoder(); const enc = getAddressEncoder(); const ro = (account: Address) => ({ address: account, role: AccountRole.READONLY }); const rw = (account: Address) => ({ address: account, role: AccountRole.WRITABLE }); const rwSigner = (signer: TransactionSigner) => ({ address: signer.address, role: AccountRole.WRITABLE_SIGNER, signer, }); function concat(...parts: Uint8Array[]): Uint8Array { const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0)); let offset = 0; for (const part of parts) { out.set(part, offset); offset += part.length; } return out; } function u32(value: number): Uint8Array { const bytes = new Uint8Array(4); new DataView(bytes.buffer).setUint32(0, value, true); return bytes; } function u64(value: bigint): Uint8Array { const bytes = new Uint8Array(8); new DataView(bytes.buffer).setBigUint64(0, value, true); return bytes; } function borshString(value: string): Uint8Array { const bytes = text.encode(value); return concat(u32(bytes.length), bytes); } async function discriminator(name: string): Promise { const hash = await crypto.subtle.digest("SHA-256", text.encode(`global:${name}`)); return new Uint8Array(hash).subarray(0, 8); } async function pda(program: Address, seeds: Uint8Array[]): Promise
{ const [derived] = await getProgramDerivedAddress({ programAddress: program, seeds }); return derived; } async function ata(owner: Address, mint: Address, tokenProgram: Address): Promise
{ return pda(ATA_PROGRAM, [enc.encode(owner), enc.encode(tokenProgram), enc.encode(mint)]); } function productIdBytes(productId: string): Uint8Array { const bytes = text.encode(productId); if (bytes.length < 1 || bytes.length > 32) throw new Error("Brand id must be 1–32 bytes."); return bytes; } function linkData(pid: Uint8Array, payout: number, floor: number): Uint8Array { const data = new Uint8Array(2 + pid.length + 2); data[0] = 3; data[1] = pid.length; data.set(pid, 2); data[2 + pid.length] = payout; data[3 + pid.length] = floor; return data; } /** * Pump create_v2. SOL pair, mayhem off, cashback off. * Creator fee stays 0, which keeps the pump.fun schedule. */ async function createV2Ix( payer: TransactionSigner, mint: TransactionSigner, meta: { name: string; symbol: string; uri: string }, ): Promise { const bondingCurve = await pda(PUMP, [text.encode("bonding-curve"), enc.encode(mint.address)]); const mayhemState = await pda(MAYHEM_PROGRAM, [text.encode("mayhem-state"), enc.encode(mint.address)]); return { programAddress: PUMP, data: concat( await discriminator("create_v2"), borshString(meta.name), borshString(meta.symbol), borshString(meta.uri), Uint8Array.from(enc.encode(payer.address)), new Uint8Array([0, 0]), u64(0n), ), accounts: [ rwSigner(mint), ro(MINT_AUTHORITY), rw(bondingCurve), rw(await ata(bondingCurve, mint.address, TOKEN_2022)), ro(GLOBAL), rwSigner(payer), ro(SYSTEM), ro(TOKEN_2022), ro(ATA_PROGRAM), rw(MAYHEM_PROGRAM), ro(MAYHEM_GLOBAL), rw(MAYHEM_SOL_VAULT), rw(mayhemState), rw(await ata(MAYHEM_SOL_VAULT, mint.address, TOKEN_2022)), ro(EVENT_AUTHORITY), ro(PUMP), ], }; } /** LOOT link_coin. Tag 3. Locks 20% burn / 80% credit before it returns. */ async function linkCoinIx(input: { payer: TransactionSigner; mint: Address; productId: string; payout: number; floor: number; }): Promise { const mintBytes = enc.encode(input.mint); const sharing = await pda(PUMP_FEE, [text.encode("sharing-config"), mintBytes]); const pumpVault = await pda(PUMP, [text.encode("creator-vault"), enc.encode(sharing)]); const pammVault = await pda(PAMM, [text.encode("creator_vault"), enc.encode(sharing)]); return { programAddress: LOOT, data: linkData(productIdBytes(input.productId), input.payout, input.floor), accounts: [ rwSigner(input.payer), ro(await pda(LOOT, [text.encode("brands")])), ro(input.mint), rw(await pda(PUMP, [text.encode("bonding-curve"), mintBytes])), rw(await pda(LOOT, [text.encode("link"), mintBytes])), ro(SYSTEM), rw(sharing), ro(GLOBAL), ro(PUMP), ro(PUMP_FEE), ro(await pda(PUMP_FEE, [text.encode("__event_authority")])), ro(await pda(PUMP, [text.encode("__event_authority")])), rw(pumpVault), rw(await ata(pumpVault, WSOL, TOKEN)), ro(WSOL), ro(TOKEN), ro(ATA_PROGRAM), ro(PAMM), ro(await pda(PAMM, [text.encode("__event_authority")])), rw(pammVault), rw(await ata(pammVault, WSOL, TOKEN)), ], }; } export async function launchCoin(input: { rpcUrl: string; payer: TransactionSigner; productId: string; name: string; symbol: string; uri: string; /** 0 dividends, 1 giveaway. */ payout: number; /** 0 is 100,000 tokens, 1 is 1,000,000, 2 is 10,000,000. */ floor: number; }) { const rpc = createSolanaRpc(input.rpcUrl); const mint = await generateKeyPairSigner(); const instructions: Instruction[] = [ await createV2Ix(input.payer, mint, input), // A dev buy, if you add one, goes here — still before link_coin. await linkCoinIx({ payer: input.payer, mint: mint.address, productId: input.productId, payout: input.payout, floor: input.floor, }), ]; const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const message = pipe( createTransactionMessage({ version: 0 }), (current) => setTransactionMessageFeePayerSigner(input.payer, current), (current) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, current), (current) => appendTransactionMessageInstructions(instructions, current), ); return { mint: mint.address, message: await signTransactionMessageWithSigners(message) }; } ``` ## 2. Modes Both bytes follow the product id in `link_coin` and are fixed for the life of the link. Credit is always computed from current token balances. Tokens use 6 decimals, so 1 token is 1,000,000 raw units. | Byte | Value | Meaning | | --- | --- | --- | | payout | 0 | Dividends | | payout | 1 | Giveaway | | floor | 0 | 100K tokens (0.01%) | | floor | 1 | 1M tokens (0.1%) | | floor | 2 | 10M tokens (1%) | ### Eligibility - A wallet qualifies if its balance is at or above the floor when the split or draw is measured. - The bonding curve, the Pump pool, and zero balances are excluded. ### Dividends - The holder share is the lamport increase from the claim. It is split when the claim is credited. - Each share is balance × lamports / total balance, in whole lamports. Wallets are sorted by address and the remainder goes to the last wallet that receives at least one lamport. - If nobody qualifies, the claim is recorded without credit. ### Giveaway - Claims are stored unassigned. At each draw, the waiting lamports are split across as many qualifying wallets as the pot can fund. Each share covers the cheapest US gift card for that brand, plus 10%. - Weight is the wallet's whole token balance. A wallet can win at most one share. The bonding curve and the Pump pool never count. - The seed is the newest waiting claim's signature, read as a big-endian integer from its base58 bytes, modulo total tickets. Walking cumulative tickets over wallets sorted by address picks a wallet. Each later seat adds the seat index to that integer and drops wallets already chosen. - If nobody qualifies, the price is missing, or the pot cannot cover one card, the claims stay unassigned until the next draw. ## 3. Integration - **LOOT program:** `LootzMHHCubYFZEgg7kjbyfdsEFBxfPkrUPNzuoExCk` ### Fee route **Fees accrue** - Creator vault `["creator-vault", sharing-config]` under Pump - sharing-config is `["sharing-config", mint]` under the Pump fee program **Claim** - `sweep_creator_fee`, then `distribute_creator_fees_v2`. `transfer_creator_fees_to_pump_v2` comes first when the Pump pool holds wrapped SOL fees - 2,000 bps to burn, 8,000 bps to credit **Credit and redeem** - Off-chain. See Modes for how the holder share is assigned. ### link_coin Instruction tag `3`. Data is the tag, a length byte, the product id, then payout and floor. The bonding curve must be quoted in SOL. `link-coin.ts` ```ts import { AccountRole, address, getAddressEncoder, getProgramDerivedAddress, type Address, type Instruction, type TransactionSigner, } from "@solana/kit"; const LOOT = address("LootzMHHCubYFZEgg7kjbyfdsEFBxfPkrUPNzuoExCk"); const PUMP = address("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); const PUMP_FEE = address("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"); const PAMM = address("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); const GLOBAL = address("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"); const SYSTEM = address("11111111111111111111111111111111"); const TOKEN = address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); const ATA_PROGRAM = address("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); const WSOL = address("So11111111111111111111111111111111111111112"); const text = new TextEncoder(); const enc = getAddressEncoder(); async function pda(program: Address, seeds: Uint8Array[]): Promise
{ const [derived] = await getProgramDerivedAddress({ programAddress: program, seeds }); return derived; } function ata(owner: Address, mint: Address, tokenProgram: Address): Promise
{ return pda(ATA_PROGRAM, [enc.encode(owner), enc.encode(tokenProgram), enc.encode(mint)]); } function productIdBytes(productId: string): Uint8Array { const bytes = text.encode(productId); if (bytes.length < 1 || bytes.length > 32) throw new Error("Brand id must be 1–32 bytes."); return bytes; } function linkData(pid: Uint8Array, payout: number, floor: number): Uint8Array { const data = new Uint8Array(2 + pid.length + 2); data[0] = 3; data[1] = pid.length; data.set(pid, 2); data[2 + pid.length] = payout; data[3 + pid.length] = floor; return data; } /** Instruction tag 3. Data is the tag, the product id, then payout and floor. The program checks the registry, wrapped SOL, and the fee lock. */ export async function linkCoinIx(input: { payer: TransactionSigner; mint: Address; productId: string; payout: number; floor: number; }): Promise { const mintBytes = enc.encode(input.mint); const sharing = await pda(PUMP_FEE, [text.encode("sharing-config"), mintBytes]); const pumpVault = await pda(PUMP, [text.encode("creator-vault"), enc.encode(sharing)]); const pammVault = await pda(PAMM, [text.encode("creator_vault"), enc.encode(sharing)]); const ro = (account: Address) => ({ address: account, role: AccountRole.READONLY }); const rw = (account: Address) => ({ address: account, role: AccountRole.WRITABLE }); return { programAddress: LOOT, data: linkData(productIdBytes(input.productId), input.payout, input.floor), accounts: [ { address: input.payer.address, role: AccountRole.WRITABLE_SIGNER, signer: input.payer }, ro(await pda(LOOT, [text.encode("brands")])), ro(input.mint), rw(await pda(PUMP, [text.encode("bonding-curve"), mintBytes])), rw(await pda(LOOT, [text.encode("link"), mintBytes])), ro(SYSTEM), rw(sharing), ro(GLOBAL), ro(PUMP), ro(PUMP_FEE), ro(await pda(PUMP_FEE, [text.encode("__event_authority")])), ro(await pda(PUMP, [text.encode("__event_authority")])), rw(pumpVault), rw(await ata(pumpVault, WSOL, TOKEN)), ro(WSOL), ro(TOKEN), ro(ATA_PROGRAM), ro(PAMM), ro(await pda(PAMM, [text.encode("__event_authority")])), rw(pammVault), rw(await ata(pammVault, WSOL, TOKEN)), ], }; } ``` ### Accounts The registry is a single account at `["brands"]`. Each coin has a rent-only link account at `["link", mint]`. `seeds.ts` ```ts import { address, getAddressEncoder, getProgramDerivedAddress, type Address } from "@solana/kit"; const LOOT = address("LootzMHHCubYFZEgg7kjbyfdsEFBxfPkrUPNzuoExCk"); const text = new TextEncoder(); const enc = getAddressEncoder(); async function pda(seeds: Uint8Array[]): Promise
{ const [derived] = await getProgramDerivedAddress({ programAddress: LOOT, seeds }); return derived; } export const registryAddress = () => pda([text.encode("brands")]); export const linkAddress = (mint: Address) => pda([text.encode("link"), enc.encode(mint)]); ``` ### Fee lock `link_coin` creates the Pump sharing config and sets two shareholders. Pump accepts this update only once, so the split cannot change afterwards. `fee-lock.ts` ```ts /** * link_coin writes this split itself. Pump accepts the update once. */ export const lootFeeLock = [ { role: "burn", bps: 2_000 }, { role: "credit", bps: 8_000 }, ] as const; ``` ## 4. Read data ### Registry Tag `5`. A 3-byte header, then 69-byte records. Readers use the count. The account can grow past 256 records, up to 10MB. Record i starts at 3 + 69i. Logos and banners live at `/brands/logos/{id}.png` and `/brands/banners/{id}.png`. | Offset | Size | Field | | --- | --- | --- | | 0 | 1 | Tag (5) | | 1 | 2 | Record count, u16 LE | | 3 + 69i | 33 | Product id, length-prefixed, max 32 | | 36 + 69i | 33 | Name, length-prefixed, max 32 | | 69 + 69i | 3 | RGB color | `registry.ts` ```ts import { address, createSolanaRpc, getProgramDerivedAddress, type Address } from "@solana/kit"; const LOOT = address("LootzMHHCubYFZEgg7kjbyfdsEFBxfPkrUPNzuoExCk"); const text = new TextEncoder(); const decoder = new TextDecoder(); const REGISTRY_HEADER = 3; const RECORD_SIZE = 69; const TAG_REGISTRY = 5; export type BrandRecord = { productId: string; name: string; color: string; logoUrl: string; bannerUrl: string }; /** One account, every card. Membership is the record. Logo and banner URLs are derived from the product id. */ export async function fetchRegistry(rpcUrl: string): Promise { const registry = await registryAddress(); const { value } = await createSolanaRpc(rpcUrl).getAccountInfo(registry, { encoding: "base64" }).send(); if (!value || value.owner !== LOOT) return []; return decodeRegistry(value.data[0]); } function decodeRegistry(dataBase64: string): BrandRecord[] { const data = Uint8Array.from(atob(dataBase64), (char) => char.charCodeAt(0)); if (data.length < REGISTRY_HEADER || data[0] !== TAG_REGISTRY) return []; const count = data[1]! | (data[2]! << 8); if (data.length < REGISTRY_HEADER + count * RECORD_SIZE) return []; const brands: BrandRecord[] = []; for (let i = 0; i < count; i += 1) { const rec = REGISTRY_HEADER + i * RECORD_SIZE; const productId = readField(data, rec, 32); brands.push({ productId, name: readField(data, rec + 33, 32), color: readColor(data, rec + 66), logoUrl: "/brands/logos/" + productId + ".png", bannerUrl: "/brands/banners/" + productId + ".png", }); } return brands; } function readField(data: Uint8Array, offset: number, cap: number): string { const length = data[offset] ?? 0; if (length < 1 || length > cap) return ""; return decoder.decode(data.subarray(offset + 1, offset + 1 + length)); } function readColor(data: Uint8Array, offset: number): string { const hex = (index: number) => (data[offset + index] ?? 0).toString(16).padStart(2, "0"); return "#" + hex(0) + hex(1) + hex(2); } async function registryAddress(): Promise
{ const [derived] = await getProgramDerivedAddress({ programAddress: LOOT, seeds: [text.encode("brands")], }); return derived; } ``` ### Link Tag `6`, 100 bytes. Payout and floor use the byte values listed in Modes. | Offset | Size | Field | | --- | --- | --- | | 0 | 1 | Tag (6) | | 1 | 32 | Mint | | 33 | 33 | Product id, length-prefixed, max 32 | | 66 | 32 | Creator | | 98 | 1 | Payout: 0 dividends, 1 giveaway | | 99 | 1 | Floor: 0 is 100K tokens, 1 is 1M, 2 is 10M | `link.ts` ```ts import { address, createSolanaRpc, getAddressEncoder, getProgramDerivedAddress, type Address } from "@solana/kit"; const LOOT = address("LootzMHHCubYFZEgg7kjbyfdsEFBxfPkrUPNzuoExCk"); const text = new TextEncoder(); const enc = getAddressEncoder(); const LINK_SIZE = 100; const TAG_LINK = 6; /** Indexed by the payout byte. */ export const PAYOUTS = ["dividends", "giveaway"] as const; /** Indexed by the floor byte. Whole tokens a wallet must hold to qualify. */ export const FLOOR_TOKENS = [100_000, 1_000_000, 10_000_000] as const; export type LinkAccount = { address: Address; mint: Address; productId: string; creator: Address; payout: (typeof PAYOUTS)[number]; floorTokens: number; }; export async function fetchLink(rpcUrl: string, mint: Address): Promise { const link = await linkAddress(mint); const { value } = await createSolanaRpc(rpcUrl).getAccountInfo(link, { encoding: "base64" }).send(); if (!value || value.owner !== LOOT) return null; const data = Uint8Array.from(atob(value.data[0]), (char) => char.charCodeAt(0)); if (data.length !== LINK_SIZE || data[0] !== TAG_LINK) return null; const pidLen = data[33] ?? 0; if (pidLen < 1 || pidLen > 32) return null; const payout = PAYOUTS[data[98] ?? -1]; const floorTokens = FLOOR_TOKENS[data[99] ?? -1]; if (payout === undefined || floorTokens === undefined) return null; return { address: link, mint: pubkeyAt(data, 1), productId: new TextDecoder().decode(data.subarray(34, 34 + pidLen)), creator: pubkeyAt(data, 66), payout, floorTokens, }; } async function linkAddress(mint: Address): Promise
{ const [derived] = await getProgramDerivedAddress({ programAddress: LOOT, seeds: [text.encode("link"), enc.encode(mint)], }); return derived; } function pubkeyAt(data: Uint8Array, offset: number): Address { return enc.decode(data.subarray(offset, offset + 32)); } ```