Dev.to WebDev 🛠 Dev 👁 0 📖 5 min read

How We Detect New Crypto Listings Across 70+ Exchanges

A coin's first hours on an exchange are when its price moves the most. Order books are thin, prices differ between exchanges, and whoever sees the listing first gets the best entry. Exchanges announce some listings in ad

A coin's first hours on an exchange are when its price moves the most. Order books are thin, prices differ between exchanges, and whoever sees the listing first gets the best entry. Exchanges announce some listings in advance, but many smaller exchanges simply switch the pair on.

I built a tracker that catches both for ArbiLayer. It watches the full market list of more than 70 centralized exchanges and perp DEXs and records the moment each coin appears. This post walks through how it works, and the three problems that made the naive version useless.

The naive idea: diff the market list

Every exchange publishes a public endpoint with all its trading pairs. Binance has exchangeInfo, Bybit has instruments-info, OKX has instruments, and so on. If a pair is in today's list and wasn't in yesterday's, it's new.

So the core is a set difference, per exchange and per market type:

// seen: venue -> { "spot:XYZ": firstSeenMs, "perp:XYZ": firstSeenMs, ... }
function diff(venue, tickers, seen) {
  const now = Date.now();
  const s = seen[venue] ??= {};
  const fresh = [];
  for (const t of tickers) {               // t = { kind: 'spot'|'perp', base, quote, last }
    const key = `${t.kind}:${t.base}`;
    if (s[key]) continue;                  // already known
    s[key] = now;
    fresh.push({ venue, ...t, at: new Date(now).toISOString() });
  }
  return fresh;
}

We run this every 15 minutes for every exchange. A listing becomes an event: exchange, spot or perpetual, base asset, quote, first traded price, time.

That's 20 lines. Making it trustworthy took a lot more.

Problem 1: the first pass says everything is new

The first time you read an exchange, every one of its 1,500 pairs is "new". The same thing happens when you add a new exchange later, or when a server restarts with an empty cache.

The fix is a baseline pass. The first full read of an exchange only fills the seen set and emits nothing. Only pairs that appear after the baseline count as listings.

const baseline = !counts[venue];           // never had a good pass for this venue
for (const t of tickers) {
  // ...
  if (!baseline) fresh.push(event);
}
counts[venue] = { spot: spotCount, perp: perpCount };

We persist seen and counts to disk, so a restart doesn't trigger a new baseline.

Problem 2: partial responses and API changes

Exchange APIs fail in creative ways. One endpoint times out, so you get spot but not perps. A paginated endpoint returns page 1 of 3. An exchange renames a field and your adapter suddenly parses half the market.

If you trust those responses, two things go wrong. Pairs missing from a bad response look new again on the next good one, so you get dozens of fake "listings" at once. Worse, if you dropped them from seen, you would report them twice.

Two guards fixed almost all of it:

// A response much smaller than the last good one is partial: skip this kind.
if (prevCount && count < prevCount * 0.8) continue;

// Dozens of "new" pairs from one exchange at once = an adapter change, not real listings.
if (fresh.length > 40) fresh.length = 0;

Real listings come in ones and twos per exchange per pass. A burst of 200 is always a bug or an API change. When that happens we still update seen, so the next pass is clean, but we don't publish anything.

Problem 3: same ticker, different coin

This one is specific to crypto and it's the most dangerous. Tickers are not unique. "ONE" is Harmony on one exchange and a different token on another. Meme coins reuse tickers constantly. Exchanges also decorate symbols: 1000PEPE, 1000SATS, 1MBABYDOGE are the same coins priced per 1,000 or per million units.

A listing alert with the wrong contract address is worse than no alert. Someone will buy the wrong token.

What we do:

Normalize symbols. Strip multiplier prefixes so 1000PEPE and PEPE group together, and ignore stablecoins and fiat as base assets.

const CLEAN = s => String(s || '').toUpperCase().replace(/^1000+|^1M(?=[A-Z])/, '');

Prefer a stablecoin quote for the reference price. If a coin trades as XYZ/USDT and XYZ/BTC, the USDT price is the one we compare.

Identify the coin by price, not by ticker. We keep a local copy of CoinGecko's coin list. When a ticker maps to several coins, we pick the one whose market price is within 30% of the price on the exchange. If none matches, the listing is shown as unverified, and we never guess a contract address.

function identify(base, price) {
  const candidates = coinsBySymbol.get(base) || [];
  if (candidates.length === 1) return candidates[0];
  const match = candidates.find(c => c.price > 0 && Math.abs(price / c.price - 1) < 0.3);
  return match || { ambiguous: candidates.length };   // shown as "check the contract"
}

This catches most cases where a well-known ticker gets reused by an unrelated token on a smaller exchange.

Announcements: the other half

Live detection tells you a pair is trading. Announcements tell you it will be. We poll the official listing feeds of Binance, OKX, Bybit, Bitget, KuCoin, Gate and MEXC every 10 minutes. Most of them have public JSON endpoints for their announcement pages.

We extract the tickers from the title ("Binance Will List XYZ (XYZ)"), store the opening time when the exchange gives one, and mark an announcement as live once the pair actually shows up in that exchange's market list. Linking the two is what makes the timeline useful: announced → spot opens → perpetual opens.

From events to pages

The tracker runs in a background worker. Every minute, if anything changed, it writes a snapshot that the website reads:

  • New listings: the latest events grouped by coin, with every exchange that lists it now.
  • A page per coin: the listing timeline, the contract when it's verified, and links to trade it.
  • A weekly report: every Monday the worker freezes the previous week. New coins, which exchanges listed the most, which exchange listed first, and how new coins performed from their first traded price.

We keep 90 days of events. That's enough for weekly reports and "listed in the last 30 days" counts per exchange, and it stays small enough to live in a JSON file.

Questions the data can answer

Once every listing has an exchange, a time and a first price, some interesting questions become simple queries:

  • Which exchanges list a coin first, and how long until the big ones follow?
  • How often does a perpetual market open before the spot market?
  • How does a new coin trade a week after its first print?

The weekly reports answer these with real numbers every Monday.

What I'd do differently

  • Start with the guards. Partial API responses are common, and every one of them becomes a burst of fake listings. The 80% rule belongs in the first version.
  • Store the first price with the event. You can't reconstruct it later, and it's the number everyone asks for.
  • Treat identity as a first-class problem. Matching by ticker alone is wrong often enough to matter.

The live tracker is at arbilayer.com/listings. Alerts go out on Telegram at t.me/arbilayer. Questions about the approach are welcome in the comments.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.