Fund screener

Build a fund screener on structured factsheet data

A fund screener lets users narrow thousands of share classes to a shortlist by asset class, region, risk rating, fees and size. This page covers what a screener needs from the underlying data, how to build the universe with FundFacts API and how to keep it fresh without burning quota.

The problem

A screener is a filter over a universe. The hard part is not the UI; it is having consistent, comparable attributes for every share class in the universe. Factsheets from different fund houses put the asset class in different places, express fees as TER or ongoing charges, quote fund size in fund currency or share-class currency, and publish risk indicators on the SRRI or SRI scale depending on the document.

If you build the attribute table by hand, you end up with a spreadsheet that is out of date the week it is finished. If you build it from raw PDFs, you spend your time on extraction rather than on the product.

What you need from the data

A useful screener typically filters on a handful of dimensions and sorts on a few more:

  • Asset class and sub-asset (equity, fixed income, multi-asset; large cap, high yield, and so on)
  • Region focus and sector tilt
  • Risk rating on the 1–7 scale used by the KID
  • Fees, usually the TER or ongoing charges figure
  • Size, as assets under management
  • Distribution policy (accumulating or distributing) and share-class currency
  • Inception date, so users can exclude funds without a track record

Every one of these must be normalised the same way across issuers, or the filter silently excludes funds that were described differently.

How it works with FundFacts API

Each call to GET /api/v1/funds/{isin} returns the normalised attributes under data. For a screener you seed the universe with the ISINs you want to cover, load each one once, and store the fields you filter on in your own database. The API is the source of truth; your table is a read model.

typescript
type ScreenerRow = {
isin: string;
name: string;
assetClass?: string;
region?: string;
riskRating?: number;
ter?: number;
aum?: number;
distribution?: string;
expiresAt: string;
};
export async function loadRow(isin: string): Promise<ScreenerRow> {
const res = await fetch(`https://fundfactsapi.com/api/v1/funds/${isin}`, {
headers: { Authorization: `Bearer ${process.env.FUNDFACTS_API_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} for ${isin}`);
const json = await res.json();
const d = json.data;
return {
isin: json.isin,
name: json.name,
assetClass: d.keyFacts?.assetClass,
region: d.profile?.regionFocus,
riskRating: d.riskRating,
ter: d.headlineMetrics?.ter,
aum: d.headlineMetrics?.aum,
distribution: d.keyFacts?.distribution,
expiresAt: json.expiresAt,
};
}

Filtering then becomes a plain SQL query or an in-memory filter over your rows. The profile block is designed for exactly this: profile.kind, profile.category, profile.riskBand, profile.regionFocus and profile.sectorTilt are short, stable labels intended for facets, while keyFacts holds the raw values you display.

A cold ISIN takes one to three minutes to load the first time because the documents have to be read from the fund house; subsequent calls return from cache in milliseconds. Seed the universe in a background job, not in a request handler.

Design notes

Caching. Data is refreshed every 24 hours. Store expiresAt next to each row and re-load only rows whose expiresAt has passed. A universe of a few hundred ISINs refreshed daily fits comfortably in the Pro plan's 100 requests per day if you spread the refresh across the day and prioritise the funds users actually open.

Quota. Read X-RateLimit-Remaining on every response and pause the refresh job when it drops below a floor you choose. A 429 rate_limited response includes X-RateLimit-Reset; sleep until then rather than retrying immediately.

Errors. Treat 404 fund_not_found as a permanent skip for that ISIN (log it, review weekly) and 502 upstream_error as a transient failure to retry on the next cycle. Validate ISINs client-side before spending a request; see what is an ISIN.

Missing values. Not every document publishes every field. Filter with null-safe comparisons and show "n/a" rather than treating a missing TER as zero, which would wrongly rank the fund as the cheapest.

Disclaimers. A screener presents facts, not recommendations. Show dataAsOf beside the figures, label the page "not investment advice", and avoid ranking language ("best", "top") that implies a judgement about fund quality.

For a deeper walkthrough of comparing funds side by side, read build a fund comparison tool.

Frequently asked questions

Can I query the API with filters instead of one ISIN at a time?

No. The endpoint takes a single ISIN and returns the full structured factsheet for that share class. A screener is built by loading the ISINs in your universe once, storing the attributes you filter on in your own database and querying that table. This keeps filtering fast, lets you add your own fields and means the API is only called when a record has expired.

How many ISINs can I keep fresh on each plan?

Data refreshes every 24 hours, so a full daily refresh needs one request per ISIN per day. Free covers 10 requests a day, Pro 100, Scale 1,000 and Enterprise is metered per request. In practice you can cover more than that by refreshing popular funds daily and the long tail less often, since expiresAt tells you exactly which rows are stale.

Why does a fund show no TER or no fund size?

The values are read from the documents each fund house publishes. If a factsheet or KID does not state a figure, the field is absent rather than guessed. Treat missing values as unknown in your filters: exclude them from a cheapest-first sort, and display n/a rather than zero so users are not misled about cost or size.

Are the risk ratings comparable across fund houses?

The riskRating field is the 1–7 indicator published in the fund's KID, so it follows the same regulatory methodology across issuers. Note that UCITS KIIDs use the SRRI and PRIIPs KIDs use the SRI, which can differ by a notch for the same fund. The profile.riskBand label gives a coarser grouping that is convenient for a facet filter.

Build your fund screener on FundFacts API

Free plan with 10 requests a day to prototype; paid plans add the AI companion kit that scaffolds the integration for you.