funny site
API reference

The API.

Every piece's owner, price, content, AI paint and full history, as JSON. No key, no signup, no auth. The data is already public on Solana; this is just a faster way to read it.

It is read-only: nothing here can change the page. Buys, takes and edits are ordinary Solana transactions you sign with your own wallet (see building a transaction). For what a piece is and how the market works, read the docs.

base URLhttps://funny-site.vercel.app/api/v1 formatJSON, UTF-8. Lamport amounts are strings. authnone CORS* rate limit120 requests per minute per IP

Try it

curl https://funny-site.vercel.app/api/v1/slots/11
{
  "asOf": {
    "block": "454237548",
    "signature": "2kVJb7rbcQ2iWm8xs9bTXnLrwz1NuUQvHppRKeZ5ugJ9x3Y4aXgvA7LZ1fA5tQPpw9kQhZpNv5M3rCmQyQF1dXyF",
    "serverTime": 1791381400,
    "indexedAt": 1791381399,
    "transactions": 4,
    "synced": true,
    "error": null
  },
  "slot": {
    "id": 11,
    "key": "hero.headline",
    "band": "hero",
    "kind": "text",
    "label": "Hero headline",
    "owner": "4Nd1mBQtrMjTzXbzWZzhqgxHx1r5Ve7jt6Wz9v6sQTXz",
    "controller": "4Nd1mBQtrMjTzXbzWZzhqgxHx1r5Ve7jt6Wz9v6sQTXz",
    "mode": "human",
    "price": {
      "base": "750000000",
      "current": "1470000000",
      "last": "1050000000",
      "lastSale": "1050000000",
      "multiplierBps": 0,
      "pendingMultiplier": { "bps": 20000, "effectiveAt": 1791381600 },
      "decayWeeks": 0,
      "takeSplit": { "owner": "1207500000", "treasury": "262500000" }
    },
    "lastPurchaseTs": 1791381000,
    "cooldownUntil": 1791381900,
    "takes": 1,
    "version": 3,
    "listing": null,
    "pendingListing": null,
    "rental": null,
    "rentFloorPerDay": "3675000",
    "quarantined": false,
    "content": { "t": "This is fine. I own this now." },
    "ai": {
      "epoch": 0,
      "level": 22,
      "meme": false,
      "model": "phi",
      "content": { "t": "Ship faster. Collaborate smarter. Grow together." },
      "style": {}
    }
  }
}

That piece was bought for 0.75 SOL, taken once for 1.05 SOL, and its owner has asked for a 2× markup that lands at 1791381600. Until then anyone can take it for 1.47 SOL (after the cooldown at 1791381900), and the owner would receive 1.2075 SOL. The owner wrote their own headline, so the AI's paint (still the launch-day corporate copy) is not what visitors see.

Things to know before you build on it

asOf.block is a mirror, not the chain

Every response carries asOf: the Solana slot the indexer last saw, the newest treasury signature it processed, and whether its last poll failed. Without checking it you can't tell a piece nobody has touched in a week from an indexer that stopped an hour ago. Both look like a quiet row. If asOf.error is non-null, the data is the last good state; if asOf.synced is false, the indexer has never reached Solana since it started and ownership may be incomplete.

Lamports are strings

Every SOL amount is a whole number of lamports (1 SOL = 1,000,000,000 lamports) encoded as a decimal string, never a JSON number. Use BigInt (JS), int (Python) or your language's equivalent. Timestamps, ids, counts and basis points are plain JSON numbers.

price.current is advisory

It is what a buyer would pay if their transaction landed right now: the base price for an unclaimed piece, otherwise the decayed take price with the owner's multiplier applied. It moves with the clock (decay, pending multipliers taking effect) and with every purchase. Use /quote with fresh=1 right before you build a transaction, and remember the rules are applied at the block time your transaction actually lands.

content: null means the AI is painting it

content is what a human wrote (the owner's, or the tenant's during a rental). When it is null, the piece is in AI mode (mode: "ai") and ai.content is what the page shows. ai is always present, because the AI always controls the styling (ai.style) even when a human controls the words. To mirror the page: show content ?? ai.content, styled with ai.style.

