DEV Community

Nyx Lesende
Nyx Lesende

Posted on

Build an XRPL Token Screener in About 50 Lines

There are 20,263 tokens issued on the XRP Ledger and only 1,682 of them traded today. A screener is how you tell those two groups apart.

Here is a complete one. No dependencies, runs on any Node 18+.

The whole thing

const API = 'https://api.xrpl.to/v1';

async function get(path, attempt = 1) {
  const r = await fetch(API + path);
  if (r.ok) return r.json();
  if ((r.status === 429 || r.status >= 500) && attempt < 4) {
    await new Promise(s => setTimeout(s, attempt * 4000));
    return get(path, attempt + 1);
  }
  throw new Error(`HTTP ${r.status}`);
}

function score(t) {
  // Volume per unique trader — high values mean few accounts moving a lot,
  // which is what wash trading looks like from the outside.
  const perTrader = t.vol24hxrp / Math.max(t.uniqueTraders24h, 1);
  // Trustlines that no longer hold a balance are historical interest.
  const stickiness = t.holders / Math.max(t.trustlines, 1);
  return { perTrader, stickiness };
}

async function screen() {
  const { tokens } = await get('/tokens?limit=100&sortBy=vol24hxrp&sortType=desc');

  return tokens
    .filter(t => t.holders >= 1000)          // ignore dust
    .filter(t => t.uniqueTraders24h >= 20)        // ignore one-account markets
    .map(t => ({ name: t.name, issuer: t.issuer, ...score(t),
                 holders: t.holders, vol: t.vol24hxrp }))
    .filter(t => t.stickiness > 0.15)             // most holders still hold
    .sort((a, b) => a.perTrader - b.perTrader);   // broadest participation first
}

screen().then(rows => {
  for (const r of rows.slice(0, 15)) {
    console.log(
      r.name.padEnd(12),
      String(r.holders).padStart(7),
      r.perTrader.toFixed(0).padStart(9),
      r.stickiness.toFixed(2)
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

What each filter is actually doing

holders >= 1000 — on the XRPL, holding a token requires an explicit trustline, and a trustline locks an XRP reserve. Every holder paid something to be counted, so this threshold means more here than a wallet count would on a contract chain.

uniqueTraders24h >= 20 — volume with three accounts behind it is not a market. This one filter removes most of the noise.

stickiness > 0.15 — the ratio of current holders to total trustlines ever opened. A very low value means people opened a line, traded, and left.

Sorting by perTrader ascending — counterintuitive, but low volume-per-trader means many accounts each trading modest size. High values mean a handful of accounts churning, which is the pattern you want to look at twice.

What it deliberately does not do

It does not read issuer flags. Two things matter and are not in this endpoint: whether the issuing account is blackholed (no further supply can be minted) and whether the issuer kept freeze authority. Both are on-ledger and worth adding via a rippled account_info call before you trust any output.

It also does not measure order-book depth. A token can pass every filter above and still have no bid you could actually sell into. Depth is the check that matters most and the one people skip.


Live prices, holder counts, trustlines and order-book depth for every XRPL token are at xrpl.to.

Top comments (0)