
TinyFish parallel agents are cloud-based browser sessions that query multiple game storefronts simultaneously and return structured pricing data — game title, current price, discount percentage, and direct purchase URL — normalized across currencies and formats into a single ranked list.
Game prices vary wildly across storefronts and change constantly during sales. IsThereAnyDeal and CheapShark aggregate some of this, but neither covers every platform — itch.io is out, regional stores are out, newer storefronts take months to appear. When a game isn't in the database, you're back to checking each store manually.
This tutorial builds a 10-platform game price comparison tool using TinyFish agents. All stores are checked simultaneously: Steam, GOG, Epic Games, Xbox, PlayStation Store, Humble Bundle, Fanatical, Green Man Gaming, itch.io, and Amazon Gaming. You get a ranked price list in the time it takes to manually open two tabs.
Build a video game price comparison tool in 4 steps:
CheapShark and IsThereAnyDeal are good starting points. They cover Steam, GOG, and most US/EU storefronts reliably — and their APIs are free. For straightforward PC game price lookup, they're often sufficient.
Browser agents make sense when:
The engineering tradeoff is straightforward:
| Approach | Best for | Limitation |
|---|---|---|
| CheapShark / IsThereAnyDeal API | Quick PC storefront lookup, free, no setup | Doesn't cover all platforms; cached data |
| Browser agents (this tutorial) | All 10 platforms, real-time, fully customizable | Credit cost per query; rate-limited at Free tier |
Choosing the right TinyFish API for this project: TinyFish offers two tools with different cost profiles. The Fetch API (api.fetch.tinyfish.ai) retrieves a page at a known URL — 1 credit = 15 pages. The Agent API (agent.tinyfish.ai) handles storefronts requiring a search step — 1 credit per step. For storefronts with stable product page URLs (GOG, itch.io), Fetch API can retrieve the price directly. For storefronts that require a search query (Steam, Epic), Agent API is required. Where you have direct product URLs, Fetch API is 15x cheaper than Agent API.
Prerequisites: Node.js 18+, TypeScript, and a TinyFish API key.
npm install @tiny-fish/sdk
npm install -D ts-node typescript
export TINYFISH_API_KEY=your_key_hereimport { TinyFish } from "@tiny-fish/sdk";
const client = new TinyFish(); // reads TINYFISH_API_KEY from env
const PLATFORMS = [
{ name: "Steam", url: "https://store.steampowered.com/search/?term=" },
{ name: "GOG", url: "https://www.gog.com/en/games?query=" },
{ name: "Epic Games", url: "https://store.epicgames.com/en-US/browse?q=" },
{ name: "Xbox", url: "https://www.xbox.com/en-US/search?q=" },
{ name: "PlayStation", url: "https://store.playstation.com/en-us/search/" },
{ name: "Humble Bundle", url: "https://www.humblebundle.com/store/search?search=" },
{ name: "Fanatical", url: "https://www.fanatical.com/en/search?search=" },
{ name: "Green Man Gaming", url: "https://www.greenmangaming.com/search/?query=" },
{ name: "itch.io", url: "https://itch.io/search?q=" },
// Add your 10th storefront here — any public game store search URL works
// e.g. regional stores, indie platforms, or subscription bundle catalogs
];
const buildGoal = (gameTitle: string, platformName: string): string => `
Search for the game "${gameTitle}" on this storefront.
Return a JSON object with:
- gameTitle: string (exact title as shown on this store)
- price: string (current price exactly as displayed, including currency symbol)
- originalPrice: string or null (if there is an active discount, the original/crossed-out price)
- discountPercent: number or null (e.g. 40 for 40% off, null if not discounted)
- currency: string (3-letter code: "USD", "EUR", "GBP", etc.)
- platform: "${platformName}" (e.g. "Steam" or "GOG")
- url: string (direct link to this game's store page)
- available: boolean (true if the game is purchasable; false if unavailable, region-locked, or subscription-only)
If the game is not found on this storefront, return null.
Return only the most relevant match for "${gameTitle}".
`;
async function compareGamePrices(gameTitle: string) {
const query = encodeURIComponent(gameTitle);
const requests = PLATFORMS.map((platform) =>
client.agent
.run({ url: platform.url + query, goal: buildGoal(gameTitle, platform.name) })
.then((response) => {
// response.result is the parsed JavaScript object returned by the agent.
// null = game not found. Check for goal failure vs legitimate null:
const result = response.result as unknown;
if (result && typeof result = "object" && !Array.isArray(result) &&
(result as Record<string, unknown>).status = "failure") {
return { platform: platform.name, result: null };
}
return { platform: platform.name, result: result ?? null };
})
);
const settled = await Promise.allSettled(requests);
return settled.map((r, i) => ({
platform: PLATFORMS[i].name,
// error: true = network/timeout failure, distinct from null (game not found)
...(r.status = "fulfilled" ? r.value : { result: null, error: true }),
}));
}Why `Promise.allSettled` not `Promise.all`: A single storefront timing out or returning an error shouldn't cancel the nine successful results. allSettled ensures every agent completes and reports independently.
Concurrency note: The Free plan supports 2 concurrent agent runs — 10 platforms run in approximately 5 batches. The Starter plan (10 concurrent) runs all 10 simultaneously. Each agent step consumes 1 credit. Querying all 10 platforms for one game title typically costs 30–60 credits depending on site complexity.