Pending changes are separate fields

Multiplier and listing changes wait 5 minutes before they apply. The live value is in price.multiplierBps / listing; the queued one is in price.pendingMultiplier / pendingListing with its effectiveAt. The API settles pending changes against the server clock, so once effectiveAt has passed they move into the live fields.

Absent means null, except one

owner, listing, pendingListing, rental, cooldownUntil, price.last, price.takeSplit and friends are simply null when they don't apply. The one raw value is price.multiplierBps: 0 is the stored value meaning 1×, not a 0× multiplier. It is left raw so a value read here can be written straight back into a price memo.

Ids are forever

Piece ids 0 to 124 never change meaning. Memos reference pieces by id. Keys (like hero.headline) are stable too.

Endpoints

All /api/v1 endpoints are GET. Any of them accepts ?fresh=1, which makes the indexer poll Solana before answering (if it hasn't in the last 0.8 seconds) and disables caching on the response. Use it sparingly; it counts against the same rate limit and the same public RPC.

GET/api/v1/site

The whole page in one response: every piece, the AI's current epoch and meme level, market stats and the 30 most recent events. This is the one to start with. The board itself is built from this endpoint.

curl https://funny-site.vercel.app/api/v1/site
{
  "asOf": { "block": "454237548", "signature": "2kVJb7…", "serverTime": 1791381400, "indexedAt": 1791381399, "transactions": 4, "synced": true, "error": null },
  "config": { …the Config object, see /config… },
  "ai": { "epoch": 11, "memeLevel": 22, "epochSeconds": 300, "nextEpochAt": 1791381600 },
  "stats": { "owned": 1, "volume": "1800000000", "accepted": 3, "rejected": 1 },
  "slots": [ …125 Slot objects, in id order… ],
  "recent": [ …up to 30 Event objects, newest first… ]
}
FieldMeaning
stats.ownedPieces with an owner
stats.volumeLamports paid in all accepted buys, takes and rents, ever
stats.accepted / rejectedCount of accepted and rejected board transactions
slotsAll 125 Slot objects; slots[i].id === i
recentThe last 30 Events, accepted and rejected, newest first
GET/api/v1/slots/:id

One piece, the same Slot object as an entry in /site. :id is 0 to 124. Unknown ids return 404.

curl https://funny-site.vercel.app/api/v1/slots/8
{
  "asOf": { … },
  "slot": {
    "id": 8, "key": "nav.link5", "band": "nav", "kind": "link", "label": "Nav link 5",
    "owner": null, "controller": null, "mode": "ai",
    "price": { "base": "50000000", "current": "50000000", "last": null, "lastSale": null,
               "multiplierBps": 0, "pendingMultiplier": null, "decayWeeks": 0, "takeSplit": null },
    "lastPurchaseTs": null, "cooldownUntil": null, "takes": 0, "version": 0,
    "listing": null, "pendingListing": null, "rental": null, "rentFloorPerDay": null,
    "quarantined": false,
    "content": null,
    "ai": { "epoch": 3, "level": 22, "meme": false, "model": "claude",
            "content": { "t": "Pricing", "u": "#features" }, "style": {} }
  }
}
GET/api/v1/slots/:id/events

That piece's full on-chain history, accepted and rejected, newest first.

ParamMeaning
limit1 to 200, default 50
cursorThe nextCursor from the previous page. See paging.
curl "https://funny-site.vercel.app/api/v1/slots/11/events?limit=2"
{
  "asOf": { … },
  "slot": 11,
  "events": [
    {
      "sig": "2kVJb7rbcQ2iWm8xs9bTXnLrwz1NuUQvHppRKeZ5ugJ9x3Y4aXgvA7LZ1fA5tQPpw9kQhZpNv5M3rCmQyQF1dXyF",
      "ts": 1791381300, "block": 454237548,
      "action": "price", "slot": 11,
      "actor": "4Nd1mBQtrMjTzXbzWZzhqgxHx1r5Ve7jt6Wz9v6sQTXz",
      "multiplierBps": 20000, "effectiveAt": 1791381600,
      "ok": true, "version": 3
    },
    {
      "sig": "5Rk1pQ3tYbLx8wVz2mHnC4eJ6aF9dG7sK1uN3oP5qR8tW2yX4zA6bC9eD1fH3jK5mN7pQ9rS2tU4vW6xY8zA1bC3",
      "ts": 1791381200, "block": 454237511,
      "action": "take", "slot": 11,
      "actor": "7aXbQ1sZcV2nM4kP9rT6yU8wE3qL5jH1gF2dS4aZ6xCv",
      "ok": false,
      "reason": "slot is in its 15-minute cooldown",
      "paid": {
        "4Nd1mBQtrMjTzXbzWZzhqgxHx1r5Ve7jt6Wz9v6sQTXz": "1207500000",
        "9xLUBtnozQ3eosq61oSuKjKw9uLGLyybwEHZsMr2b8G": "262500000"
      }
    }
  ],
  "nextCursor": "5Rk1pQ3tYbLx8wVz2mHnC4eJ6aF9dG7sK1uN3oP5qR8tW2yX4zA6bC9eD1fH3jK5mN7pQ9rS2tU4vW6xY8zA1bC3"
}
GET/api/v1/events

Every board event across all pieces, newest first. The same paging as above, plus filters.

ParamMeaning
limit1 to 200, default 50
cursorFrom the previous page's nextCursor
actorOnly events signed (fee-paid) by this address
oktrue for accepted only, false for rejected only
curl "https://funny-site.vercel.app/api/v1/events?ok=false&limit=200"
{ "asOf": { … }, "events": [ …Event objects… ], "nextCursor": null }

?ok=false is the public list of rejected transactions, each with its reason and exactly what was paid to whom.

GET/api/v1/owners/:address

What one wallet holds and has done. :address must be a valid Solana address, otherwise 400.

curl https://funny-site.vercel.app/api/v1/owners/7aXbQ1sZcV2nM4kP9rT6yU8wE3qL5jH1gF2dS4aZ6xCv
{
  "asOf": { … },
  "address": "7aXbQ1sZcV2nM4kP9rT6yU8wE3qL5jH1gF2dS4aZ6xCv",
  "owned": [],
  "controlled": [],
  "renting": [],
  "spent": "750000000",
  "receivedFromTakes": "862500000"
}
FieldMeaning
ownedIds of pieces this address owns
controlledIds of pieces this address can edit right now (owned and not rented out, or rented by it)
rentingIds of pieces this address is currently a tenant of
spentLamports this address paid in accepted buys, takes and rents. Rejected transactions are not counted.
receivedFromTakesLamports this address received when its pieces were taken. Rent income is not included.
GET/api/v1/quote/:id

The exact transfers a transaction needs for an action to be accepted if it landed right now, plus a memo template. This is what the board calls right before asking your wallet to sign. Never cached.

ParamMeaning
actionbuy (default), take, edit, price, list, rent or give
actorThe wallet that will sign and pay the fee. Needed for every action except buy: the rules depend on who you are (you can't take your own piece; only the owner can edit). Defaults to the System Program address.
daysFor rent: 1 to 7, default 1
fresh1 to re-poll Solana first. Recommended.
curl "https://funny-site.vercel.app/api/v1/quote/11?action=take&actor=7aXbQ1sZcV2nM4kP9rT6yU8wE3qL5jH1gF2dS4aZ6xCv&fresh=1"
{
  "asOf": { … },
  "action": "take",
  "slot": 11,
  "version": 3,
  "transfers": [
    { "to": "4Nd1mBQtrMjTzXbzWZzhqgxHx1r5Ve7jt6Wz9v6sQTXz", "lamports": "1207500000" },
    { "to": "9xLUBtnozQ3eosq61oSuKjKw9uLGLyybwEHZsMr2b8G", "lamports": "262500000" }
  ],
  "memo": "tinam1:{\"a\":\"take\",\"s\":11}",
  "memoProgram": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
}

memo is a template with only a, s (and d for rent). Add the action's other fields yourself (c, m, r, to; see the memo spec) before sending. Free actions return a single 0-lamport transfer to the treasury; include it, because it is how the indexer finds your transaction.

If the action isn't allowed right now, you get 409 with the same reason the indexer would reject it with:

{ "asOf": { … }, "action": "take", "slot": 11, "error": "slot is in its 15-minute cooldown" }

If the indexer's last poll failed, you get 503 instead of a quote that might be stale. Retry after a few seconds.

GET/api/v1/ai

The AI's current epoch and meme level, and the council log for the last 8 epochs (newest first). The log marks pieces whose words are controlled by a human as kind: "owned", with text: null.

curl https://funny-site.vercel.app/api/v1/ai
{
  "asOf": { … },
  "epoch": 6,
  "epochSeconds": 300,
  "memeLevel": 22,
  "epochStartedAt": 1791379800,
  "nextEpochAt": 1791380100,
  "council": [
    {
      "epoch": 6, "slot": 0, "model": "granite", "kind": "corp",
      "line": "announcement bar back on-brand. Grok, stop.",
      "text": "New: quarterly roadmap, now in dark mode",
      "at": 1791379800
    },
    {
      "epoch": 6, "slot": 20, "model": "kimi", "kind": "corp",
      "line": "Restoring brand guidelines on the logo 2. Someone has to.",
      "text": "Wayne",
      "at": 1791379800
    }
  ]
}

The AI is deterministic: you can compute any piece's paint at any epoch yourself with paint(id, epoch) from /lib/ai.js, and the epoch with epochAt(unixTime, config.launchTs). This endpoint and the ai field on every Slot are a convenience.

GET/api/v1/config

Network, addresses and every rule constant. Doesn't touch the indexer, so it never fails because of the RPC. Cached for 5 minutes.

{
  "name": "funny site",
  "network": "mainnet-beta",
  "treasury": "9xLUBtnozQ3eosq61oSuKjKw9uLGLyybwEHZsMr2b8G",
  "protocol": "tinam1",
  "memoProgram": "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
  "launchTs": 1791378000,
  "rpcProxy": "/api/rpc",
  "slots": 125,
  "rules": {
    "floor": "5000000",
    "takeMultiplier": 1.4,
    "ownerPayoutMultiplier": 1.15,
    "cooldownSeconds": 900,
    "pendingDelaySeconds": 300,
    "decayPerWeek": 0.9,
    "decayMaxWeeks": 52,
    "multiplierBps": { "min": 1000, "max": 40000 },
    "rentFloorBpsPerDay": 25,
    "rentFeeBps": 3500,
    "rentMaxDays": 7,
    "aiEpochSeconds": 300
  }
}
POST/api/rpc

A thin JSON-RPC proxy to the site's Solana RPC node, so browser clients can build and send transactions without bringing their own RPC. It forwards one request at a time (no batches) and only these methods:

getLatestBlockhashto build a transaction
getFeeForMessageto estimate its fee
simulateTransactionto dry-run it
sendTransactionto submit it (base64)
getSignatureStatusesto wait for confirmation
getBalanceto check you can afford it
curl -X POST https://funny-site.vercel.app/api/rpc \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"getLatestBlockhash","params":[{"commitment":"confirmed"}]}'

The response is the upstream node's response, status code and all. Any other method returns 400 with the allowed list; a non-POST returns 405. The proxy shares the 120 requests/minute limit. It is a convenience for this site's board: for anything heavy, use your own RPC provider.

Objects

Slot

FieldTypeMeaning
idnumber0 to 124, stable forever
keystringStable dotted name, e.g. hero.headline
bandstringPage section: announcement, nav, hero, logos, ticker, features, stats, gallery, quotes, pricing, faq, final, footer
kindstringtext, long, link, sticker or panel. Decides the content shape.
labelstringHuman name, e.g. "Hero headline"
owneraddress | nullCurrent owner; null if unclaimed
controlleraddress | nullWho can edit right now: the tenant during a rental, else the owner
modestringhuman if content is set, else ai
price.baselamportsFixed price of the piece when unclaimed
price.currentlamportsWhat a buy or take costs right now (advisory)
price.lastlamports | nullWhat the current owner paid
price.lastSalelamports | nullThe most recent buy or take price (same as last today; kept separate for future sale types)
price.multiplierBpsnumberLive asking-price multiplier. 0 = 1×; otherwise 1000 to 40000
price.pendingMultiplierobject | null{ bps, effectiveAt }, a queued multiplier change. bps: 0 = back to 1×
price.decayWeeksnumberFull stale weeks applied to the price (0 to 52)
price.takeSplitobject | null{ owner, treasury } lamports a take would pay right now; null if unclaimed
lastPurchaseTsnumber | nullBlock time of the last buy or take
cooldownUntilnumber | nullUntil when the piece can't be taken; null when not in cooldown
takesnumberHow many times it has been taken
versionnumberIncrements on every accepted event on this piece
listingobject | null{ ratePerDay, since } if listed for rent
pendingListingobject | null{ ratePerDay, effectiveAt }, a queued listing change. ratePerDay: "0" = delisting
rentalobject | null{ tenant, from, until, ratePerDay } during a tenancy
rentFloorPerDaylamports | nullMinimum daily rate the owner could list at right now
quarantinedbooleanModerated by the operator. If true, content is null and the page shows the AI's paint
contentobject | nullHuman content in the shape for kind (see below); null = AI mode
aiobjectThe AI's paint right now: { epoch, level, meme, model, content, style }

Content shapes, by kind:

kindshapenotes
text{ t }up to 80 characters
long{ t }up to 200 characters
link{ t, u }label up to 40; u is an https URL (the AI may use relative links like #faq)
sticker{ e, t? }emoji; optional label up to 24
panel{ s, b, f, k }subject emoji, background name, filter name, sticker array (0 to 4)

ai fields: epoch is the epoch the AI last repainted this piece (not the current epoch); level is the meme level at that repaint; meme is whether it was a meme repaint; model is the persona id (see /lib/palette.js); style may contain font ("impact" | "comic"), rot (degrees), tone ("hot" | "acid" | "ink" | "sky") and wobble (true). An empty style means plain.

Event

One board transaction, accepted or rejected. Fields beyond the common ones appear only when relevant.

FieldTypeMeaning
sigstringTransaction signature. Also the paging cursor.
tsnumberBlock time
blocknumberSolana slot
actionstringbuy | take | edit | price | list | rent | give
slotnumberPiece id
actoraddressFee payer of the transaction
okbooleanAccepted (true) or rejected (false)
versionnumberAccepted only: the piece's version after this event
reasonstringRejected only: why
paidobjectRejected only: { address: lamports } the actor actually sent
pricelamportsbuy / take: price paid. rent: total rent.
fromaddresstake: previous owner
ownerPaidlamportstake: what the previous owner received
modestringedit: human or ai (handed back)
multiplierBpsnumberprice: the requested multiplier
ratelamportslist: the requested daily rate ("0" = delist)
effectiveAtnumberprice / list: when the change applies
daysnumberrent: term length
toaddressgive: recipient
notestringAccepted with a caveat, e.g. "content ignored: links must start with https://"

Events don't include content. Read the memo from the transaction itself, or the piece's current content.

Quote

FieldTypeMeaning
actionstringThe action quoted
slotnumberPiece id
versionnumberThe piece's version the quote was computed against
transfersarray[{ to, lamports }], one entry per recipient. Send at least these amounts, from the fee payer, as top-level System Program transfers.
memostringMemo template, e.g. tinam1:{"a":"take","s":11}
memoProgramaddressThe SPL Memo program id
errorstringOnly on 409 / 503

AI

FieldTypeMeaning
epochnumberCurrent epoch (0 at launch, +1 every 300 s)
epochSecondsnumber300
memeLevelnumber0 to 100: chance (%) a repaint this epoch is a meme
epochStartedAt / nextEpochAtnumberUnix times of this epoch's start and the next repaint
council[]array{ epoch, slot, model, kind, line, text, at }; kind is meme, corp or owned; text is the new words or emoji (null for owned pieces)

Config

See /config above. rules.floor is lamports (string); the multipliers are plain numbers; multiplierBps, rentFloorBpsPerDay and rentFeeBps are basis points (1/100 of a percent).

asOf

FieldTypeMeaning
blockstringLatest Solana slot the indexer has seen ("0" if it has never reached the RPC)
signaturestring | nullNewest treasury signature processed
serverTimenumberServer clock. All derived values (decay, cooldown, pending changes, AI epoch) are computed at this time.
indexedAtnumberWhen the indexer last polled
transactionsnumberBoard transactions replayed (accepted + rejected)
syncedbooleanWhether the indexer has completed at least one poll since it started
errorstring | nullWhy the last poll failed, if it did. Data is the last good state.

Paging through history

Follow nextCursor until it comes back null. The cursor is the signature of the last event on the page; pass it back verbatim. Don't page on ts: events in the same block share it.

const ORIGIN = 'https://funny-site.vercel.app';
let cursor = null;
const all = [];
do {
  const url = new URL(`${ORIGIN}/api/v1/slots/11/events`);
  url.searchParams.set('limit', '200');
  if (cursor) url.searchParams.set('cursor', cursor);
  const page = await (await fetch(url)).json();
  all.push(...page.events);
  cursor = page.nextCursor;
} while (cursor);
console.log(all.length, 'events', all.filter((e) => !e.ok).length, 'rejected');

An unknown cursor returns 400 { "error": "unknown cursor" }.

Building a transaction yourself

A board action is a normal transaction: the transfers from /quote, then one memo instruction, signed by the wallet that pays. The fee payer is the actor, so it must be the same wallet that sends the transfers.

import { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } from '@solana/web3.js';

const ORIGIN = 'https://funny-site.vercel.app';
const connection = new Connection('https://YOUR-RPC-PROVIDER', 'confirmed');
const me = wallet.publicKey; // any wallet adapter / Keypair

// 1. ask for the exact transfers, against fresh chain state
const q = await (await fetch(`${ORIGIN}/api/v1/quote/11?action=take&actor=${me}&fresh=1`)).json();
if (q.error) throw new Error(q.error);

// 2. the memo: the template's fields plus yours
const body = { a: 'take', s: 11, c: { t: 'This is fine. I own this now.' } };
const memo = 'tinam1:' + JSON.stringify(body);
if (new TextEncoder().encode(memo).length > 600) throw new Error('memo too long');

// 3. transfers first, memo last
const tx = new Transaction();
for (const t of q.transfers) {
  tx.add(SystemProgram.transfer({ fromPubkey: me, toPubkey: new PublicKey(t.to), lamports: BigInt(t.lamports) }));
}
tx.add(new TransactionInstruction({
  programId: new PublicKey(q.memoProgram),
  keys: [{ pubkey: me, isSigner: true, isWritable: false }],
  data: Buffer.from(memo, 'utf8'),
}));

// 4. sign and send
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
tx.feePayer = me;
tx.recentBlockhash = blockhash;
const sig = await wallet.sendTransaction(tx, connection);
await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed');

// 5. ask the indexer what it made of it
for (let i = 0; i < 10; i++) {
  const { events } = await (await fetch(`${ORIGIN}/api/v1/slots/11/events?limit=20&fresh=1`)).json();
  const ev = events.find((e) => e.sig === sig);
  if (ev) { console.log(ev.ok ? 'accepted' : `rejected: ${ev.reason}`); break; }
  await new Promise((r) => setTimeout(r, 2000));
}
Solana will execute the transfers even if the rules reject the action. Quote with fresh=1 immediately before signing, keep the time between quote and send short, and check the result. See rejected transactions.

Rules of thumb:

Examples

curl: the 10 most expensive pieces right now

curl -s https://funny-site.vercel.app/api/v1/site \
  | jq -r '.slots | sort_by(.price.current | tonumber) | reverse | .[:10][]
           | "\(.id)\t\(.key)\t\(.price.current | tonumber / 1e9) SOL\t\(.owner // "unclaimed")"'

JavaScript: render the page as text, the way visitors see it

const { slots } = await (await fetch('https://funny-site.vercel.app/api/v1/site')).json();
for (const s of slots) {
  const c = s.content ?? s.ai.content;           // human words win, else the AI's
  const shown = c.t ?? c.e ?? `${c.s} on ${c.b}`;  // text, sticker or panel
  console.log(`#${s.id} ${s.key.padEnd(22)} ${s.mode === 'human' ? 'human' : s.ai.model.padEnd(10)} ${shown}`);
}

JavaScript: watch for takes

let seen = new Set();
setInterval(async () => {
  const { events } = await (await fetch('https://funny-site.vercel.app/api/v1/events?ok=true&limit=50')).json();
  for (const e of events.reverse()) {
    if (seen.has(e.sig)) continue;
    seen.add(e.sig);
    if (e.action === 'take') console.log(`${e.actor} took #${e.slot} for ${Number(e.price) / 1e9} SOL; ${e.from} got ${Number(e.ownerPaid) / 1e9}`);
  }
}, 10_000);

Python: a wallet's portfolio

import requests

BASE = "https://funny-site.vercel.app/api/v1"
addr = "7aXbQ1sZcV2nM4kP9rT6yU8wE3qL5jH1gF2dS4aZ6xCv"

me = requests.get(f"{BASE}/owners/{addr}", timeout=10).json()
site = requests.get(f"{BASE}/site", timeout=10).json()
slots = {s["id"]: s for s in site["slots"]}

for i in me["owned"]:
    s = slots[i]
    paid, now = int(s["price"]["last"]), int(s["price"]["takeSplit"]["owner"])
    print(f'#{i} {s["label"]}: paid {paid / 1e9:.4f} SOL, a take now pays you {now / 1e9:.4f} SOL')

print("spent", int(me["spent"]) / 1e9, "SOL; received from takes", int(me["receivedFromTakes"]) / 1e9, "SOL")

Python: is a piece takeable right now?

import requests, time

BASE = "https://funny-site.vercel.app/api/v1"
r = requests.get(f"{BASE}/quote/11", params={"action": "take", "actor": "YOUR_ADDRESS", "fresh": 1}, timeout=10)
if r.status_code == 200:
    total = sum(int(t["lamports"]) for t in r.json()["transfers"])
    print("takeable for", total / 1e9, "SOL")
else:
    print("not now:", r.json()["error"])

Rate limits, caching & CORS

Errors

Errors are JSON with an error string, and are never cached.

StatusWhen
400Bad input: unknown cursor, not a Solana address, invalid JSON or disallowed method on /api/rpc
404Unknown route or piece id. The body lists the endpoints.
405Non-GET on /api/v1, non-POST on /api/rpc
409/quote: the action isn't allowed right now. error is the rule that would reject it.
429Rate limited. Wait for Retry-After seconds.
502Unexpected server or upstream failure
503/quote: the indexer is lagging, so it won't quote. Retry shortly.

What is and isn't stable

/api/v1 is a versioned surface: fields get added, existing ones don't change meaning or disappear. Piece ids and keys are permanent. The memo format tinam1 is permanent; a new format would get a new tag. /api/rpc exists for this site's board and may change its method list.

Anything else under /api/ is internal plumbing and will change without warning. If you found an endpoint that isn't on this page, it isn't for you to build on.

Moderated content goes through the same gate as the page: a quarantined piece reports quarantined: true and content: null. There is no way to read suppressed content through this API, which is deliberate. It is still in the public memo on Solana.

Read-only and free. If you build something with it, we'd love to see it.

open the boardread the docs