Skip to content

How to add GameMonetize games to your website

Juan, author photoBy Juan

You can add GameMonetize games to your website without an account, an API key or a plugin. The catalogue is a public feed and each entry carries an iframe URL you can use as it comes. The hard part is what comes after: the feed hands you just over 31,000 games, and most of them should not go near your site.

This is how PuzzleMoss does it. I wrote the brief for the site and an AI coding agent built it from that brief (the longer story is in the post on building PuzzleMoss with AI). The snippets below are cut-down versions of the code it wrote, and the counts come from a full sweep of the feed on 12 November 2026.

Step 1: read the feed

The endpoint is https://rss.gamemonetize.com/rssfeed.php. The gamemonetize.com address redirects there. The parameters are format=json, category, type=html5, popularity, company=All and amount=All.

const FEED = 'https://rss.gamemonetize.com/rssfeed.php';

type RawGame = {
  id: string; title: string; description: string; instructions: string;
  url: string; category: string; tags: string; thumb: string;
  width: string; height: string;
};

async function fetchCategory(category: string): Promise<RawGame[]> {
  const url = new URL(FEED);
  url.search = new URLSearchParams({
    format: 'json', category, type: 'html5',
    popularity: 'newest', company: 'All', amount: 'All',
  }).toString();
  const res = await fetch(url);
  if (!res.ok) throw new Error(`${category}: HTTP ${res.status}`);
  const body = await res.json();
  return Array.isArray(body) ? body : [];
}

The response is a bare JSON array with no wrapper. width and height are strings, so convert them with Number() and keep a fallback. tags is one comma-separated string rather than an array.

The category parameter does not behave as documented. category=Puzzle returns an empty array, while category=Puzzles returns the puzzle bucket. In this sweep Puzzle, AI, Baby Hazel and Stickman all came back empty. Each query is also capped at 5,001 rows, newest first, and the All, Arcade and Puzzles queries all hit that ceiling, so a single category=All call misses most of the catalogue.

The popularity parameter changes nothing: every value tried returned the same newest-first list. The feed carries no popularity data, so a "most popular" list built from it is made up.

Step 2: dedupe by embed token

Sweep every documented category value plus Puzzles and merge the results. The same game turns up under several categories, so you need a key. Use the path segment of the url field, the long token after html5.gamemonetize.co/. It is the same wherever the game appears.

function embedToken(url: string): string | null {
  const m = (url ?? '').match(/gamemonetize\.co\/([a-z0-9]+)\/?/i);
  return m ? m[1] : null;
}

const byToken = new Map<string, RawGame>();
for (const category of CATEGORIES) {
  for (const game of await fetchCategory(category)) {
    const token = embedToken(game.url);
    if (token && !byToken.has(token)) byToken.set(token, game);
  }
}

The overlap is large. The Puzzles query returned 5,001 rows, and only 3,481 of them had not already arrived through an earlier category. The whole sweep came to 31,086 unique games.

Step 3: decode the text

Descriptions arrive HTML-encoded twice. You find &amp;ndash; where a dash should be, and decoding once leaves &ndash; sitting in your page. In this sweep 2,136 descriptions still held a double-encoded entity. Decode in a loop until the text stops changing, then strip tags and collapse the whitespace.

const NAMED: Record<string, string> = {
  amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ',
  mdash: '-', ndash: '-', rsquo: "'", ldquo: '"', rdquo: '"',
};

function decodeEntities(text: string): string {
  let out = text ?? '';
  for (let pass = 0; pass < 4; pass++) {
    const next = out
      .replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
      .replace(/&([a-z]+);/gi, (m, n) => NAMED[n.toLowerCase()] ?? m);
    if (next === out) break;
    out = next;
  }
  return out.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
}

Run it over the title, description, instructions and tags before any filtering. A filter looking for a word will not find it behind an entity.

Step 4: filter, and count what you drop

PuzzleMoss only carries puzzles, so 25,255 of the 31,086 games went at the first check, and 893 more were puzzles of a type the site does not carry, such as jigsaws and escape rooms. That left 5,831 candidates. The filters after that would apply to any games site.

Banned content

Gambling, adult, horror and crypto games, matched in the title, tags and description: 88 dropped. Check the description too. The filter code in this project carries a note about a sports-betting game that once got past a title and tag check with a description opening "Free to Play social sports betting game!". Ad networks restrict gambling and adult content, and a reviewer judges what is on the page.

IP clones

773 dropped. GameMonetize licenses the game file from its developer, and nobody in that chain owns Minecraft, Squid Game or Candy Crush. A takedown notice goes to the site showing the game, which is you. Match protected names and characters, never genres: a block puzzle is fine, a game called Tetris is not. Ambiguous words need a decision: on PuzzleMoss "princess" and "blocky" in a title or tag drop the game, because in this feed they nearly always front a Disney or Minecraft lookalike. Games that copy characters their developer does not own do not go on my sites, and this filter is where that rule lives.

Kid-directed games and memes

