Everything you need to build apps that live inside posts.
How embedpad turns a sentence into a multiplayer app, the SDK every app gets, the limits, the REST API, and how coins and fee splits work on chain.
Overview
embedpad builds a complete web app from a description, hosts it in a sandbox, and gives you a share link that X renders as a playable card right inside the post.
- 1Describe itType what you want on the home page and press build. The builder streams the app as it writes.
- 2IterateAsk for changes in plain words. Every version is saved and can be restored. Runtime errors show a one-click
Fix it. - 3Share the linkCopy the share link (
www.embedpad.app/s/…) and post it on X. The post unfurls into the live app. - 4Pair a coin (optional)Hit
Launch a coinin the builder to create a pump.fun coin paired with the app, in one wallet approval.
<!doctype html>
<html>
<body>
<button id="btn">0 taps</button>
<script>
// The runtime injects window.embedpad before your code runs.
const btn = document.getElementById('btn')
embedpad.room.subscribe((state) => {
btn.textContent = (state.taps ?? 0) + ' taps'
})
btn.onclick = () => {
embedpad.room.set('taps', (embedpad.room.get('taps') ?? 0) + 1)
}
</script>
</body>
</html>Writing good prompts
The builder already knows the canvas size and the SDK. Spend your words on what makes the app fun.
A tap-to-flap bird game. Everyone who opens the post shares one global high score with their name on it.
A 24×24 pixel wall. Pick a color, click to paint, and see everyone else painting live.
A poll with three options and live animated bars that update as people vote.
A reaction-time test. Show the fastest 10 times of everyone who played, today only.
- Say what should be shared (a high score, a canvas, votes) and the builder wires up the room for you.
- Name the feel: colors, pace, sound. “Arcade, neon, fast” goes a long way.
- Keep follow-ups small and specific. One change per message gives the cleanest results.
window.embedpad
The runtime injects a tiny global into every app before your code runs. No imports, no keys.
| Member | Description |
|---|---|
| embedpad.site | The app id. |
| embedpad.player | This visitor: { id, name, color }. |
| embedpad.setName(name) | Change the display name (max 24 characters). |
| embedpad.inX | true when framed (inside a post or any iframe). |
| embedpad.room | Shared key-value state for everyone with the link. |
| embedpad.players | Who has the app open right now. |
window.postware is an alias kept for older apps. New code should use window.embedpad.embedpad.player
// → { id: "k2v9x0qa", name: "guest-k2v9", color: "#00ba7c" }
// Let people pick a display name (max 24 characters).
embedpad.setName(input.value)
// true when running inside a post (or any iframe)
if (embedpad.inX) document.body.classList.add('compact')Room state
One shared JSON key-value store per app. Writes show up locally right away and reach everyone else within about a second.
// Wait for the first sync before reading.
const state = await embedpad.room.ready
embedpad.room.get('score') // one key
embedpad.room.all() // a copy of every key
await embedpad.room.set('score', 42) // any JSON value, up to 8 KB
await embedpad.room.remove('score') // same as set(key, null)
// Called right away with the current state, then on every change.
const stop = embedpad.room.subscribe((state, changedKeys) => {
if (changedKeys.includes('score')) draw(state.score)
})
stop() // unsubscribe| Method | Returns | Notes |
|---|---|---|
| room.ready | Promise<state> | Resolves after the first sync. |
| room.get(key) | value | undefined | Reads the local copy, no network. |
| room.all() | object | A shallow copy of every key. |
| room.set(key, value) | Promise | Optimistic. null deletes the key. |
| room.remove(key) | Promise | Same as set(key, null). |
| room.subscribe(fn) | () => void | fn(state, changedKeys). Runs once immediately. |
'score:' + player.id) and combine them when you render.Presence
Know who else is here. A visitor counts as present while they've synced in the last 20 seconds.
embedpad.players.subscribe((people) => {
counter.textContent = people.length + ' here now'
avatars.innerHTML = people
.map((p) => '<span style="background:' + p.color + '">' + p.name[0] + '</span>')
.join('')
})
embedpad.players.list() // last known list, no network callPresence is only sent while something is subscribed, and never from preview thumbnails, so gallery views don't inflate counts.
// One key per player avoids two people overwriting the same list.
function submit(score) {
const key = 'score:' + embedpad.player.id
const best = embedpad.room.get(key)?.score ?? 0
if (score > best) {
embedpad.room.set(key, { name: embedpad.player.name, score, at: Date.now() })
}
}
embedpad.room.subscribe((state) => {
const top = Object.entries(state)
.filter(([k]) => k.startsWith('score:'))
.map(([, v]) => v)
.sort((a, b) => b.score - a.score)
.slice(0, 10)
render(top)
})// localStorage works even inside the sandbox. If the browser blocks it,
// the runtime swaps in an in-memory copy so your code never throws.
localStorage.setItem('muted', '1')
// Per-device settings → localStorage
// Anything other people should see → embedpad.room| Limit | Value |
|---|---|
| Key length | 1–80 characters |
| Value size | 8 KB of JSON |
| Keys per room | 5,000 |
| Writes per visitor | 40 per 10 s, per app |
| Sync interval | ~0.9 s subscribed · 4 s idle · paused in hidden tabs |
| Presence window | 20 s · up to 50 listed |
| Builds | 30 per hour per visitor |
Playing inside X
X renders a web page inside a post when the link carries a player card. Your share page sets it for you.
<meta name="twitter:card" content="player">
<meta name="twitter:player" content="https://www.embedpad.app/p/k3n7q2xw9p">
<meta name="twitter:player:width" content="480">
<meta name="twitter:player:height" content="480">
<meta name="twitter:image" content="https://www.embedpad.app/api/card/k3n7q2xw9p">| URL | What it is |
|---|---|
| /s/<id> | Share page. Post this link. Humans see the app page, X sees the card. |
| /p/<id> | The player X frames at 480×480. Your HTML plus the runtime. |
| /api/card/<id> | Preview image shown before the player loads. |
Sandbox & branding
Player pages are served with a sandbox allow-scripts CSP, which gives every app an opaque origin. Apps can't read cookies, your account or this site, and can only be framed by X and embedpad.
Each app opens with a short embedpad splash (tap to skip) showing the title and paired ticker, and keeps a small badge in the bottom-right that links to the app page and its coin.
| Model | Good at |
|---|---|
| Claude Sonnet 5.5 | Best all-rounder |
| GPT-5.6 Sol | Strong at game logic |
| Kimi K2.7 Code | Fast coder |
| Gemini 3.8 Flash | Quickest |
| GLM 5.3 | Playful visuals |
Pairing a coin
A real pump.fun coin, created from your own wallet and permanently linked to your app.
- 1Name itName, ticker, description and image. The default image is coin art with your ticker.
- 2Choose the fee splitAll creator fees to you, or split them between up to 10 wallets.
- 3Optional dev buyBuy in the same transaction as the launch, so nobody can buy before you.
- 4Approve onceOne wallet approval signs everything. The coin’s website is your app, and the ticker shows on the app badge.
Fee splits
Creator fees route through pump.fun's native fee sharing, so payouts happen on chain without us.
[
{ "address": "YourWa11et…", "bps": 7000 }, // 70%
{ "address": "Co11abWa11et…", "bps": 2000 }, // 20%
{ "address": "Charity…", "bps": 1000 } // 10%
]
// Up to 10 wallets. bps must add up to exactly 10,000.Token pages
Every paired coin gets a page at /coin/<mint> with a live chart, trades, and buy/sell.
Trades are built on the server and signed in your wallet. Coins on the bonding curve trade on pump.fun directly; graduated coins route through Jupiter. Each transaction is checked against an allowlist of programs before it's sent.
/api/room/:id?since=:revstate & presence/api/room/:idwrite one key# Full state on the first call, then only changes after a given rev
curl "https://www.embedpad.app/api/room/k3n7q2xw9p?since=0"
# Write one key (any JSON value, up to 8 KB)
curl -X POST "https://www.embedpad.app/api/room/k3n7q2xw9p" \
-H "content-type: application/json" \
-d '{"key":"score","value":42}'Response
{
"rev": 531,
"changes": {
"score": 42,
"p:4,2": "#ffd400",
"old-key": null
},
"more": false,
"players": [
{ "id": "k2v9x0qa", "name": "guest-k2v9", "color": "#00ba7c" }
]
}| Field | Description |
|---|---|
| rev | Room revision. Pass it back as since to get only newer changes. |
| changes | Keys changed since since. null means the key was deleted. |
| more | true when more than 1,000 changes are waiting. Request again. |
| players | Present only when p, n and c (id, name, color) are sent. |
Try it
www.embedpad.app/api/room/pixel-wall?since=0This hits the real public room for the demo pixel wall. Send once to get the full state, then send again to get only what changed.
Apps API
/api/sitescreate/api/sites/:id/generatex-site-token/api/sites/:idapp + versions/api/sites/:id/coinpaired coin# Start a new app. Keep the token: it's the only way to edit it.
curl -X POST "https://www.embedpad.app/api/sites" \
-H "content-type: application/json" \
-d '{"prompt":"A shared pixel wall","model":"anthropic/claude-sonnet-5.5"}'
# → 201 { "id": "k3n7q2xw9p", "token": "…" }
# Build it (streams the HTML as plain text while it writes)
curl -N -X POST "https://www.embedpad.app/api/sites/k3n7q2xw9p/generate" \
-H "x-site-token: $TOKEN" \
-H "content-type: application/json" \
-d '{"prompt":"Make the colors pastel"}'curl "https://www.embedpad.app/api/sites/k3n7q2xw9p"
# → { "site": { "id", "title", "html", "version", "model", "views", … },
# "versions": [{ "version": 3, "prompt": "…", "createdAt": "…" }] }
curl "https://www.embedpad.app/api/sites/k3n7q2xw9p/coin?fees=1"
# → { "coin": { "mint", "symbol", "name", "feeSplit", … } | null,
# "market": { "mcapUsd", "complete", … } | null,
# "fees": { "status", "shareholders", … } | null }/api/coins/:mint?kind=market|candles|tradesMINT=...pump
curl "https://www.embedpad.app/api/coins/$MINT" # market + 24h stats
curl "https://www.embedpad.app/api/coins/$MINT?kind=candles&tf=5m" # 1m 5m 15m 1h 4h 1d
curl "https://www.embedpad.app/api/coins/$MINT?kind=trades" # latest trades| Status | When |
|---|---|
| 400 | Bad body, missing prompt, key outside 1–80 characters, unknown timeframe. |
| 403 | Missing or wrong x-site-token. |
| 404 | Unknown app, or a mint that isn’t paired here. |
| 413 | Value larger than 8 KB. |
| 429 | Too many writes or builds. Wait and retry. |
| 502 | pump.fun or Jupiter didn’t answer. |
| 507 | Room is full (5,000 keys). Delete keys to make space. |
FAQ
Do I need to write code?
No. Describe the app and ask for changes in plain words. The SDK reference is here for when you want to read or edit the HTML yourself.
Does it really play inside the post?
Yes, on X's web and apps that support player cards. X frames the app at 480×480. Where player cards aren't supported, the post shows the preview image and links to the app.
Who can edit my app?
Only the browser that created it. The edit token lives in that browser. Anyone can remix a copy, which gets its own token.
Is room data private?
No. Room state is public to anyone who has the app link. Never store secrets, emails or anything personal in it.
Can I change the fee split later?
No. pump.fun locks the split after it is set, so double-check wallets and percentages before launching.
Do I have to pair a coin?
No. Apps work fine on their own. A coin is optional and can be added any time from the builder.
embedpad