DEV Community

Tinyfishie
Tinyfishie

Posted on • Originally published at tinyfish.ai

Build a Video Game Price Comparison Tool Across 10 Platforms

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 multi-platform game price comparison tool using TinyFish agents. Your configured storefronts are checked simultaneously — major PC storefronts, indie platforms, regional stores, or any public game store with a search page. 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:

  1. Define your platform list with store search URLs
  2. Write a goal prompt that extracts price, discount, and link consistently
  3. Run all 10 agents in parallel with Promise.allSettled
  4. Normalize currencies and rank by discounted price

Why Building Beats Using CheapShark or IsThereAnyDeal

CheapShark and IsThereAnyDeal are good starting points. They cover major PC storefronts reliably — and their APIs are free. For straightforward PC game price lookup, they're often sufficient.

Browser agents make sense when:

  • Your target platform isn't indexed — regional stores, newer indie platforms, and console storefronts have partial or no CheapShark coverage
  • You need real-time data — API-backed aggregators cache prices, sometimes aggressively. During major sale events, cached prices can lag the actual storefront by hours
  • You're building beyond price lookup — availability status, regional pricing differences, bundle detection, and DLC pricing require reading the actual store page rather than an API summary

The engineering tradeoff is straightforward:

Approach Best for Limitation
CheapShark / IsThereAnyDeal API Quick PC storefront lookup, free, no setup Doesn't cover all storefronts; 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 access patterns. The Fetch API (api.fetch.tinyfish.ai) retrieves a page at a known URL — free (0 credits). The Agent API (agent.tinyfish.ai) handles storefronts requiring a search step — 1 credit per step. For storefronts with stable product page URLs, Fetch API can retrieve the price directly (free, 0 credits). For storefronts that require a search query, Agent API is required (1 credit per step). Where you have direct product URLs, Fetch API is the right choice — free on all plans.

Video Game Price Comparison: Running Parallel Agents

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_here
Enter fullscreen mode Exit fullscreen mode
import { TinyFish } from "@tiny-fish/sdk";

const client = new TinyFish(); // reads TINYFISH_API_KEY from env

// Replace these with the storefronts you want to compare.
// Before adding any storefront, check its Terms of Service regarding automated access.
// Many storefronts have public APIs (e.g. CheapShark, IsThereAnyDeal) that are
// the preferred integration method — use browser agents only for stores without APIs.
const PLATFORMS = [
  { name: "Storefront A", url: "https://example-store-a.com/search?q=" },
  { name: "Storefront B", url: "https://example-store-b.com/games/search?query=" },
  { name: "Storefront C", url: "https://example-store-c.com/search?term=" },
  // Add more storefronts here — any public search URL works
];

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. "Storefront A" or "Storefront B")
  - 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 }),
  }));
}
Enter fullscreen mode Exit fullscreen mode

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 TinyFish agents querying Steam, GOG, Epic, Xbox, PlayStation, and five other game storefronts simultaneously, returning normalized USD prices

Normalizing Prices Across 10 Currencies and Formats

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

Three normalization decisions worth explaining:

  1. Currency conversion at query time. Build the lookup table once, apply it at normalization — don't call an exchange rate API per result. In production, fetch rates daily and cache them.
  2. "Free" returns 0. Sorting by price puts free games at the top, which is almost always the right behavior for a deals tool.
  3. null for subscription/unavailable. A subscription-only game shouldn't appear as $0 — it's not a direct purchase price. null price pushes it to the bottom of sorted output.

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}`);
});
Enter fullscreen mode Exit fullscreen mode

Run with npx ts-node index.ts.


Building a Deal Alert Bot

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");
Enter fullscreen mode Exit fullscreen mode

Extension: Sale season monitor. Run the comparison daily during major sale periods (typically mid-June and late November for PC storefronts). When any game in your watchlist drops more than 50%, trigger the webhook immediately. Track prices over time to see which stores run simultaneous sales 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.


FAQ

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. Add any storefront with a public search page by appending an entry to the PLATFORMS array — including regional stores, indie platforms, and storefronts not covered by existing aggregator APIs.

When should I use browser agents instead of CheapShark API? Use this decision framework:

If you need → Use
Storefronts not covered by aggregator APIs → Browser agents (configure your own platform list)
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 (not a direct purchase price). 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 storefronts discount simultaneously, how long post-launch discounts take to appear across different store types, and which platforms hold full price longest. Add a cron job to run daily checks for your watchlist.


Deploy This with a Free Account

The complete workflow above runs on TinyFish's free tier. 500 free steps, no credit card — enough to deploy this project and validate it against real data before choosing a plan.

Get your free API key →

Related Reading:


Want to scrape the web without getting blocked? Try TinyFish — a browser API built for AI agents and developers.

Top comments (0)