How to Earn $15,000 from Browser Games: Claude Fable 5 + Claude Code + Poki/CrazyGames

On Poki and CrazyGames you earn not by selling the game, but from the ads that players see. The platform runs ads around and inside the game and shares the revenue. With Poki it's 50/50 when the player came through Poki.com or their marketing. With CrazyGames the exact rate is not publicly announced and depends on various factors, but there's a trick: two months of launch exclusivity raises your share by roughly 50%.
# Game: Star Dodger (working title)
## Platform and stack
- Pure HTML5 + CSS + vanilla JavaScript. No frameworks or bundlers.
- Structure: index.html, game.js, (optional) assets in /assets with RELATIVE paths.
- IMPORTANT (Poki requirement): no external requests in production — no Google Fonts,
no external CDNs (jsDelivr, etc.), no external images/sounds. Everything local.
- IMPORTANT (Poki requirement): works in incognito mode — wrap any access to localStorage
in try/catch.
- Keep the initial load size small (Poki requires < 8 MB).
- Works offline from a local folder and inside an iframe on the portal.
## Genre and mechanics
- Hyper-casual dodging arcade.
- The player controls a circle at the bottom of the screen.
- Objects fall from above: red = death, yellow = +points.
- Goal: survive as long as possible; the score grows over time.
## Controls (BOTH are MANDATORY)
- Desktop: arrow keys ← →, A/D, mouse.
- Mobile/tablet: touch (pointer events), the finger drags the player horizontally.
## Session economy
- Session length: target 2–5 minutes. Fast restart without reloading the page.
- Progression: rising difficulty (object speed/frequency) based on score.
## Ads — via an abstract window.Ads interface (do NOT tie it to a portal!)
Methods:
- Ads.loadingFinished()
- Ads.gameplayStart() // call on the player's FIRST input, not on load
- Ads.gameplayStop() // on any pause/death/menu
- Ads.interstitial(): Promise
- Ads.rewarded(): Promise<boolean> // true if the video was watched to the end
By default Ads is a stub (logs + reward=true). I'll plug in the portal implementations later.
Apply: rewarded() for "continue after death" (once per run) and "double the score";
interstitial() before a new run.
## Technical requirements
- Responsive canvas, devicePixelRatio support (cap ×2). Fast first frame.
- Clean, commented code, with explicit spots marked for integration.
## Deliverables
- A working game + instructions for running it locally.
- The final build in /dist: index.html at the root, relative paths, no external domains.The Core Formula
Revenue ≈ (number of game sessions) × (revenue per 1,000 sessions).
Revenue per 1,000 sessions (RPM on the developer's side, already after the platform's cut) is not officially disclosed by the portals, and many developers are under NDA, so the figures below are a planning estimate, not a guarantee:
| Game quality | RPM estimate | Sessions to reach $15,000 |
|---|---|---|
| Weak (few ads, cheap audience) | ~$2 | ~7,500,000 |
| Normal (interstitials + decent retention) | ~$4 | ~3,750,000 |
| Strong (rewarded video + good geos) | ~$6–8 | ~1,900,000 – 2,500,000 |
Bottom line: $15,000 is roughly 2–7 million game sessions. That sounds like a lot, but the portals are huge: CrazyGames has on the order of 300 million sessions/month, and Poki has ~90 million players per month. A single featured hit racks up millions of plays. So there are two realistic paths:
- One hit -> a single game makes it into the homepage selection and delivers most of the sum.
- A portfolio -> 5–10 solid games, each contributing a little. Less dependent on luck, but slower.
What Actually Determines the Money
Not "making a game" (with Fable 5 that's no longer the bottleneck), but:
- Retention and session length -> the longer people play, the more ads they see and the more aggressively the portal promotes you. A real example: the same game on CrazyGames held a player for ~4.5 min and took off, while on Poki it held ~2 min -> and Poki pulled it.
- Rewarded video -> a voluntary 15–30 sec view in exchange for a bonus. The most lucrative format. Build it in from the very start.
- Preview CTR (icon + title) -> the first multiplier of the entire funnel.
- Load speed and mobile-friendliness -> most of the traffic is from phones.
Next -> how to assemble such a game and carry it all the way to the money.

Part 1. Claude Fable 5 -> Access, Specification, Game Generation
Why Fable 5 Specifically
Fable 5 is the first publicly available Mythos-class model. For our task it has a concrete advantage: it assembles playable browser games (including 3D) from a single prompt and works in long autonomous sessions -> up to a dozen hours from a multi-page brief, planning on its own, checking itself, and reworking. For game dev this means: you describe a concept -> you get a working build, not a fragment.
A detail: Fable 5 has cybersecurity and biology filters with a fallback to Opus 4.8. For games this almost never triggers -> just don't waste tokens on borderline wordings.

Access
- In the Claude app -> select the Fable 5 model.
- Via the API -> the identifier claude-fable-5. Price: $10 per million input and $50 per million output tokens (up to a 90% discount on input with caching).
- Also on Amazon Bedrock, Google Cloud, and Microsoft Foundry.
For game dev, the most convenient way is to run Fable 5 through Claude Code (Part 2): it creates and edits files directly on your disk.
Principle: Not a "Prompt," but a "Specification"
To unlock the long autonomous runs, don't write "make a game." Give the model a specification document, SPEC.md, and ask it to implement the whole thing. Bonus: the portals' requirements (16:9, no external requests, incognito support -> see Part 3) are conveniently baked right into the spec, so you don't have to redo things later.
Step 1. The SPEC.md File (template, edit to fit your concept)
# Game: Star Dodger (working title)
## Platform and stack
- Pure HTML5 + CSS + vanilla JavaScript. No frameworks or bundlers.
- Structure: index.html, game.js, (optional) assets in /assets with RELATIVE paths.
- IMPORTANT (Poki requirement): no external requests in production — no Google Fonts,
no external CDNs (jsDelivr, etc.), no external images/sounds. Everything local.
- IMPORTANT (Poki requirement): works in incognito mode — wrap any access to localStorage
in try/catch.
- Keep the initial load size small (Poki requires < 8 MB).
- Works offline from a local folder and inside an iframe on the portal.
## Genre and mechanics
- Hyper-casual dodging arcade.
- The player controls a circle at the bottom of the screen.
- Objects fall from above: red = death, yellow = +points.
- Goal: survive as long as possible; the score grows over time.
## Controls (BOTH are MANDATORY)
- Desktop: arrow keys ← →, A/D, mouse.
- Mobile/tablet: touch (pointer events), the finger drags the player horizontally.
## Session economy
- Session length: target 2–5 minutes. Fast restart without reloading the page.
- Progression: rising difficulty (object speed/frequency) based on score.
## Ads — via an abstract window.Ads interface (do NOT tie it to a portal!)
Methods:
- Ads.loadingFinished()
- Ads.gameplayStart() // call on the player's FIRST input, not on load
- Ads.gameplayStop() // on any pause/death/menu
- Ads.interstitial(): Promise
- Ads.rewarded(): Promise<boolean> // true if the video was watched to the end
By default Ads is a stub (logs + reward=true). I'll plug in the portal implementations later.
Apply: rewarded() for "continue after death" (once per run) and "double the score";
interstitial() before a new run.
## Technical requirements
- Responsive canvas, devicePixelRatio support (cap ×2). Fast first frame.
- Clean, commented code, with explicit spots marked for integration.
## Deliverables
- A working game + instructions for running it locally.
- The final build in /dist: index.html at the root, relative paths, no external domains.Step 2. The Build Prompt
Read SPEC.md. First, give a short plan: screens, file structure, and a list of
functions with their purposes. Wait until I say "ok." Then implement the whole project
according to the specification, creating the files. At the end, verify the local run and
give the command.Step 3. Iterations (short, targeted)
The start feels slow: the first object comes after 0.4s — speed up the difficulty ramp
in the first 15 seconds.On the phone the finger sometimes loses the circle. Make pointer tracking smooth,
handle pointercancel and the finger leaving the edge.Add a game-over screen: "Continue (ad)" via Ads.rewarded() once per run,
"Double the score (ad)" and "Restart" via Ads.interstitial().Genres That Do Well on the Portals
.io games (instant entry), hyper-casual (one action), puzzles (long sessions), arcades/runners (replayability). The key trick is the Ads abstraction: the game doesn't know about Poki/CrazyGames, so the very same code works on both portals -> you just plug in the adapter (Part 3).
Part 2. Claude Code -> Installation, Game Code, Build

Installation
Method 1 -> native installer (recommended, Node.js NOT required):
# Windows (PowerShell)
irm https://claude.ai/install.ps1 | iex
# macOS / Linux / WSL
curl -fsSL https://claude.ai/install.sh | bash
# Homebrew (macOS/Linux)
brew install --cask claude-codeCheck:
claude --versionMethod 2 -> npm (Node.js 18+ required, current LTS is 22):
npm install -g @anthropic-ai/claude-codeImportant: without sudo. If you hit permission errors on Linux, configure npm to use ~/.npm-global and add it to your PATH.
Launch and Selecting Fable 5
mkdir star-dodger && cd star-dodger
claudeInside the session:
- /model -> select Claude Fable 5 (this is what gives you the long autonomous runs).
- /help -> commands, /clear -> clear context, /exit -> quit.
Put SPEC.md in the folder and give the build prompt. Below is the finished result, which you can run right now.
Full Game Code
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>Star Dodger</title>
<!-- In the portal version, the SDK <script> (Poki/CrazyGames) is added here BEFORE game.js -->
<style>
html, body { margin: 0; height: 100%; background: #0b0f1a; overflow: hidden;
font-family: system-ui, -apple-system, sans-serif; }
#game { display: block; width: 100vw; height: 100vh; touch-action: none; }
.overlay { position: fixed; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 14px; text-align: center;
color: #fff; background: rgba(5,8,16,.72); padding: 20px; }
.hidden { display: none; }
.btn { background: #3b82f6; color: #fff; border: 0; border-radius: 12px;
padding: 14px 22px; font-size: 18px; font-weight: 600; cursor: pointer; min-width: 220px; }
.btn.alt { background: #f59e0b; }
h1 { font-size: 32px; margin: 0; }
p { margin: 0; opacity: .85; line-height: 1.4; }
.score { font-size: 22px; }
#hud { position: fixed; top: 10px; left: 0; right: 0; text-align: center;
color: #fff; font-size: 22px; font-weight: 700; pointer-events: none; text-shadow: 0 1px 4px #000; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud" class="hidden">0</div>
<div id="start" class="overlay">
<h1>Star Dodger</h1>
<p>Dodge the red ones, catch the yellow ones.<br>Touch/mouse or arrow keys ← →</p>
<button class="btn" id="playBtn">Play</button>
</div>
<div id="over" class="overlay hidden">
<h1>Game Over</h1>
<p class="score" id="finalScore">Score: 0</p>
<button class="btn alt" id="reviveBtn">Continue (ad)</button>
<button class="btn alt" id="doubleBtn">Double the score (ad)</button>
<button class="btn" id="againBtn">Restart</button>
</div>
<!-- Default ad stub. In Part 3 this is replaced by the portal adapter. -->
<script>
window.Ads = window.Ads || {
loadingFinished() { console.log('[ads] loading finished'); },
gameplayStart() { console.log('[ads] gameplay start'); },
gameplayStop() { console.log('[ads] gameplay stop'); },
interstitial() { console.log('[ads] interstitial (stub)'); return Promise.resolve(); },
rewarded() { console.log('[ads] rewarded (stub) -> success'); return Promise.resolve(true); },
};
</script>
<script src="game.js"></script>
</body>
</html>game.js
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const hud = document.getElementById('hud');
const startScreen = document.getElementById('start');
const overScreen = document.getElementById('over');
const finalScoreEl = document.getElementById('finalScore');
let W, H, dpr;
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth; H = window.innerHeight;
canvas.width = W * dpr; canvas.height = H * dpr;
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
const player = { x: W / 2, y: H - 90, r: 22 };
let items = []; // { x, y, vy, r, type: 'bad' | 'good' }
let score = 0, best = 0;
let running = false, revivedThisRun = false;
let spawnTimer = 0, lastTime = 0;
// ---- Controls: touch/mouse (pointer) + keyboard ----
let targetX = null;
canvas.addEventListener('pointerdown', e => { targetX = e.clientX; });
canvas.addEventListener('pointermove', e => { if (running) targetX = e.clientX; });
canvas.addEventListener('pointercancel', () => { targetX = null; });
const keys = {};
window.addEventListener('keydown', e => {
keys[e.key] = true;
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', ' '].includes(e.key)) e.preventDefault();
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
function reset() {
score = 0; items = []; spawnTimer = 0; revivedThisRun = false;
player.x = W / 2; player.y = H - 90; targetX = null;
}
function spawn() {
const good = Math.random() < 0.28;
items.push({
x: 24 + Math.random() * (W - 48),
y: -30,
r: good ? 14 : 18 + Math.random() * 10,
vy: 140 + score * 1.4 + Math.random() * 60,
type: good ? 'good' : 'bad',
});
}
function update(dt) {
player.y = H - 90;
const speed = 520;
if (keys['ArrowLeft'] || keys['a'] || keys['A']) player.x -= speed * dt;
if (keys['ArrowRight'] || keys['d'] || keys['D']) player.x += speed * dt;
if (targetX !== null) player.x += (targetX - player.x) * Math.min(1, dt * 12);
player.x = Math.max(player.r, Math.min(W - player.r, player.x));
spawnTimer -= dt;
const interval = Math.max(0.28, 0.7 - score * 0.004);
if (spawnTimer <= 0) { spawn(); spawnTimer = interval; }
for (let i = items.length - 1; i >= 0; i--) {
const it = items[i];
it.y += it.vy * dt;
if (Math.hypot(it.x - player.x, it.y - player.y) < it.r + player.r) {
if (it.type === 'good') { score += 5; items.splice(i, 1); continue; }
gameOver(); return;
}
if (it.y - it.r > H) items.splice(i, 1);
}
score += dt * 8; // points for surviving
hud.textContent = Math.floor(score);
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#60a5fa';
ctx.beginPath(); ctx.arc(player.x, player.y, player.r, 0, Math.PI * 2); ctx.fill();
for (const it of items) {
ctx.fillStyle = it.type === 'good' ? '#fbbf24' : '#ef4444';
ctx.beginPath(); ctx.arc(it.x, it.y, it.r, 0, Math.PI * 2); ctx.fill();
}
}
function loop(t) {
if (!running) return;
const dt = Math.min(0.05, (t - lastTime) / 1000 || 0);
lastTime = t;
update(dt);
if (running) { draw(); requestAnimationFrame(loop); }
}
function startGame() {
reset();
startScreen.classList.add('hidden');
overScreen.classList.add('hidden');
hud.classList.remove('hidden');
running = true; lastTime = performance.now();
Ads.gameplayStart(); // player's first input (Poki requirement)
requestAnimationFrame(loop);
}
function resume() {
overScreen.classList.add('hidden');
hud.classList.remove('hidden');
running = true; lastTime = performance.now();
Ads.gameplayStart();
requestAnimationFrame(loop);
}
function gameOver() {
running = false;
Ads.gameplayStop(); // any stop of gameplay
best = Math.max(best, Math.floor(score));
finalScoreEl.textContent = `Score: ${Math.floor(score)} (best ${best})`;
document.getElementById('reviveBtn').style.display = revivedThisRun ? 'none' : '';
document.getElementById('doubleBtn').style.display = '';
hud.classList.add('hidden');
overScreen.classList.remove('hidden');
}
// ---- Buttons + ads ----
document.getElementById('playBtn').onclick = startGame;
document.getElementById('againBtn').onclick = async () => {
await Ads.interstitial(); // interstitial ad before a new run
startGame();
};
document.getElementById('reviveBtn').onclick = async () => {
const ok = await Ads.rewarded(); // rewarded video
if (ok) {
revivedThisRun = true;
items = items.filter(it => it.type === 'good' || it.y < H * 0.4); // clear nearby threats
resume();
}
};
document.getElementById('doubleBtn').onclick = async () => {
const ok = await Ads.rewarded();
if (ok) {
score *= 2;
best = Math.max(best, Math.floor(score));
finalScoreEl.textContent = `Score: ${Math.floor(score)} (best ${best})`;
document.getElementById('doubleBtn').style.display = 'none';
}
};
Ads.loadingFinished(); // assets here are instantLocal Run and Test
From the project folder:
python3 -m http.server 8000Open http://localhost:8000. Then:
- In DevTools (F12), enable device mode (phone/tablet) -> test touch.
- Open the game in incognito mode -> Poki checks this (localStorage is restricted there).
- Double-clicking index.html sometimes breaks the scripts -> a local server is more reliable.
Final Build
Assemble a release build into /dist: index.html at the root, game.js next to it, assets in
/dist/assets, all paths relative, no external domains. Remove debug code and logs.
Verify that the initial load size is < 8 MB and that the game runs from /dist.Common Problems
- command not found: claude -> PATH didn't refresh. Restart the terminal or use the native installer.
- Permission errors with npm -> don't use sudo; configure ~/.npm-global.
- Scripts don't work on double-click -> run via a local server.
- Game isn't full screen -> check width: 100vw; height: 100vh and the canvas recalculation in resize().
Part 3. Poki / CrazyGames -> SDK Integration, Requirements, Publishing
Thanks to the Ads abstraction, we don't touch the game code. We make two entry files -> poki.html and crazygames.html -> each loading its own SDK and defining its own window.Ads before loading game.js.
A. Poki
Docs: https://sdk.poki.com
Inspector: https://inspector.poki.dev
contact: developersupport@poki.com
Terms: 50/50 revenue on Poki-sourced traffic; all games go through manual selection; web exclusivity is required (Steam/mobile are fine); Poki for Developers is in closed beta -> apply via the form.

Game requirements (checked at review) -> checklist:
- Support for desktop, mobile, and tablet, full-screen game (portrait/landscape).
- 16:9 aspect ratio, scaling to 640×360 / 836×470 / 1031×580, filling the entire canvas.
- Incognito mode -> localStorage access in try/catch.
- No external requests -> Poki blocks calls to third-party services by default (Google Fonts, external CDNs/assets). The game must be self-contained (our code is exactly that).
- No third-party ads or splash screens, no external links; the studio logo -> only on the loading screen (ads run through the Poki SDK).
- The game is playable with an ad blocker enabled -> don't hide the gameplay behind an ad-blocker message (Poki handles that on its side).
- Initial load size < 8 MB, a clean build with no debug code.
- Static and animated icons (both mandatory for a global release).
- Don't let the game's scrolling jolt the parent page.
poki.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>Star Dodger</title>
<!-- 1) Poki SDK BEFORE the game code -->
<script src="https://game-cdn.poki.com/scripts/v2/poki-sdk.js"></script>
<style>/* ... the same styles as in index.html ... */</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud" class="hidden">0</div>
<!-- ... the same start / over overlays ... -->
<!-- 2) Ads adapter for Poki (INSTEAD of the stub) -->
<script>
function muteForAd() { /* stop music/sound and disable game input */ }
function unmuteForAd() { /* restore sound and input */ }
window.Ads = {
loadingFinished() { PokiSDK.gameLoadingFinished(); },
gameplayStart() { PokiSDK.gameplayStart(); },
gameplayStop() { PokiSDK.gameplayStop(); },
// Won't ALWAYS show a video — Poki decides based on its own timer
interstitial() {
return PokiSDK.commercialBreak(muteForAd).then(unmuteForAd);
},
// success === true only if the video was watched to the end
rewarded() {
return PokiSDK.rewardedBreak({ size: 'medium', onStart: muteForAd })
.then((success) => { unmuteForAd(); return !!success; });
},
};
PokiSDK.init()
.then(() => console.log('Poki SDK ok'))
.catch(() => console.log('Poki SDK failed, continue anyway'));
</script>
<!-- prevent the page from jumping on arrows/space/wheel (the game is inside Poki's long page) -->
<script>
window.addEventListener('keydown', ev => {
if (['ArrowDown', 'ArrowUp', ' '].includes(ev.key)) ev.preventDefault();
});
window.addEventListener('wheel', ev => ev.preventDefault(), { passive: false });
</script>
<!-- 3) Game code, unchanged -->
<script src="game.js"></script>
</body>
</html>SDK event rules (Poki checks these strictly):
- gameplayStart() -> on the player's first input, NOT on load.
- gameplayStop() -> on any stop (pause, menu, end of level, death).
- Events must not fire twice in a row (start after start, stop after stop is an error).
- During an ad, do not call SDK events.
- commercialBreak() -> only when exiting a pause back into gameplay.
Our code already complies: startGame/resume → gameplayStart, gameOver → gameplayStop, and there's always a stop between them.
On the 16:9 aspect ratio. The base code stretches to the whole window (any shape), while Poki requires 16:9 with scaling. Add a logical 16:9 field and letterboxing -> prompt for Claude Code:
Make a logical game field of 640x360 (16:9) and scale it proportionally, centered,
with black bars (letterbox), while keeping support for any window size and
devicePixelRatio. Recompute input coordinates to account for the scale.The Poki publishing funnel (it's not just "uploaded"):
- Upload to Poki for Developers (first accept the Terms & Conditions).
- Playtesting / Feedback -> how real players interact (results within hours).
- Player Fit Test -> whether players like the game.
- Web Fit Test -> how the game lives in the open web and monetizes. Here the icon/preview is critical (there are A/B tests of cover art and animated icons). If it doesn't pass -> see their "What To Do If My Game Doesn't Make It" section (a new icon/title and a retry often saves it).
- Final Poki Review -> the final check and release.
B. CrazyGames
Docs: https://docs.crazygames.com
Submit: https://developer.crazygames.com/games
Payouts: https://docs.crazygames.com/payouts
Terms: ~300 million sessions/month; the standard share is not publicly disclosed; 2 months of launch exclusivity raises the share by ~50%; sitelock is enabled automatically (the game only works on CrazyGames and its affiliates); a universal HTML5 SDK v3 is supported; only compressed builds (Brotli/Gzip) are accepted -> dev builds won't run.

crazygames.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>Star Dodger</title>
<!-- 1) CrazyGames SDK v3 BEFORE the game code -->
<script src="https://sdk.crazygames.com/crazygames-sdk-v3.js"></script>
<style>/* ... the same styles ... */</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="hud" class="hidden">0</div>
<!-- ... the same overlays ... -->
<!-- 2) Ads adapter for CrazyGames + loading the game after init -->
<script>
function muteForAd() { /* disable sound + game input */ }
function unmuteForAd() { /* restore sound + input */ }
(async () => {
try {
await window.CrazyGames.SDK.init(); // you must wait for init
} catch (e) {
console.log('CrazyGames init failed', e);
}
const CG = window.CrazyGames.SDK;
window.Ads = {
loadingFinished() { CG.game.sdkGameLoadingStop?.(); },
gameplayStart() { CG.game.gameplayStart(); },
gameplayStop() { CG.game.gameplayStop(); },
// requestAd takes callbacks, not a Promise — we wrap it in a Promise
interstitial() {
return new Promise((resolve) => {
CG.ad.requestAd('midgame', {
adStarted: () => muteForAd(),
adFinished: () => { unmuteForAd(); resolve(); },
adError: () => { unmuteForAd(); resolve(); }, // on error, continue anyway
});
});
},
// grant the reward ONLY in adFinished
rewarded() {
return new Promise((resolve) => {
CG.ad.requestAd('rewarded', {
adStarted: () => muteForAd(),
adFinished: () => { unmuteForAd(); resolve(true); }, // watched -> reward
adError: () => { unmuteForAd(); resolve(false); }, // didn't show -> no reward
});
});
},
};
// load the game after the adapter is ready
const s = document.createElement('script');
s.src = 'game.js';
document.body.appendChild(s);
})();
</script>
</body>
</html>Notes: the ad is shown as an overlay -> be sure to mute the sound in adStarted and restore it in adFinished/adError. The game must remain playable with an ad blocker enabled.
CrazyGames payouts: they go out monthly via Tipalti. Before submitting the game, you must complete payment onboarding in the Developer Portal (the billing section) -> otherwise you can't submit the game. Payout methods and thresholds depend on your country -> details are on the payouts page. (This isn't tax/financial advice -> when in doubt, consult a professional.)
CrazyGames publishing:
- A developer account on developer.crazygames.com + payment onboarding.
- SDK v3 (above), a compressed build.
- Submit at developer.crazygames.com/games: cover art, description, tags → quality review.
C. One Codebase -> Two Portals
star-dodger/
├── game.js ← game logic, shared (DOES NOT change)
├── index.html ← local version (Ads stub) — for development/testing
├── poki.html ← Poki version
└── crazygames.html ← CrazyGames versionA prompt for Claude Code to generate both versions:
Based on index.html and game.js, assemble poki.html and crazygames.html. In each one:
load the required SDK in <head>, implement window.Ads ON TOP of our interface
(loadingFinished/gameplayStart/gameplayStop/interstitial/rewarded), then load game.js.
Do NOT change game.js. Implementations: Poki — commercialBreak/rewardedBreak; CrazyGames —
SDK.ad.requestAd('midgame'/'rewarded') with callbacks. Mute sound/input during ads,
and for Poki add the anti-page-jump snippet.D. What Actually Brings in the Money
The code is just the foundation. The money is made by:
- Icon + title (CTR) -> test variations; Poki has A/B tests of cover art and animated icons.
- The first 30 seconds -> instantly clear and addictive, no long tutorials.
- Session length and retention -> the portals use these to decide whether to promote you.
- Rewarded video in organic spots (continue / double) -> the most lucrative format.
- Load speed -> light assets, a fast first frame, size < 8 MB.
- Iterating on analytics -> watch the portal's metrics → tweak in Claude Code → update the game.
Part 4. Conclusion
What changed with Fable 5's arrival: "making a game" is no longer expensive and slow. What used to require a team now assembles from a prompt in a single session. The barrier to entry for prototyping has dropped sharply -> that's your opening.
But, honestly, about the $15,000: the tool stack itself doesn't bring in money. Fable 5 + Claude Code give you a game cheaply and quickly; Poki/CrazyGames give you a huge audience and ready-made monetization. The sum comes down to one thing: did you hit on something people actually want to play. The bottleneck has shifted from "making" to "what people will play and who will see it."
A Week-by-Week Route
- Week 1. Install Claude Code, select Fable 5, and from SPEC.md assemble 1–2 simple games with spots built in for rewarded video. The goal is not a masterpiece, but a playable build.
- Week 2. Make poki.html / crazygames.html, run through the requirements checklist (16:9, incognito, < 8 MB, no external requests), polish the first 30 seconds and the loading, and prepare the icons.
- Weeks 3–4. Upload to the Poki Inspector / to CrazyGames, pass the tests (Player Fit → Web Fit). Repackage a weak game (icon/title) or replace it. Strengthen and promote a strong one.
- Beyond. Don't bet everything on one game: build a portfolio, cheaply try many concepts, and double down where the sessions take off. The $15,000 comes either from a single featured hit, or from the sum of several solid games reaching millions of sessions.

Thank you for reading the guide, you will succeed, I know that 🩷
Save it to your bookmarks so you don't lose it
Related articles

You Built a Mobile Game This Weekend. Here's Why - and How to Fix It in One File.
The game is done. The wall isn't the code. It's everything that comes after.

You Built a Mobile Game This Weekend. Here's Why - and How to Fix It in One File.
*"Developer entity required."*
