The crypto Fear & Greed Index from Alternative.me is one of those free endpoints everybody wires up in ten minutes. No key, no account, and it answers with access-control-allow-origin: *, so you can call it from the browser. I wired it up in ten minutes too, and then spent longer than that finding out why my numbers disagreed with theirs.
Here is the whole call:
const res = await fetch("https://api.alternative.me/fng/?limit=31");
const { data, metadata } = await res.json();
And here is what the first entry looked like on 25 September 2026:
{
"value": "71",
"value_classification": "Greed",
"timestamp": "1790294400",
"time_until_update": "33779"
}
Four things in that response tripped me up.
Every field is a string
value is "71", not 71. The timestamp and time_until_update are strings as well. Nothing breaks until you compare or sort, and then it breaks quietly: "9" > "71" is true in JavaScript, because strings compare character by character. Convert once, at the edge, and never touch the raw objects again.
async function fearGreed(days = 31) {
const res = await fetch(`https://api.alternative.me/fng/?limit=${days}`);
const { data, metadata } = await res.json();
if (metadata.error) throw new Error(metadata.error);
return data.map((d) => ({
value: Number(d.value),
label: d.value_classification,
day: new Date(Number(d.timestamp) * 1000).toISOString().slice(0, 10),
}));
}
The timestamp is midnight UTC, not the time of the reading
I pulled the full history with limit=0 and every one of the 3,155 timestamps falls exactly on 00:00 UTC. It names the day, not the moment the value was computed. Format it in local time and a reader in New York sees the wrong date: 1790294400 is 25 September in UTC and 24 September in America/New_York. If you display the date, pass timeZone: "UTC", or keep the day string from the function above.
time_until_update only works in the default format
The newest entry carries time_until_update, the number of seconds until the next refresh, which is handy for cache headers. Ask for readable dates with date_format=world and the timestamp becomes "25-09-2026", while time_until_update turns into "-1790260597". It is computed from the timestamp field, so once that field is a date string the result is garbage. Read it from a call without date_format, or ignore it and refresh a little after midnight UTC.
The history has holes, and indexes do not know about them
limit=0 returned 3,155 days, from 1 February 2018 to 25 September 2026. That span has 3,159 days. Four are missing: 14 to 16 April 2018 and 26 October 2024.
That matters the moment you compute "a week ago" by position. Counting back through an array assumes one entry per day, so any window that crosses a gap drifts by a day. Positions also invite an off by one of your own. I first built "a week ago" as history[history.length - 7] on a 30 item list reversed to oldest first, and that is six days back, not seven. The oldest item of a 30 item list is 29 days old, not a month. Both figures were plausible, and both were a day short.
Looking values up by date fixes both problems at once:
const series = await fearGreed(31);
const byDay = new Map(series.map((d) => [d.day, d.value]));
function daysAgo(n, from = series[0].day) {
const t = new Date(`${from}T00:00:00Z`);
t.setUTCDate(t.getUTCDate() - n);
return byDay.get(t.toISOString().slice(0, 10)) ?? null;
}
daysAgo(7); // 56 on 25 September 2026
daysAgo(30); // 65
A missing day now returns null instead of silently borrowing its neighbour, and you can decide what to show. Note that you need limit=31 for a value 30 days back, not 30.
One condition before you ship it
Alternative.me lets you use the data commercially, but only with the attribution placed right next to where you display it. That is one line of markup, and it is easy to forget when the widget works on the first try.
If you want to compare your numbers against a finished page, this fear and greed index chart reads the same endpoint and shows the week and month values alongside a 30 day history. On any given day, your daysAgo(7) and daysAgo(30) should match what it shows.
Top comments (0)