DEV Community

lulzasaur
lulzasaur

Posted on

Build a Marketplace Arbitrage Finder in 50 Lines of JavaScript

Build a Marketplace Arbitrage Finder in 50 Lines of JavaScript

I flip stuff on the side. Not full-time — more like "I check prices when I'm bored and occasionally make $50 on a guitar pedal someone underpriced on Reverb."

The tedious part was always manually searching the same item across multiple platforms. So I wrote a script that does it for me. It checks TCGPlayer and Reverb, compares prices, and spits out anything with a meaningful price gap.

Here's the whole thing.

The Idea

Price arbitrage = same item listed at different prices on different platforms. A Boss DS-1 pedal for $35 on Reverb might be $55 on eBay. A Charizard card listed at $12 on one platform might sell for $20 on another.

The trick is finding those gaps before everyone else does.

Setup

You need Node.js and a RapidAPI key (free tier works for testing).

mkdir arbitrage-finder && cd arbitrage-finder
npm init -y
npm install node-fetch
Enter fullscreen mode Exit fullscreen mode

Create a .env file:

RAPIDAPI_KEY=your_key_here
Enter fullscreen mode Exit fullscreen mode

The Script

Here's find-deals.mjs — the whole thing:

import fetch from 'node-fetch';

const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY;
const RAPIDAPI_HOST = 'marketplace-price-tracker.p.rapidapi.com';

const headers = {
  'x-rapidapi-key': RAPIDAPI_KEY,
  'x-rapidapi-host': RAPIDAPI_HOST,
};

async function searchReverb(query) {
  const url = `https://${RAPIDAPI_HOST}/reverb/search?query=${encodeURIComponent(query)}&limit=10`;
  const res = await fetch(url, { headers });
  const data = await res.json();
  return (data.results || []).map(item => ({
    title: item.title,
    price: parseFloat(item.price?.amount || 0),
    platform: 'Reverb',
    url: item.url,
  }));
}

async function searchTCG(query) {
  const url = `https://${RAPIDAPI_HOST}/tcg/search?query=${encodeURIComponent(query)}&limit=10`;
  const res = await fetch(url, { headers });
  const data = await res.json();
  return (data.results || []).map(item => ({
    title: item.name || item.title,
    price: parseFloat(item.marketPrice || item.price || 0),
    platform: 'TCGPlayer',
    url: item.url,
  }));
}

async function findArbitrage(query, minGapPercent = 20) {
  console.log(`\nSearching: "${query}"\n`);

  const [reverbResults, tcgResults] = await Promise.all([
    searchReverb(query),
    searchTCG(query),
  ]);

  const allListings = [...reverbResults, ...tcgResults]
    .filter(item => item.price > 0)
    .sort((a, b) => a.price - b.price);

  if (allListings.length < 2) {
    console.log('Not enough listings to compare.');
    return;
  }

  const lowest = allListings[0];
  const highest = allListings[allListings.length - 1];
  const gap = ((highest.price - lowest.price) / lowest.price) * 100;

  console.log(`Lowest:  $${lowest.price.toFixed(2)} on ${lowest.platform}`);
  console.log(`Highest: $${highest.price.toFixed(2)} on ${highest.platform}`);
  console.log(`Gap:     ${gap.toFixed(1)}%\n`);

  if (gap >= minGapPercent) {
    console.log(`🔥 DEAL: Buy on ${lowest.platform} → sell on ${highest.platform}`);
    console.log(`   Potential profit: $${(highest.price - lowest.price).toFixed(2)}`);
  } else {
    console.log('No significant arbitrage found.');
  }
}

// Run it
const query = process.argv[2] || 'Boss DS-1';
findArbitrage(query);
Enter fullscreen mode Exit fullscreen mode

Running It

RAPIDAPI_KEY=your_key node find-deals.mjs "Charizard Base Set"
Enter fullscreen mode Exit fullscreen mode

Output looks like:

Searching: "Charizard Base Set"

Lowest:  $12.50 on TCGPlayer
Highest: $24.99 on Reverb
Gap:     99.9%

🔥 DEAL: Buy on TCGPlayer → sell on Reverb
   Potential profit: $12.49
Enter fullscreen mode Exit fullscreen mode

Making It Useful

The basic version above is a starting point. Here's how I actually use it:

1. Batch search from a watchlist:

const watchlist = [
  'Boss DS-1',
  'MXR Phase 90',
  'Ibanez Tube Screamer',
  'Pokemon Base Set Booster',
  'Charizard VMAX',
];

for (const item of watchlist) {
  await findArbitrage(item, 15);
  await new Promise(r => setTimeout(r, 1000)); // rate limit
}
Enter fullscreen mode Exit fullscreen mode

2. Run on a cron:

# Check every 6 hours
0 */6 * * * cd /path/to/arbitrage-finder && node find-deals.mjs "Boss DS-1" >> deals.log
Enter fullscreen mode Exit fullscreen mode

3. Add more marketplaces:

The same API also has endpoints for OfferUp, Poshmark, eBay, Mercari, and a bunch of others. Same pattern — just swap the route:

// Same headers, different endpoint
const url = `https://${RAPIDAPI_HOST}/offerup/search?query=${query}`;
Enter fullscreen mode Exit fullscreen mode

What I Learned

  • Timing matters more than price gaps. A 30% gap means nothing if the item takes 3 months to sell.
  • Condition is everything. "Used - Good" on one platform might be "Fair" on another. Always check photos.
  • Shipping eats profits. Factor in $5-15 shipping per flip before getting excited about a $10 gap.
  • The real edge is speed. Underpriced items get snatched fast. Running this on a cron and checking results on your phone is the actual workflow.

The API

I built the marketplace search API for my own flipping workflow. It's on RapidAPI with a free tier (50 requests/month) if you want to try it: Marketplace Price Tracker.

Supports 20+ marketplaces. The whole thing runs on Apify actors behind a Railway backend — I wrote about the architecture in a previous post.

If you've got a better arbitrage strategy or want to add a marketplace I'm missing, drop a comment.

Top comments (0)