‹ Build a Screener Lesson 3 of 17
Contents Lesson 3 of 17

7 min read · practitioner

Your first screen, through the proxy

Time to cut a market down to a handful of names. Same rule as always: the request goes through your own server, never from the browser.

Course 1 pointed you at the academy's reference for choosing a column. Use it the same way here, one level up: /screener is one of roughly ninety endpoints, and the reference lists each against the market question it answers. Confirming that this is the right one takes a minute; discovering it was not, after you have built a table around its response shape, takes an afternoon.

The shape of a screen

/screener takes filters, a sort and a limit, and answers in one call:

/screener
  ?filters=[["market_capitalization",">",1000000000],["exchange","=","us"]]
  &sort=market_capitalization.desc
  &limit=2

The filters are JSON, url-encoded, and each one is a triple: field, operator, value. Through your proxy the same request is:

const filters = JSON.stringify([
  ["market_capitalization", ">", 1_000_000_000],
  ["exchange", "=", "us"],
]);
const res = await fetch(
  `/api/proxy/screener?filters=${encodeURIComponent(filters)}` +
    `&sort=market_capitalization.desc&limit=2`,
);

Run on 2026-08-25 that returned, exactly:

{"data":[
  {"code":"NVDA","name":"NVIDIA Corporation","last_day_data_date":"2026-08-24",
   "adjusted_close":208.48,"refund_1d":-6.24,"refund_1d_p":-2.91,
   "market_capitalization":5049593888768,"earnings_share":6.34,
   "dividend_yield":0.0002,"sector":"Technology","industry":"Semiconductors",
   "avgvol_1d":135187288,"avgvol_200d":164865302.27},
  {"code":"AAPL","name":"Apple Inc.", "...": "..."}
]}

Three things in that response worth stopping on

It is wrapped. The rows are under a data key. Most endpoints in this API answer with a bare array; this one does not. Code that does rows.map(...) on the response object gets nothing and throws nothing, which is the worst kind of wrong.

last_day_data_date is 2026-08-24, not today. A screen runs on the last completed session, not on live prices. That is correct behaviour and it must appear on your screen, because a person reading a list of "cheapest stocks" deserves to know it is yesterday's cheapest.

dividend_yield is 0.0002. Not 0.02%. Not 2%. It is a fraction: 0.0002 means 0.02%. Render it raw beside a percentage sign and you have overstated every yield by a hundred times. Unit 3 lesson 12 is about exactly this class of error; note it now, because you will meet it in about ten minutes.

One call, and what it costs

A screen is a single request regardless of how many rows come back, and limit=500 was verified to return 500 rows in one call on 2026-08-25.

That makes screening cheap per result and expensive per attempt — and attempts are what you make while tuning filters. Twenty tweaks while getting a screen right is twenty calls, which on a small allowance is a real bite out of a morning. Your meter is on screen already; watch it while you experiment.

Read the errors, they are unusually specific

Ask for a field that does not exist and the API tells you precisely which part of your filter is wrong:

{"errors":{"filters.0.field":["The selected filters.0.field is invalid."]}}

Note the shape: an errors object keyed by the path into your input. This is a different error envelope from the plain message other endpoints return, and code that only knows how to read one of them will show a person "undefined". Handle both, and surface the path — "filter 1: unknown field" is a message somebody can act on.

The finance behind it

Your filters compare companies to each other, which is only fair under conditions this lesson does not cover: How do you compare two companies fairly?

Try it now

Get one screen rendering through your proxy with two filters of your own. Then break it deliberately: misspell a field name and look at what your UI shows. If it says "something went wrong" rather than naming the filter, you have thrown away the most useful error message in this API.