Ten storefronts return ten different price formats: "$29.99", "€24,99", "£19.99", "AU$39.95", "Free", "N/A (subscription only)". Without normalization, sorting by price is impossible.
// normalize.ts
const CURRENCY_TO_USD: Record<string, number> = {
USD: 1.0,
EUR: 1.09, // update these rates from an exchange rate API in production
GBP: 1.27,
AUD: 0.65,
CAD: 0.73,
};
function parsePriceToUsd(raw: string, currency: string): number | null {
if (!raw || raw.toLowerCase().includes("free")) return 0;
if (raw.toLowerCase().includes("n/a") || raw.toLowerCase().includes("subscription")) return null;
// Strip currency symbols and labels, normalize decimal separator
// Remove all non-numeric chars except dot; handle comma as thousands separator
const cleaned = raw.replace(/[^0-9.,]/g, "").replace(/,/g, "");
const amount = parseFloat(cleaned);
if (isNaN(amount)) return null;
const rate = CURRENCY_TO_USD[currency] ?? 1.0;
return Math.round(amount * rate * 100) / 100; // round to cents
}
function calcSavings(
currentUsd: number | null,
originalUsd: number | null
): { savingsUsd: number; discountPct: number } | null {
if (currentUsd = null || originalUsd = null || originalUsd <= 0) return null;
const savingsUsd = originalUsd - currentUsd;
const discountPct = Math.round((savingsUsd / originalUsd) * 100);
return savingsUsd > 0 ? { savingsUsd, discountPct } : null;
}
export function normalizeGameResult(raw: {
price?: string;
originalPrice?: string;
currency?: string;
discountPercent?: number | null;
available?: boolean;
[key: string]: unknown;
}) {
const currency = raw.currency ?? "USD";
const priceUsd = parsePriceToUsd(raw.price ?? "", currency);
const originalUsd = raw.originalPrice
? parsePriceToUsd(raw.originalPrice, currency)
: null;
const savings = calcSavings(priceUsd, originalUsd);
return {
...raw,
priceUsd,
originalUsd,
effectiveDiscountPct: raw.discountPercent ?? savings?.discountPct ?? 0,
savingsUsd: savings?.savingsUsd ?? 0,
};
}Three normalization decisions worth explaining:
Putting it together:
import { normalizeGameResult } from "./normalize";
const rawResults = await compareGamePrices("Elden Ring");
const normalized = rawResults
.filter((r) => r.result ! null)
.map((r) => ({ ...r, result: normalizeGameResult(r.result as Record<string, unknown>) }))
.sort((a, b) => {
const aPrice = a.result.priceUsd ?? Infinity;
const bPrice = b.result.priceUsd ?? Infinity;
return aPrice - bPrice;
});
normalized.forEach((r) => {
const disc = r.result.effectiveDiscountPct > 0 ? ` (${r.result.effectiveDiscountPct}% off)` : "";
console.log(`${r.platform}: $${r.result.priceUsd?.toFixed(2)}${disc} → ${r.result.url}`);
});Run with npx ts-node index.ts.
The comparison tool becomes a deal monitor with a scheduled run and a threshold check:
import { WebhookClient } from "discord.js";
const DEAL_THRESHOLD_USD = 15; // alert when any platform drops below this
const webhook = new WebhookClient({ url: process.env.DISCORD_WEBHOOK_URL! });
async function checkForDeals(gameTitle: string) {
const results = await compareGamePrices(gameTitle);
const normalized = results
.filter((r) => r.result !== null)
.map((r) => ({ ...r, result: normalizeGameResult(r.result as Record<string, unknown>) }));
const deals = normalized.filter(
(r) => (r.result.priceUsd ?? Infinity) < DEAL_THRESHOLD_USD
);
if (deals.length > 0) {
const message = deals
.map((d) => `**${d.platform}**: $${d.result.priceUsd?.toFixed(2)} ${d.result.effectiveDiscountPct > 0 ? `(-${d.result.effectiveDiscountPct}%)` : ""} — ${d.result.url}`)
.join("\n");
await webhook.send({ content: `🎮 Deal alert for **${gameTitle}**:\n${message}` });
}
}
// Run daily via cron or GitHub Actions
await checkForDeals("Elden Ring");
await checkForDeals("Baldur's Gate 3");Extension: Steam Summer Sale monitor. Run the comparison daily in mid-June and late November — the periods when Steam historically runs its major sales. When any game in your watchlist drops more than 50%, trigger the webhook immediately. Track prices over time to see which stores match Steam sale prices and which don't.
Extension: multi-game watchlist. Pass an array of game titles to compareGamePrices in parallel via Promise.all — 5 games × 10 platforms = 50 concurrent agent runs on a Pro plan.
For the same 10-platform parallel pattern applied to retail products, see how parallel agents work for medicine price comparison across pharmacy chains.
Game stores update prices hourly during sales. Checking 10 platforms manually for a single title takes long enough to miss a flash deal. Parallel agents collapse that to a single function call — all 10 checked simultaneously, prices normalized across currencies, ranked by actual cost. The same architecture that powers commercial deal aggregators, without the platform partnership requirements.
Can this build a video game price comparison tool that checks all major storefronts? Yes — browser agents navigate public storefront pages and require no per-platform API registration. The example covers 10 platforms including ones not indexed by existing aggregators: itch.io, Xbox, PlayStation Store, and Amazon Gaming. Add any storefront with a public search page by appending an entry to the PLATFORMS array.
When should I use browser agents instead of CheapShark API? Use this decision framework:
| If you need | → Use |
|---|---|
| All major platforms including itch.io, Xbox, PlayStation | → Browser agents (CheapShark doesn't index these) |
| Real-time pricing during an active sale | → Browser agents (aggregators cache) |
| PC storefronts only, fast prototype | → CheapShark API (free, no credits needed) |
| Availability, bundle status, or regional pricing | → Browser agents (read the actual store page) |
In practice: start with CheapShark for the platforms it covers, add browser agents for the gaps.
Why do existing APIs like CheapShark miss some platforms? CheapShark and IsThereAnyDeal aggregate prices from storefronts that maintain API partnerships or accept data feeds. Platforms without those partnerships — itch.io, console storefronts, newer indie platforms — don't appear. Browser agents have no such dependency: they read the public storefront pages that any customer visits, regardless of whether the platform has an API program.
How does the price normalization handle "Free" or subscription-only titles? The parsePriceToUsd function returns 0 for "Free" (putting free games first in sorted output) and null for titles that are only available through a subscription service (Game Pass, PlayStation Plus). null prices sort to the bottom — a subscription title isn't a direct purchase price and shouldn't displace paid options.
What happens when a storefront doesn't carry the game?
null is the correct result — the game isn't listed on that platform. This is distinct from error: true (network failure or timeout). The comparison output shows null-result platforms as "not found" in the display layer, so users can see at a glance which stores carry the title.
How many platforms can run simultaneously? The Free plan (PAYG, 500 credits to start) supports 2 concurrent agent runs — 10 platforms run in 5 batches. The Starter plan ($15/mo, 10 concurrent) runs all 10 at once. Each agent step consumes 1 credit; a typical 10-platform query costs 30–60 credits total depending on site complexity and JavaScript rendering requirements.
Can I extend this to track prices over time? Yes. Save each run's output to a database keyed by game title, platform, and timestamp. Plotting priceUsd over time reveals sale patterns — which stores match Steam sale prices, how long post-launch discounts take to appear on console storefronts, and which platforms hold full price longest. Add a cron job to run daily checks for your watchlist.
Related Reading:
No credit card. No setup. Run your first operation in under a minute.