Documentation
The manual for games that live in the feed.
How one sentence turns into a hosted game, what the window.feedcade SDK hands every build, the limits that keep rooms quick, the HTTP endpoints underneath, and what happens on chain when you back a game with a coin.
Start
Overview#
feedcade writes a whole browser game from a short description, serves it from an isolated origin, and gives you a link that X unfolds into a card people can play without leaving the timeline.
A single file
Each game is one HTML document with its styles and scripts inline. There is nothing to install and nothing to bundle.
Shared by default
Every game owns a room: a JSON key-value store plus a live list of who is playing, the same for every visitor.
Coin optional
Tie a pump.fun coin to a game whenever you want. Its creator fees can be split between wallets on chain.
Start
Quickstart#
Everything happens in the browser. You never touch a build tool or a server.
- 1
Write a sentence
Describe the game on the home page and press build. The HTML streams into the studio as the model writes it, and the preview starts the moment it’s saved.
- 2
Tweak it
Ask for changes in plain language, one at a time. Each build is stored as a version you can go back to, and when the preview throws an error the studio offers to send it back as a fix request.
- 3
Post the link
Copy the share link (
feedcade.xyz/s/…) into a post. X reads its player card and shows the game itself instead of a plain preview. - 4
Back it (optional)
In the studio,
Back it with a coinlaunches a pump.fun coin tied to the game. One wallet approval covers the whole launch.
<!doctype html>
<html>
<body>
<button id="five">high five</button>
<p id="count">0 high fives so far</p>
<script>
// window.feedcade already exists by the time this runs.
const label = document.getElementById('count')
feedcade.room.subscribe((state) => {
label.textContent = (state.fives ?? 0) + ' high fives so far'
})
document.getElementById('five').onclick = () => {
const now = feedcade.room.get('fives') ?? 0
feedcade.room.set('fives', now + 1)
}
</script>
</body>
</html>Open that page in two tabs and press the button in one: the count changes in both. That’s the whole multiplayer model. The SDK keeps one shared object in sync and calls you when it changes.
Start
Writing prompts#
The builder already knows the card size, the SDK and where the badge sits. Spend your words on the game: the goal, the controls, and the part everyone shares.
prompt
A one-button lighthouse game: tap to sweep the beam and catch ships before they hit the rocks. Shared top-10 board with names.
prompt
A 32×32 mural everyone paints together. Eight colors, tap to fill a cell, strokes from other people appear live.
prompt
A daily five-letter word with six tries. Show how many visitors cracked today’s word and the average number of guesses.
prompt
Tug of war between team sun and team moon. Every tap pulls the rope; first side past the line takes the round, then it resets.
- Say what’s shared. A leaderboard, a canvas, a tally, a crown. Naming it is enough for the builder to put it in the room.
- Give it a mood. “Pastel and slow”, “CRT green, frantic” or “bubbly with lots of particles” shapes the result more than a list of features.
- Change one thing per follow-up. Small, specific requests keep what already works intact.
SDK
window.feedcade#
The host adds one small global to every game before any of its own scripts run. There is nothing to import and no key to paste.
| Member | Type | What it gives you |
|---|---|---|
| feedcade.site | string | This game’s id. The same id names its room and its share link. |
| feedcade.player | { id, name, color } | The person playing, remembered on this device. |
| feedcade.setName(name) | string | Renames the player (trimmed, at most 24 characters) and returns the name kept. |
| feedcade.inX | boolean | true when the game is running inside a frame, such as a post. |
| feedcade.room | object | The shared key-value state every visitor reads and writes. |
| feedcade.players | object | Who has the game open right now. |
window.feedcade as read-only. Don’t define your own object with that name; the runtime owns it.SDK
Player#
On a first visit the runtime picks a random id, a guest name and a color, and stores them on that device. Coming back later keeps the same identity.
feedcade.player
// { id: "q3m8x1ka", name: "player-q3m8", color: "#7dd3fc" }
// Let people choose what the leaderboard calls them.
nameInput.onchange = () => {
nameInput.value = feedcade.setName(nameInput.value)
}
// The card in a post is small, so tighten the layout there.
if (feedcade.inX) document.body.classList.add('in-post')SDK
Room state#
One JSON key-value store per game. A write shows up on your own screen at once and reaches every other open copy on the next sync, usually within a second.
// Resolves after the first pull, with everything in the room.
const state = await feedcade.room.ready
feedcade.room.get('round') // one key, from the local copy
feedcade.room.all() // a copy of every key
feedcade.room.set('round', { n: 3, startedAt: Date.now() }) // any JSON up to 8 KB
feedcade.room.remove('round') // same as set('round', null)
// Runs right away with the current state, then on every change.
const stop = feedcade.room.subscribe((state, changedKeys) => {
if (changedKeys.includes('round')) drawRound(state.round)
})
stop() // unsubscribe| Method | Returns | Notes |
|---|---|---|
| room.ready | Promise<state> | Settles once the first pull finishes (or fails, with whatever is known). |
| room.get(key) | value | undefined | Reads the local copy. No request is made. |
| room.all() | object | A deep copy of every key, safe to mutate. |
| room.set(key, value) | Promise<{ ok, rev }> | Updates locally first, then writes. Passing null deletes the key. |
| room.remove(key) | Promise<{ ok, rev }> | Shorthand for set(key, null). |
| room.subscribe(fn) | () => void | fn(state, changedKeys) runs immediately and after every change. Call the return value to stop. |
Each key keeps the latest write
If two players write the same key at the same moment, one of the writes is lost. Give every player a key of their own, such as'score:' + feedcade.player.id, and combine those keys when you draw.SDK
Presence#
The players list shows who has this game open right now, with the name and color each person picked.
feedcade.players.subscribe((people) => {
here.textContent = people.length === 1 ? 'just you' : people.length + ' here'
dots.replaceChildren(...people.map((p) => avatar(p.name, p.color)))
})
feedcade.players.list() // the last list received, no requestA visitor counts as present while their copy of the game is syncing with a subscriber attached. They drop off 20 s after their last sync. Lists hold up to 50 people, most recently seen first. Board thumbnails load with ?thumb=1 and never show up as players.
SDK
Recipes#
Patterns the builder reaches for most often. Each one keeps writes per player so nobody overwrites anybody else.
// One key per player: only your own best score is ever written.
function submit(score) {
const key = 'best:' + feedcade.player.id
const prev = feedcade.room.get(key)
if (prev && prev.score >= score) return
feedcade.room.set(key, { name: feedcade.player.name, score })
}
feedcade.room.subscribe((state) => {
const top = Object.entries(state)
.filter(([k]) => k.startsWith('best:'))
.map(([, v]) => v)
.sort((a, b) => b.score - a.score)
.slice(0, 10)
renderBoard(top)
})SDK
Local storage#
Settings that only matter on this device belong in localStorage. Anything another player should see belongs in the room.
localStorage.setItem('sound', 'off')
localStorage.getItem('sound') // "off"
// Some sandboxes refuse storage. When that happens the runtime puts an
// in-memory stand-in in its place: calls still work, nothing survives a reload.| Keep it in | Good for |
|---|---|
| localStorage | Mute switch, control scheme, “tutorial seen”, this device’s own best. |
| feedcade.room | Scores, votes, shared canvases, turns, anything other people need to see. |
SDK
Limits#
Room for real games, with enough headroom that every room stays fast.
| Limit | Value |
|---|---|
| Key length | 1–80 characters |
| Value size | 8 KB of JSON |
| Keys per room | 5,000 |
| Room writes | 40 per 10 s, per visitor, per game |
| Changes per pull | 1,000 (more: true means ask again) |
| Sync interval | ~0.9 s subscribed · 4 s idle · paused in hidden tabs |
| Presence | 20 s window · up to 50 listed |
| Builds | 30 per hour, per visitor |
Platform
Playing inside X#
X plays a web page inside a post when the link’s page carries player card tags. Every share page sets them for you.
<meta name="twitter:card" content="player">
<meta name="twitter:player" content="https://feedcade.xyz/p/<id>">
<meta name="twitter:player:width" content="480">
<meta name="twitter:player:height" content="480">
<meta name="twitter:image" content="https://feedcade.xyz/api/card/<id>">| URL | What it serves |
|---|---|
| /s/<id> | The share page. Post this one: it carries the card and shows the game at full size. |
| /p/<id> | The player X frames: the game’s HTML with the runtime added first. |
| /p/<id>?thumb=1 | Preview mode for boards: no splash, no presence, no play counted. |
| /p/<id>?v=N | An earlier saved version, as listed in the game’s history. |
| /api/card/<id> | The still image X shows before someone taps play. |
Design for a 480 × 480 square
The card is square and small. Scale your stage tomin(100vw, 100vh) so the same game also fills a phone or a desktop window on the share page, and make every control work with a thumb.Platform
Sandbox & badge#
Games run where they can’t reach anything that matters, and the host frames them with a short intro and a corner badge.
The player is served with a sandbox allow-scripts content security policy and without same-origin access, so each game gets an opaque origin. It can’t read cookies or storage from this site or from X. Its frame-ancestors list only allows X and feedcade itself to embed it.
Before the game appears, a feedcade splash shows for about 1.5 seconds; a tap skips it. A small badge then stays in the bottom-right corner, linking to the game page and, once the game has a coin, to its ticker. Both live outside the game’s own code, so a game can’t restyle or hide them.
Leave the bottom-right corner empty
Buttons or score text placed there end up under the badge. The builder already avoids that area; keep it in mind when you ask for layout changes.Platform
Models#
Pick the model in the composer. Any follow-up can switch to a different one, and the history records which model wrote each version.
| Model | Lab | Strength | |
|---|---|---|---|
| Claude Sonnet 5.5 | Anthropic | Polished, playable games that hold up on the first build. | default |
| GPT-5.6 Sol | OpenAI | Careful with rules, scoring and physics that add up. | Strong game logic |
| Kimi K2.7 Code | Moonshot AI | Quick, tidy builds for small games and tools. | Fast coder |
| Gemini 3.8 Flash | The quickest first draft, made for rapid tweaks. | Quickest | |
| GLM 5.3 | Z.ai | Loud colors, bouncy motion and juicy effects. | Playful visuals |
Platform
Versions & remixes#
No accounts. The browser that creates a game holds its edit key; everyone else can play it or start their own copy.
edit key
Ownership lives in your browser
Creating a game returns a token once. The studio keeps it locally and sends it with every change. Lose it and the game stays playable, but nobody can edit it.
history
Every build is a version
Versions are numbered and kept. Open any of them with ?v=N, and restore one to make it current again without losing the others.
remix
Fork anything
Remix copies a game’s current HTML into a new game with a fresh id and your own edit key. The original is untouched.
Coins
Backing a coin#
Any game can carry one pump.fun coin, launched from its studio whenever you’re ready. The game becomes the coin’s website and the coin’s ticker joins the game’s badge.
- 1
Open the studio
On your game, choose
Back it with a coin. - 2
Name it
Name (up to 32 bytes), ticker (up to 10 characters), a description and an image. If you skip the image, generated coin art is used.
- 3
Choose who earns
Keep every creator fee for yourself or split them across up to 10 wallets.
- 4
Add a first buy (optional)
A dev buy goes in the same transaction as the launch, so it lands before anyone else can trade.
- 5
Approve once
Phantom or Solflare asks for a single approval that covers the coin, the fee split and the optional buy.
Coins
Fee splits#
Creator fees are routed by pump.fun’s own fee-sharing program, so payouts happen on chain with no feedcade server in the loop.
[
{ "wallet": "7fUq…you", "bps": 6000 }, // 60%
{ "wallet": "Hx2m…artist", "bps": 2500 }, // 25%
{ "wallet": "3kPa…prizes", "bps": 1500 } // 15%
]
// Up to 10 wallets. The bps must total exactly 10,000.A split can’t be edited later
The launch transaction revokes the split’s admin, so nobody, including you, can add, remove or reweight wallets afterwards. Check addresses before you approve.Coins
Coin pages#
Each backed coin gets a page at /coin/<mint> with a live chart, recent trades and a buy and sell panel.
On the curve
While the coin is still on its bonding curve, trades go straight to pump.fun’s program.
After graduation
Once it graduates, buys and sells are routed through Jupiter for the best available price.
Checked before you sign
The server builds each trade and verifies every program it touches against an allowlist. Your wallet signs.
Reference
Room API#
The same two endpoints the SDK calls. Public, CORS-enabled, no key required.
# Everything on the first call, then only what changed after a rev
curl "https://feedcade.xyz/api/room/pixel-wall?since=0"
# Write a single key (any JSON up to 8 KB; null deletes it)
curl -X POST "https://feedcade.xyz/api/room/pixel-wall" \
-H "content-type: application/json" \
-d '{"key":"px:4,7","value":"#fde047"}'Response
{
"rev": 842,
"changes": {
"px:4,7": "#fde047",
"best:q3m8x1ka": { "name": "player-q3m8", "score": 31 },
"px:0,0": null
},
"more": false,
"players": [
{ "id": "q3m8x1ka", "name": "player-q3m8", "color": "#7dd3fc" }
]
}| Field | Meaning |
|---|---|
| rev | The room’s current revision. Send it back as since to receive only newer changes. |
| changes | Keys changed after since. null means the key was deleted. With since=0 only live keys are returned. |
| more | true when more than 1,000 changes are waiting. Request again with the new rev. |
| players | Only included when you identify yourself with p, n and c (player id, name and color). Doing so also counts you as present. |
A POST takes {"key": string, "value": any JSON} and answers {"ok": true, "rev": n}.
Try it
This calls the real room behind the demo pixel wall. The first send returns every key that has a value. The box then moves since to the returned rev, so the next send only brings back what changed in between. Paint a square on the wall in another tab and send again to watch it arrive.
Reference
Games API#
Create, build, read and fork games over HTTP. Changing a game needs the edit key returned when it was created.
| Call | Send | Get back |
|---|---|---|
| POST /api/sites | { prompt, model } | 201 with { id, token }. The token is only shown this once. |
| POST /api/sites/:id/generate | x-site-token header, body { prompt, model? }. The first build may leave out the prompt and reuse the one it was created with. | The HTML as text/plain while it’s written, ending with a marker line: <!--feedcade:saved:vN--> or <!--feedcade:error:…-->. |
| GET /api/sites/:id | nothing | { site: { id, title, prompt, html, version, model, plays, … }, versions: [{ version, prompt, model, createdAt }] } |
| POST /api/sites/:id/remix | nothing | { id, token } for the new copy. |
| POST /api/sites/:id/restore | x-site-token header, body { version } | Makes that version current again. |
| GET /api/sites | sort, limit, offset | A page of games: new, popular or mcap. |
const base = 'https://feedcade.xyz'
// 1. create the game and keep the edit key
const { id, token } = await fetch(base + '/api/sites', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ prompt: 'a two-player reaction duel', model: 'anthropic/claude-sonnet-5.5' }),
}).then((r) => r.json())
// 2. build it; the body streams back as the model writes
const res = await fetch(base + '/api/sites/' + id + '/generate', {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-site-token': token },
body: '{}',
})
const text = await res.text()
const end = text.match(/<!--feedcade:(saved|error):(.*?)-->/)
console.log(end[1] === 'saved' ? base + '/s/' + id : 'failed: ' + end[2])Reference
Coin data API#
Market data for coins backed on feedcade. Mints that were never paired with a game here answer 404.
# The coin behind a game (each field is null until it exists)
curl "https://feedcade.xyz/api/sites/<id>/coin"
# -> {"coin": {...} | null, "market": {...} | null, "fees": {...} | null}
# Candles for the chart
curl "https://feedcade.xyz/api/coins/<mint>?kind=candles&tf=1h"Reference
Errors#
Every failure is JSON shaped like {"error": "what went wrong"}, with a status that tells you what to do next.
| Status | When |
|---|---|
| 400 | The body is malformed, a prompt is missing, a key is outside 1–80 characters, or a timeframe is unknown. |
| 403 | The x-site-token header is missing or doesn’t match the game. |
| 404 | No game with that id, or a mint that isn’t backed on feedcade. |
| 413 | The value is larger than 8 KB of JSON. |
| 429 | Too many room writes or builds. Back off for a moment and retry. |
| 502 | An upstream service (pump.fun, Jupiter or the model provider) didn’t answer properly. |
| 507 | The room already holds 5,000 keys. Delete some to make space. |
Reference
FAQ#
Do I need to know how to code?
Who is allowed to change my game?
Is room data private?
Why does my link show a plain preview on X?
Can a game load libraries?
Can I change the fee split after launch?
Does every game need a coin?
Got an idea for a game?
Describe it in a sentence and it can be playing in a post in a few minutes.