1,138 dropped for wording such as toddler, baby, preschool or "for kids", a bare "kids" tag, or meme names like skibidi and brainrot. I want my sites to suit any age, which is a different thing from being made for small children. A site directed at children falls under rules such as the US COPPA rule, and the FTC's COPPA guidance counts cookies and other persistent identifiers as personal information, which puts behavioural advertising on such a site behind parental consent. The meme games are a quality call.

Unsure and thin

1,636 games did not clearly belong to one subcategory, and they were skipped rather than pushed into the nearest one. Another 237 passed every filter with under 50 words of description and instructions between them. Those stay in the pool and never ship, for the reason in step 7.

Reskins

The same game is often listed several times under different titles with near-identical descriptions. Publish them all and the catalogue looks padded. PuzzleMoss finds them in two passes: locality-sensitive hashing over each description's rarest words across the whole pool, then an exact word-overlap check within each subcategory that clusters anything above 0.6. That removed 72.

function tokens(text: string): Set<string> {
  return new Set(
    text.toLowerCase().replace(/[^a-z0-9 ]/g, ' ').split(/\s+/).filter((w) => w.length >= 4),
  );
}

function jaccard(a: Set<string>, b: Set<string>): number {
  let shared = 0;
  a.forEach((w) => { if (b.has(w)) shared++; });
  const union = a.size + b.size - shared;
  return union === 0 ? 0 : shared / union;
}

// Same subcategory and jaccard(a, b) > 0.6: one cluster, one survivor.

After all of that the pool holds 1,231 games, 14 of which have since been rejected by hand. Roughly one game in 25 makes it through. The site groups them into eight subcategories: 266 in Brain & Numbers, 234 in Match-3, 150 in Connect & Draw, 145 in Sort & Organise, and 88 at the small end in Hidden Object & Spot the Difference.

Step 5: load the game on click

The url field is the iframe src. Use it exactly as it comes, with no extra parameters. What matters is when it loads. The iframe pulls in the game, its engine and its ad code, and a page that mounts it on load is slow before anyone has decided to play. Render the thumbnail and a button, and create the iframe only after the click.

'use client';
import { useRef, useState } from 'react';

export function ClickToPlay({ src, thumb, title }: { src: string; thumb: string; title: string }) {
  const [playing, setPlaying] = useState(false);
  const box = useRef<HTMLDivElement>(null);

  return (
    <div>
      <div ref={box} style={{ aspectRatio: '16 / 9' }}>
        {playing ? (
          <iframe src={src} title={title} allow="fullscreen" allowFullScreen />
        ) : (
          <button type="button" onClick={() => setPlaying(true)} aria-label={`Play ${title}`}>
            <img src={thumb} alt={`${title} title screen`} loading="lazy" />
          </button>
        )}
      </div>
      <button type="button" disabled={!playing} onClick={() => box.current?.requestFullscreen()}>
        Fullscreen
      </button>
    </div>
  );
}

The fullscreen button acts on the wrapper, so it works for games that offer no fullscreen control of their own, and allow="fullscreen" covers the ones that do. Keep the outer box at 16:9 and size the iframe inside it from the feed's width and height, so portrait games are pillarboxed rather than stretched. The PuzzleMoss version also asks first if the visitor has not accepted optional cookies, since the game may set its own. Nothing from GameMonetize loads until someone presses Play.

Step 6: ads.txt and the site review

Reading the feed is free and anonymous. Earning from the games is a separate step: you register as a publisher, add your site in the dashboard, and GameMonetize reviews it. Their guide for publishers seeing zero ad impressions says the ads.txt file must be on the root domain and that they do not accept sites on subdomains, including free ones on blogspot.com or wordpress.com. Copy the exact lines from your dashboard. In a Next.js static export, public/ads.txt lands at the root of the build. Revenue terms change, so take them from the GameMonetize FAQ rather than from a figure quoted in a blog post.

One Next.js trap if you add your own ad network: in a static export React 18 reorders and drops head scripts declared in the layout, so PuzzleMoss injects them into the built HTML after next build instead. At launch it injects nothing, because the site runs no ads yet.

Step 7: write your own page copy

Every site that embeds a GameMonetize game can paste the feed description onto its page. Copy it and your page says the same as every other site carrying that game. Search engines have no reason to prefer yours, and ad reviewers read it as thin content.

I set the rule for PuzzleMoss that every game page needs 250 words written about that specific game, and that a game which cannot reach 250 honest words does not ship. Honest is the hard part. The copy may only state what the feed's description and instructions support, so there are no invented level counts, modes or tips. That is why the 237 thin listings stay unpublished: there are not 250 true words to be had from a 30-word blurb. The copy checker also fails any page that reuses a run of ten words from the feed, so a mahjong game cannot quietly borrow its developer's blurb.

What not to do

Do not strip the ads or branding out of the games. They are the developers' income, and removing them breaks GameMonetize's terms. If you do not want in-game ads on your site, this is the wrong source of games.

Do not load iframes automatically. A homepage with a dozen live game frames downloads a dozen games and their ad code before the visitor has done anything, and any game that plays sound on start will do so uninvited.

Do not publish 5,000 pages because the feed makes it easy. Thousands of game pages that each carry the feed's two sentences are the pattern ad networks reject as made for advertising. Start with fewer games and a real page behind each one, and add more as the copy gets written.