Build a Crypto Screener with One API Call
Every result for this search is a weekend data pipeline: websocket collectors, a candle database, a scheduler, and a project that dies when the collector needs a babysitter. Here is the other version: one request returns the whole market already ranked, a second reads the regime, and about forty lines of Python make a terminal screener that refreshes every 30 seconds.
Search "build a crypto screener" and the tutorials agree on the architecture: subscribe to exchange websockets, collect candles into a database, compute indicators on a schedule, then query your own tables for movers. That is a data pipeline with a hobby attached, and most of them die the first weekend the websocket drops and the database gaps. This post takes the shortcut we built for ourselves: the Osiris Developer API runs that pipeline as its day job, and the rankings endpoint hands you the finished product, every pair on the board sorted by the metric you name, in one request. We are going to turn it into a terminal screener in about forty lines of Python: one call for the ranked list, one for market regime, two crossed for a real multi-factor screen, refreshing on the same clock the engine itself runs on. The bot tutorial ended by naming rank intersection as the natural next step. This is that step, done properly.
What you need
Python 3, the requests package, and an API key, created in Settings → Developer on the Developer plan and kept in an environment variable. Every file in this series opens the same way, with a helper that talks to the API and sleeps exactly as long as a 429 tells it to; the bot post explains that branch in detail, so here it is without the sermon:
import os
import time
import requests
BASE = "https://osiristrader.com/api"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["OSIRIS_KEY"]
def osiris(path, **params):
while True:
res = session.get(BASE + path, params=params, timeout=10)
if res.status_code == 429:
time.sleep(int(res.headers.get("Retry-After", "5")))
continue
res.raise_for_status()
return res.json()
Everything below is plain GET requests through that function. The whole surface is documented at developers/introduction if you want to wander off the path.
One request is already a screener
The rankings endpoint is the Osiris screener grid as JSON. Name one of seven categories (volatility, volsurge, volume, trades, correlation, funding, trend) and a metric from its whitelist, and the response is every pair that currently has that number, sorted. We start with volatility's headline metric, NATR: average true range as a percent of price, which makes pairs directly comparable. A reading of 0.9 on the 5m means a typical five-minute bar spans close to a percent, and big NATR is where something is happening right now.
movers = osiris("/v1/rankings/volatility", metric="NATR", interval="5m", limit=15)
for row in movers["data"]:
print(f"{row['symbol']:14} {row['value']:5.2f}")
That prints something like ZENUSDT 1.31, 1000PEPEUSDT 0.94, WIFUSDT 0.88: the fifteen most volatile pairs on the whole board for one request, young listings without enough history simply absent rather than present with fake zeros. Typo the metric and the 422 answers with the allowed set, which makes the endpoint self-documenting in practice. Two categories rank by magnitude instead of sign, funding and trend, because a violently negative funding rate is at least as interesting as a positive one; read the sign of value for direction.
Read the room before you read the list
Fifteen fast movers mean one thing when the board is green and a different thing when 300 of 500 pairs are red and your movers are just the fastest sinkers. So the screener gets a header. /v1/market/overview compresses the whole venue into breadth counts, average change, BTC itself, and our fear and greed composite, which is computed entirely from the live futures tape and rescored every 10 seconds, 0 extreme fear to 100 extreme greed. One call, one line of terminal:
def regime():
o = osiris("/v1/market/overview", count=1)
m = o["overview"]
fng = o["widgets"]["fng"]["value"]
return (f"BTC {m['btc']['price']:,.0f} ({m['btc']['change']:+.2f}%) | "
f"{m['up']}/{m['pairs']} up, avg {m['avgChange']:+.2f}% | "
f"F&G {fng}")
The composite's six components ride in the same payload, and their disagreements are often worth more than the headline: funding scoring greedy while breadth scores fearful means leveraged longs are paying up on an advance that fewer and fewer pairs are joining, and noticing it costs no extra request. Reading market context is the human version of this header; the API just turns it into a variable.
Two rankings crossed make a real screen
A ranked list is a leaderboard. A screen is a question with an AND in it: which pairs are moving the most AND being bought while they do it? Volatility answers the first half. The second comes from the trades category's FLOW metric, net taker flow of the current bar, buys minus sells over total volume, running from -1 to +1: the sign of aggression right now. Pull both rankings deep, intersect the symbol sets, keep what survives:
def screen(top=10):
vol = osiris("/v1/rankings/volatility", metric="NATR", interval="5m", limit=100)
flow = osiris("/v1/rankings/trades", metric="FLOW", interval="5m", limit=100)
natr = {row["symbol"]: row["value"] for row in vol["data"]}
return [(r["symbol"], natr[r["symbol"]], r["value"])
for r in flow["data"]
if r["symbol"] in natr and r["value"] > 0][:top]
The ordering comes free: the trades ranking arrives sorted by FLOW, highest first, so the list reads "of the hundred most volatile pairs, the ones being bought hardest, in that order." One venue note: the trades category only means something on Binance, the default, because the other venues publish no public trade tape. And swap either leg to taste: volsurge's SCORE for abnormality instead of raw range, SIZE_GROWTH to demand the buying arrive in large prints, or a BTC correlation ceiling to drop pairs that are just drafting behind bitcoin, which the bot tutorial builds from the bulk metrics call. An empty result is a result: in a calm or evenly sold market nothing is both moving and being bought, and a screener that admits that beats one that pads the table.
Put it on the engine's clock
Now the loop, and the number 30 is not a style choice. The metrics behind rankings recompute on a 30 second sweep, and the rate limits page spells out the consequence: polling faster than the data's cadence returns identical bytes. A 5 second loop would spend six times the requests to watch the same table not change.
while True:
rows = screen()
line = regime()
os.system("cls" if os.name == "nt" else "clear")
print(time.strftime("%H:%M:%S"), line)
print(f"{'PAIR':14} {'NATR':>6} {'FLOW':>6}")
for symbol, natr, flow in rows:
print(f"{symbol:14} {natr:6.2f} {flow:+6.2f}")
if not rows:
print("nothing is both moving and getting bought")
time.sleep(30)
Budget check, because adults count: three requests per cycle is 8,640 a day against the plan's 50,000, and six a minute against the 120 burst ceiling. You could run five of these side by side on different factor pairs and still use under half of quota. The screener you were quoted a month of plumbing for costs under a fifth of a free key's budget.
The push upgrade
There is a point where polling stops being the right shape, and you do not reach it by shrinking the sleep. The WebSocket stream carries this exact screener: subscribe with a category and metric and the server pushes the whole sorted view roughly every 3 seconds, up to ten screener subscriptions per connection, live candles on the same socket. The terminal table becomes a push consumer with the request loop deleted, your REST budget drops back to near zero, and the table updates faster than any polite polling could. When 30 seconds starts feeling slow, that is the door. It was never "poll harder."
Forty lines, three endpoints, and the part of screener-building that usually eats a month, the data pipeline, is someone else's production system. From here the branches are obvious: hydrate the survivors with the bulk metrics call and filter on BTC correlation, point price alerts at the names the screen keeps surfacing and let the webhook wake your code, both covered in the bot tutorial, or re-rank by any of the thirty-plus fields in the API docs. The full script from this post lives in our examples repo on GitHub, next to everything from the bot post. One honest line to close, because a screener is a candidate machine, not a trade machine: it tells you where the action is, never what the action is worth at the level, and that second question is still yours to answer. The pipeline was never the edge. The reading was.