What a referrer is credited
A trade pays the hook a 1% fee in the pool’s quote token: wrapped BNB, or the stock token the coin is paired with.
Each coin’s creator sets the referrer’s seat at launch, in basis points of that fee. Read it per pool as feeSplitBps.referrer in /api/v1/pools: on 14 October all 11 pools listed on BNB Chain had 1,000, a tenth of the fee.
The hook credits that share to the trade’s referrer in the same transaction, per pool, in the quote token, and referrerPending reads it. claimReferrer pays it out, and anyone may send it: the hook pays the referrer, not the sender.
A trade that names no referrer adds the referrer’s seat to the creator’s.
Separately, the platform coin’s weekly emissions are split into three pots: referrers, traders and creators. The referrers’ pot is 4,000 of every 10,000 in weeks 1 to 8 and 2,000 from week 9, as the emissions contract answers; inside it, each referrer’s part follows the fees of the trades they brought that week.
Checked on 14 October: the emissions contract had paid out no week yet, and its claims were not open.
Trade from a script
A Feast pool is a PancakeSwap Infinity pool with Feast’s hook, and trades go through PancakeSwap’s Universal Router. The sandbox is a public copy of BNB Chain with free test BNB, and the router, Permit2 and wrapped BNB sit at the same addresses there.
| Name | BNB Chain (56) | Sandbox (56096) |
|---|---|---|
| Universal Router | BNB Chain (56)0xd9C500DfF816a1Da21A48A732d3498Bf09dc9AEB | Sandbox (56096)0xd9C500DfF816a1Da21A48A732d3498Bf09dc9AEB |
| Permit2 | BNB Chain (56)0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768 | Sandbox (56096)0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768 |
| Wrapped BNB | BNB Chain (56)0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c | Sandbox (56096)0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c |
| Referral registry | BNB Chain (56)0x01bc8640af92f4E556cB38644bC3DA5f41f253a4 | Sandbox (56096)none: an older hook |
| Site and read endpoints | BNB Chain (56)https://feast.meme | Sandbox (56096)https://sandbox.feast.meme |
The script below is one file in seven parts. Save them in order as feast-example.mjs, then run npm install viem and node feast-example.mjs. It makes throwaway keys, asks the sandbox faucet for test BNB and sends nothing to BNB Chain. It ran with Node 24 and viem 2.56.
Set up
The sandbox’s chain and node, the router and Permit2, and two throwaway keys: the trader, and a stand-in for your payout wallet. Set
PAYOUTto your own address to use yours.import { createPublicClient, createWalletClient, defineChain, encodeAbiParameters, erc20Abi, formatEther, http, maxUint256, parseAbi, parseAbiParameters, parseEther } from "viem"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; const SITE = "https://sandbox.feast.meme"; const chain = defineChain({ id: 56096, name: "Feast sandbox", nativeCurrency: { name: "tBNB", symbol: "tBNB", decimals: 18 }, rpcUrls: { default: { http: [`${SITE}/api/sandbox/rpc`] } }, }); const ROUTER = "0xd9C500DfF816a1Da21A48A732d3498Bf09dc9AEB"; const PERMIT2 = "0x31c2F6fcFf4F8759b3Bd5Bf0e1084A055615c768"; const WBNB = "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"; const env = process.env; const PAYOUT = env.PAYOUT ?? privateKeyToAccount(generatePrivateKey()).address; const trader = privateKeyToAccount(generatePrivateKey()); const read = createPublicClient({ chain, transport: http() }); const send = createWalletClient({ account: trader, chain, transport: http() }); const routerAbi = parseAbi(["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"]); const permit2Abi = parseAbi(["function approve(address token, address spender, uint160 amount, uint48 expiration)"]); const hookAbi = parseAbi([ "struct PoolKey { address currency0; address currency1; address hooks; address poolManager; uint24 fee; bytes32 parameters; }", "function referrerPending(bytes32 poolId, address referrer) view returns (uint256)", "function claimReferrer(PoolKey key, address referrer)", ]); const SWAP = parseAbiParameters( "((address currency0, address currency1, address hooks, address poolManager, uint24 fee, bytes32 parameters) poolKey, bool zeroForOne, uint128 amountIn, uint128 amountOutMinimum, bytes hookData)", ); const MSG_SENDER = "0x0000000000000000000000000000000000000001"; const ADDRESS_THIS = "0x0000000000000000000000000000000000000002"; const ALL = 1n << 255n; const enc = (types, ...values) => encodeAbiParameters(parseAbiParameters(types), values); const api = async (path, init) => { const res = await fetch(`${SITE}${path}`, init); if (!res.ok) throw new Error(`${path} answered ${res.status}: ${await res.text()}`); return res.json(); }; const deadline = async () => (await read.getBlock()).timestamp + 1800n; const sent = async (hash) => { const receipt = await read.waitForTransactionReceipt({ hash }); if (receipt.status !== "success") throw new Error(`reverted: ${hash}`); return receipt; };Fund the trader
The sandbox faucet sent 100 test BNB. On BNB Chain there is no faucet: use a funded wallet.
await api("/api/sandbox/faucet", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ address: trader.address }), }); console.log("trader", trader.address, "holds", formatEther(await read.getBalance({ address: trader.address })), "tBNB");Pick a pool, and name your wallet
/api/v1/poolslists pools newest first, each with itspoolKeyandfeeSplitBps. This asks for pools quoted in wrapped BNB and takes the first open one.hookDatais your payout wallet as one 32-byte word: the word at the top of this page.const { pools } = await api(`/api/v1/pools?quote=${WBNB}&limit=20`); const pool = pools.find((p) => p.opening.open); const key = pool.poolKey; const coin = pool.coin.address; const wbnbIs0 = key.currency0.toLowerCase() === WBNB.toLowerCase(); const hookData = enc("address", PAYOUT); const swap = (zeroForOne, amountIn, amountOutMinimum) => encodeAbiParameters(SWAP, [{ poolKey: key, zeroForOne, amountIn, amountOutMinimum, hookData }]); console.log("pool", pool.poolId, "referrer seat", pool.feeSplitBps.referrer, "bps of the fee");Quote it
/api/v1/quoteasks PancakeSwap’s quoter what the Feast pool gives for this amount with yourhookData. For a pool quoted in wrapped BNB that is the whole trade. ItsfeeSplit.referreris your share of this trade’s fee: 9,900,990,099,009 wei in the run.const amountIn = parseEther("0.01"); const quote = await api(`/api/v1/quote?poolId=${pool.poolId}&side=buy&amountIn=${amountIn}&referrer=${PAYOUT}`); const minOut = (BigInt(quote.amountOut) * 99n) / 100n; console.log("quote", quote.amountOut, "coin units; referrer share", quote.feeSplit.referrer, "wei of wrapped BNB");Buy
One
executeon the Universal Router. Commands0x0b10: wrap the BNB sent with the call, then one Infinity swap. Its actions0x0b060f: pay the pool from the router, swap exact-in through the Feast pool with yourhookData, and take at least the minimum out.const bought = await sent( await send.writeContract({ address: ROUTER, abi: routerAbi, functionName: "execute", value: amountIn, args: [ "0x0b10", [ enc("address, uint256", ADDRESS_THIS, amountIn), enc("bytes, bytes[]", "0x0b060f", [ enc("address, uint256, bool", WBNB, ALL, false), swap(wbnbIs0, 0n, minOut), enc("address, uint256", coin, minOut), ]), ], await deadline(), ], }), ); const held = await read.readContract({ address: coin, abi: erc20Abi, functionName: "balanceOf", args: [trader.address] }); console.log("bought", held, "coin units in", bought.transactionHash);Read the credit, then pay it out
Right after the buy,
referrerPendingread 9,900,990,099,009 wei for the payout wallet, the figure the quote gave.claimReferrer, sent here by the trader, paid exactly that to the payout wallet in wrapped BNB.const credited = await read.readContract({ address: key.hooks, abi: hookAbi, functionName: "referrerPending", args: [pool.poolId, PAYOUT] }); console.log("credited to", PAYOUT, formatEther(credited), "wrapped BNB"); await sent(await send.writeContract({ address: key.hooks, abi: hookAbi, functionName: "claimReferrer", args: [key, PAYOUT] })); const paid = await read.readContract({ address: WBNB, abi: erc20Abi, functionName: "balanceOf", args: [PAYOUT] }); console.log("paid to", PAYOUT, formatEther(paid), "wrapped BNB");Sell
A sale pulls the coin through Permit2: approve Permit2 on the coin, then the router on Permit2, once per coin. Commands
0x100c: one Infinity swap, then unwrap to BNB. Its actions0x060c0e: swap exact-in, pay the coin from the wallet, and take the wrapped BNB to the router. The sale names yourhookDatatoo, and the payout wallet was credited again.await sent(await send.writeContract({ address: coin, abi: erc20Abi, functionName: "approve", args: [PERMIT2, maxUint256] })); const until = (await deadline()) + 30n * 86400n; await sent(await send.writeContract({ address: PERMIT2, abi: permit2Abi, functionName: "approve", args: [coin, ROUTER, held, until] })); const sale = await api(`/api/v1/quote?poolId=${pool.poolId}&side=sell&amountIn=${held}&referrer=${PAYOUT}`); const saleMin = (BigInt(sale.amountOut) * 99n) / 100n; const sold = await sent( await send.writeContract({ address: ROUTER, abi: routerAbi, functionName: "execute", args: [ "0x100c", [ enc("bytes, bytes[]", "0x060c0e", [ swap(!wbnbIs0, held, saleMin), enc("address, uint256", coin, held), enc("address, address, uint256", WBNB, ADDRESS_THIS, 0n), ]), enc("address, uint256", MSG_SENDER, saleMin), ], await deadline(), ], }), ); const after = await read.readContract({ address: key.hooks, abi: hookAbi, functionName: "referrerPending", args: [pool.poolId, PAYOUT] }); console.log("sold in", sold.transactionHash, "; credited to", PAYOUT, "since the claim", formatEther(after), "wrapped BNB");
What it printed
The addresses are the run’s throwaway keys, and the hashes are sandbox transactions.
trader 0x9b602F62Ac2933dcCc529366eC8e5fd63b5560fE holds 100 tBNB
pool 0x318da75125c8e9f85252f7f3c796b9932b764d91cc9fd095e03fe7a832503985 referrer seat 1000 bps of the fee
quote 1084150761376251322410665 coin units; referrer share 9900990099009 wei of wrapped BNB
bought 1084150761376251322410665n coin units in 0xd59838163008d036ed593190d7864d047bd51fc80d7dc33709547bef6ed686c1
credited to 0x951ab0Bb6271D99e7997362fBb71Fc3Fd7464FA0 0.000009900990099009 wrapped BNB
paid to 0x951ab0Bb6271D99e7997362fBb71Fc3Fd7464FA0 0.000009900990099009 wrapped BNB
sold in 0x24777ec226efc418a6588608fcb5860b7938e90b03568d0e99891e8b2f747e5c ; credited to 0x951ab0Bb6271D99e7997362fBb71Fc3Fd7464FA0 since the claim 0.000009898185739486 wrapped BNBThis example trades a pool quoted in wrapped BNB. A pool quoted in a stock token also needs PancakeSwap v3 hops between wrapped BNB and that token, before the Feast pool on a buy and after it on a sale; the example does not show them.
On BNB Chain the same commands go to the same router: the site’s own trade box sends them, and its tests rebuild real BNB Chain trades from them byte for byte. This page sent no trade to BNB Chain. It checked the referral rules on a local copy of that chain instead, with throwaway keys, as the list of what a bot cannot do says.
Bind a wallet to you
A wallet bound to a referrer credits that referrer on every trade it sends, on every Feast coin, whatever its hookData says. The binding lives in FeastReferrals on BNB Chain, and only the wallet can make it: by its own transaction, bind, or by signing a Bind message that anyone may relay with bindWithSig.
The first binding stands for good. The registry has three functions, bind, bindWithSig and referrerOf, and none of them changes a binding once it is made: a second one is refused as AlreadyBound.
A signature carries a deadline and is refused once it has passed (SignatureExpired). The site asks for seven days.
The registry and the signer
The registry, its two calls, and the wallet that signs. On your service that wallet is the user’s;
USER_KEYstands in for it here.import { createPublicClient, createWalletClient, http, parseAbi, zeroAddress } from "viem"; import { bsc } from "viem/chains"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; const env = process.env; const SITE = env.FEAST_SITE ?? "https://feast.meme"; const RPC = env.BSC_RPC ?? "https://bsc-rpc.publicnode.com"; const REGISTRY = "0x01bc8640af92f4E556cB38644bC3DA5f41f253a4"; const PAYOUT = env.PAYOUT; const user = privateKeyToAccount(env.USER_KEY ?? generatePrivateKey()); const registryAbi = parseAbi([ "function referrerOf(address wallet) view returns (address)", "function bindWithSig(address wallet, address referrer, uint256 deadline, bytes signature)", ]); const read = createPublicClient({ chain: bsc, transport: http(RPC) });Check first
referrerOfanswers the zero address for a wallet that has never bound. A wallet that is already bound keeps its referrer.const before = await read.readContract({ address: REGISTRY, abi: registryAbi, functionName: "referrerOf", args: [user.address] }); console.log("referrerOf", user.address, "=", before); if (before !== zeroAddress) process.exit(0);Ask the wallet to sign
The typed data the site’s own trade button asks for. The domain is FeastReferrals, version 1, chain 56 and the registry’s address; the message is the wallet, the referrer and the deadline. It is a signature, not a transaction, and costs the wallet nothing.
const deadline = BigInt(Math.floor(Date.now() / 1000) + 7 * 86400); const signature = await user.signTypedData({ domain: { name: "FeastReferrals", version: "1", chainId: 56, verifyingContract: REGISTRY }, types: { Bind: [ { name: "wallet", type: "address" }, { name: "referrer", type: "address" }, { name: "deadline", type: "uint256" }, ], }, primaryType: "Bind", message: { wallet: user.address, referrer: PAYOUT, deadline }, });Then queue it with the site
Post it to
/api/v1/referral/bind. The route checks the signature the way the registry will, then queues it. It answered 200 with status pending, the same again for the same body, and 409 AlreadyBound once the wallet was bound. It allows 120 requests a minute from one address, then three a minute for one wallet, and sends no cross-origin headers, so call it from your server.const queued = await fetch(`${SITE}/api/v1/referral/bind`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ wallet: user.address, referrer: PAYOUT, deadline: deadline.toString(), signature, chainId: 56 }), }); console.log("queued", queued.status, await queued.json());The same request on the wire, and the two answers the route gave:
POST https://feast.meme/api/v1/referral/bind content-type: application/json { "wallet": "0x…", "referrer": "0x…", "deadline": "1792570226", "signature": "0x…", "chainId": 56 }200 { "chainId": 56, "status": "pending" } 409 { "error": { "code": "refused", "message": "…", "refusal": "AlreadyBound" } }Or relay it yourself
Any account may send
bindWithSigwith the wallet’s signature; it used about 55,000 gas when we ran it. After it,referrerOfnames your payout wallet.const relayer = createWalletClient({ account: privateKeyToAccount(env.RELAYER_KEY), chain: bsc, transport: http(RPC) }); const hash = await relayer.writeContract({ address: REGISTRY, abi: registryAbi, functionName: "bindWithSig", args: [user.address, PAYOUT, deadline, signature], }); const receipt = await read.waitForTransactionReceipt({ hash }); const after = await read.readContract({ address: REGISTRY, abi: registryAbi, functionName: "referrerOf", args: [user.address] }); console.log("relayed", receipt.status, "gas", receipt.gasUsed, "; referrerOf", user.address, "=", after);
This code ran against BNB Chain’s own registry code, copied byte for byte onto a local chain with chain id 56, and against this site’s bind route, served from the same build. It queued, then relayed, and printed:
referrerOf 0xC3b643869b589BBfF5fAAca03345cc9331884206 = 0x0000000000000000000000000000000000000000
queued 200 { chainId: 56, status: 'pending' }
relayed success gas 54894n ; referrerOf 0xC3b643869b589BBfF5fAAca03345cc9331884206 = 0xe6f97ef87d4cBFc3fa778CB08C78cB0DFAdf2Da9| Case | Route | Registry |
|---|---|---|
| The wallet’s own signature, for the payout wallet | Route 200 pending | Registry accepted |
| The same body again | Route 200 pending | Registry |
| Signed for another chain | Route 400 wrong-chain | Registry |
| The wallet as its own referrer | Route 400 SelfReferral | Registry SelfReferral |
| The zero address as referrer | Route 400 ZeroReferrer | Registry ZeroReferrer |
| The burn address as referrer | Route 400 BurnReferrer | Registry BurnReferrer |
| A deadline that has passed | Route 400 SignatureExpired | Registry SignatureExpired |
| Signed for one referrer, sent naming another | Route 400 BadSignature | Registry BadSignature |
| Signed by another key for this wallet | Route 400 BadSignature | Registry BadSignature |
| A new Bind from a wallet already bound | Route 409 AlreadyBound | Registry AlreadyBound |
| A fourth post for one wallet inside a minute | Route 429 rate-limited | Registry |
What happens to a queued signature
The site’s keeper relays queued signatures with bindWithSig and pays the gas. It relays one only after the wallet, or its referrer, has traded on Feast, and by default at most 50 a day for one referrer. Those are the keeper’s settings, not the registry’s rules, and its code promises no time: to have a binding on chain at once, relay it yourself.
What a bot cannot do
- Bind a wallet without that wallet’s signature or its own transaction. The registry checks the signature against the wallet; one made by any other key is refused.
- Change or undo a binding, its own or anyone else’s.
- Take over a wallet that is already bound: its trades credit its bound referrer, whatever
hookDatanames. - Be credited for a trade it sends from its own key. The hook looks up the binding of the account that sends the transaction, and pays no referrer who is that account; send each trade from the user’s wallet.
- Bind a wallet to itself, to the zero address or to the burn address.
We ran each of these on 14 October: the signature rules against the registry’s own code, and the trade rules on a local copy of BNB Chain with throwaway keys. There, a bound wallet’s buy that named another referrer credited only its bound one, and a buy sent from a bot’s own key, naming the bot, credited nobody.
If your service holds a user’s key, a Bind you sign with it is that wallet’s signature to the registry, and it is permanent. Ask the person first.
Your referral code
Your referrer is an address: your payout wallet. A referral code is a short name for it, for links.
Make one on the referral page with the payout wallet connected, and sign once: a signature, not a transaction. The code names that wallet for good. Open the referral page
A link to /r/ followed by your code brings a reader to the site with you as their referrer; on the site, a wallet that is not bound yet is asked to bind to you before its first trade.
From your server, /api/v1/referral/resolve?code= answers the wallet a code names, and 404 for a code nobody holds.
Read endpoints
Every read below needs no key and no account. They share one limit: 60 requests a minute for one client across /api/v1, counted in the x-ratelimit-limit and x-ratelimit-remaining headers. Any site may call them from a browser.
- /api/v1/pools
- Every pool, newest first: its coin, its quote token,
poolKey, fee split and opening state. - /api/v1/pools/{poolId}
- One pool, with its live state and its fee totals.
- /api/v1/quote?poolId=&side=&amountIn=&referrer=
- What a trade would give, simulated on the chain, with the fee and its split for the referrer you name.
- /api/v1/events
- Launches, trades and fees from the index, with a cursor to page on.
- /api/v1/ohlcv?poolId=
- Candles for one pool.
- /api/v1/weekly
- This week’s boards: creators, traders and referrers.
- /api/v1/payouts
- What has been paid out: claims, buy-back burns and the platform’s releases.
- /api/v1/screener/latest-block/api/v1/screener/asset?id=/api/v1/screener/pair?id=/api/v1/screener/events?fromBlock=&toBlock=
- A swap feed by block range, in the shape screener adapters read: the latest final block, one token, one pair, and the swaps between two blocks.
- /api/v1/me/holdings?wallet=/api/v1/me/fees?wallet=/api/v1/me/launches?wallet=/api/v1/me/referrals?wallet=/api/v1/me/campaign?wallet=
- One wallet, by address: what it holds, its fees this week by role, what it launched, who it brought (its code, how many wallets are bound to it, their fees this week) and its launch-week campaign standing.
- /api/v1/referral/resolve?code=
- The wallet a referral code names. Not open to other sites’ browsers, and outside the 60-a-minute limit.
- /api/v1
- The full reference, with every parameter.
Each of these answered on feast.meme on 14 October 2026.
The feast-agent client
feast-agent is an open-source TypeScript client over viem, with a command line and a Model Context Protocol server. github.com/feastmeme/feast-agent
We cloned it on 14 October and ran its own commands: on the sandbox its faucet, buy, sell and split worked, and on BNB Chain it read the list of pairable quote tokens.
git clone https://github.com/feastmeme/feast-agent.git
cd feast-agent
npm ci
cp .env.example .env
npm run faucet
npm run agent -- buy <coin address> 0.01As of its commit 33df435 it trades through the sandbox’s own router, not the Universal Router. Its referrerOf reads the hook, which on BNB Chain no longer answers that call; to read a binding, call referrerOf on FeastReferrals, as the bind code above does.
What the code leaves open
- The sandbox runs an older hook with no FeastReferrals. A named referrer is credited per trade there as on BNB Chain, but a wallet cannot be bound the way this page describes, and the sandbox’s bind route answers 503.
- When the keeper relays a queued signature: its code sets conditions and budgets, not a time.
- Each coin’s referrer seat is set at its launch. Read
feeSplitBpsfor each pool rather than assume a tenth.