Your first game
A real, playable game — from a blank file to the feed — in five steps. Copy each block, run it, change it. No build step, no framework, one HTML file.
- ~15 minutes
- One HTML file
- Ends published
A TipTap game is a single self-contained HTML file that runs in one panel of a swipeable feed. The platform injects window.TipTap for you — there is nothing to install and nothing to import. We'll build Tap Rush: tap the dot before the clock runs out. Each step is a small change on the one before it.
A page that plays
Start with a game and no platform at all — a canvas, a loop, and a dot you can tap. Paste this into a file called
index.htmland open it in a browser. It runs anywhere; the platform is just where it will live.index.html — a complete, runnable game
<!doctype html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Tap Rush</title> <style> html, body { margin: 0; height: 100%; background: #12101c; overflow: hidden; touch-action: none; } canvas { display: block; } </style> </head> <body> <canvas id="c"></canvas> <script> // window.TipTap is injected for you on the platform — no import, no build step. const canvas = document.getElementById("c"); const ctx = canvas.getContext("2d"); let W, H; function fit() { W = canvas.width = innerWidth; H = canvas.height = innerHeight; } fit(); addEventListener("resize", fit); let score = 0; let target = spawn(); function spawn() { return { x: 40 + Math.random() * (innerWidth - 80), y: 90 + Math.random() * (innerHeight - 180), r: 34, }; } // Tap the dot: score goes up and it jumps somewhere new. canvas.addEventListener("pointerdown", (e) => { const dx = e.clientX - target.x; const dy = e.clientY - target.y; if (dx * dx + dy * dy < target.r * target.r) { score++; target = spawn(); } }); function frame() { ctx.clearRect(0, 0, W, H); ctx.fillStyle = "#7c5cff"; ctx.beginPath(); ctx.arc(target.x, target.y, target.r, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#fff"; ctx.font = "bold 28px system-ui, sans-serif"; ctx.fillText(String(score), 20, 46); requestAnimationFrame(frame); } frame(); </script> </body> </html>✓ You have a game that runs. No SDK yet — that's next.
Keep score
Now put it on the board. Add a countdown so a run ends, then two calls:
updateScoreshows a live score in the platform's top bar during play, andsubmitScorewrites the final number to the leaderboard.submitScoreis silent — it shows no UI of its own, so we pass{ showLeaderboard: true }to raise the board when the run is over.The changes — a clock, and the two score calls
let time = 15; // seconds on the clock let over = false; let last = performance.now(); function endGame() { over = true; // Silent write to the leaderboard, and raise it in the same call. TipTap.submitScore(score, { showLeaderboard: true }); } // In your pointerdown handler, after score++: TipTap.updateScore(score); // live score in the platform's top bar // In frame(), before you draw — advance the clock: const now = performance.now(); time -= (now - last) / 1000; last = now; if (time <= 0 && !over) endGame(); // ...and draw the timer next to the score: ctx.fillText(Math.max(0, Math.ceil(time)) + "s", W - 70, 46);✓ Your score lands on the leaderboard. See Scores & leaderboards for challenges and daily seeds.
React to the result
When
submitScoreis processed,onResulthands you the standing this run earned — rank, percentile, whether it was a personal best. Show it, celebrate a best, and let the player go again when they close the board.Register these once, near the top of your script
TipTap.onResult((r) => { // Fires after submitScore is processed, with the standing this run earned. message = "Rank #" + r.rank + " — top " + Math.round(r.percentile) + "%"; if (r.personalBest) message = "New best! " + message; }); // Let the player go again when they close the leaderboard. TipTap.onLeaderboardClose(() => { score = 0; time = 15; over = false; last = performance.now(); target = spawn(); });✓ The round-trip works: play → submit → result → play again.
Behave in the feed
Your game lives in a feed the player swipes through, so it has to be a good citizen when it is not the one on screen. The platform pauses your game as it scrolls away — honour that so the clock doesn't drain in the background — and never play a sound unless
canPlayAudio()says you may.Pause with the platform; gate your audio
let running = true; // The platform pauses your game when it scrolls out of the feed. Respect it: // stop advancing the clock and the loop, resume exactly where you were. TipTap.onPauseChange((paused) => { running = paused ? false : true; }); // At the top of frame(), before anything moves: if (!running) { requestAnimationFrame(frame); return; } // And before you ever play a sound: if (TipTap.canPlayAudio()) hitSound.play();✓ It pauses when swiped away and resumes where it left off. Full detail on Game lifecycle.
Publish it
You have a real game. To get it into the feed:
- Go to Upload a game and drop in your HTML file.
- Set a realistic
scoreMax— the highest score a legitimate run could reach. A submission above it is refused, which is how the leaderboard stays honest. - Send it for review. A human checks every game before it reaches the feed — how review works walks through what they look for and how long it takes.
✓ It's in the queue. Once approved, it's live in the feed.
Where to go next
You now know the shape of a TipTap game. Everything else is going deeper.
- The game document → the feed model and what's injected, in full.
- Cookbook → complete prompts that validate first time.
- SDK reference → every call, with examples, when you need one.
- Build with your agent → have an AI write it against these same docs.

