This guide is the one-line /api/token lookup for a chat bot. A product card, a terminal panel, an
app — or any new integration asking "is anyone building this token?" — starts with the Partner Card,
GET /api/v1/builder, HEY's canonical by-contract read (/developers/partners).
The three by-contract reads print the same status words (since 2026-10-18). The two answer an address HEY
has not published differently: here it is a 200 with status: "unknown", there a 404 with a
scan_url.
Someone pastes a contract address into a chat. You want one line that says whether anyone is actually building it. This is how, end to end, in one HTTP call and no account.
The one call
GET https://heyresearch.xyz/api/token/4663/{contractAddress}
No key. No account. No signup. 120 requests a minute per client, cached 60 seconds at the
edge (the two partner cards, /api/v1/builder and /api/v1/scan, are private, max-age=60 since
2026-10-05 so every partner call is attributed and counted), and readable from a browser as well as a server — access-control-allow-origin: * is set
and the preflight is answered.
Try it now, on a project that is shipping:
curl -s https://heyresearch.xyz/api/token/4663/0xebb4c5b97e4117e30ec82ce025e6f21dded05436
What comes back
An example answer, in the shape the call returns as of 10 October 2026 — your call returns today's values. A code ship's title names its week, absolute, never "since":
{
"chainId": 4663,
"contractAddress": "0xebb4…5436",
"status": "published",
"project": {
"slug": "darkroute",
"name": "DarkRoute",
"symbol": "dark",
"url": "https://heyresearch.xyz/project/darkroute",
"activityStatus": "SHIPPING",
"activityLabel": "Shipping",
"activityHelp": "Shipped something meaningful in the last 7 days.",
"shipsLast30Days": 4,
"lastShipAt": "2026-09-16T02:38:59.000Z",
"lastShip": {
"title": "Code changes, week of 2026-09-14 – 2026-09-20 · 3 commits",
"publishedAt": "2026-09-16T02:38:59.000Z",
"sourceUrl": "https://github.com/darkrouteRH/contracts"
},
"deployedAt": "2026-09-08T09:25:46.000Z",
"badgeUrl": "https://heyresearch.xyz/badge/darkroute.svg"
},
"scanUrl": "https://heyresearch.xyz/scan?address=0xebb4…5436",
"disclaimer": "…"
}
| Field | What it is |
|---|---|
activityStatus |
One of SHIPPING, ACTIVE, QUIET, DORMANT, RESUMED, UNKNOWN. Derived from what a project ships, never from price. |
activityLabel |
The same status in words. Use this rather than mapping the enum yourself — see below. |
shipsLast30Days |
Ship records in the window the field names, counted the way the project page's timeline counts them. Always sent, 0 included where HEY measured nothing — a legacy field kept as it was (machine rule 10). Print meaningfulShipsLast30Days instead, and only when activityMeasured is true. |
activityMeasured |
Whether HEY measured this project's building at all (2026-09-26). false: HEY has nothing it can read building from, so a zero beside it is not a finding. |
meaningfulShipsLast30Days |
Meaningful ships in 30 days by the rule behind the status. Absent, never 0, when HEY did not measure and holds none. |
lastShip |
The most recent one, with sourceUrl — the actual commit, release or post it was read from. Link it. |
deployedAt |
When the contract went on chain, read from the block. Absent where HEY has not read one. |
badgeUrl |
A ready-made SVG: name, status, last ship. Drop it in the card if it fits. |
url |
The page behind all of it. Always render this. |
The three answers you must handle
| Case | HTTP | Do this |
|---|---|---|
status: "published" |
200 | Render the line. |
status: "unknown" |
200 | HEY has no published page. There is no project. Offer scanUrl. |
error |
400 | Not an address, or not this chain. Say nothing about the token. |
Why unknown is a 200 and not a 404. Most addresses pasted into a group chat are not
indexed projects. A 404 would make your ordinary case an error branch. So the shape is uniform,
you check one field, and scanUrl gives you somewhere to send the person: HEY reads the chain
live for an address it has never seen.
An address HEY holds but has not reviewed also answers unknown. An unreviewed launch record
is not a project, and this endpoint will not pretend otherwise.
The bot, in about thirty lines
const HEY = 'https://heyresearch.xyz';
async function heyLine(address) {
const r = await fetch(`${HEY}/api/token/4663/${address}`);
// 400 means the address is not one, or not this chain.
if (r.status === 400) return null;
if (!r.ok) return null; // stay quiet on a hiccup
const d = await r.json();
if (d.status !== 'published') {
return `HEY: not indexed yet · read the chain → ${d.scanUrl}`;
}
const p = d.project;
// A count only where HEY measured building: without a source to read, a zero is not a finding.
const n = p.meaningfulShipsLast30Days;
const ships = p.activityMeasured && typeof n === 'number' ? ` · ${n === 1 ? '1 ship' : `${n} ships`} in 30d` : '';
const when = p.lastShipAt ? `, last ${daysAgo(p.lastShipAt)}` : '';
// p.activityLabel is HEY's own wording ("No builder source linked" where HEY has nothing to read). Do not invent your own.
return `HEY: ${p.activityLabel}${ships}${when} → ${p.url}`;
}
function daysAgo(iso) {
const days = Math.floor((Date.now() - new Date(iso)) / 86400000);
return days === 0 ? 'today' : days === 1 ? 'yesterday' : `${days}d ago`;
}
Which renders as:
HEY: Shipping · 4 ships in 30d, last today → heyresearch.xyz/project/darkroute
HEY: No builder source linked → heyresearch.xyz/project/bitdeer-ai
HEY: not indexed yet · read the chain → heyresearch.xyz/scan?address=0x7655…d78a
Never print shipsLast30Days on its own (corrected 2026-10-18). This guide's bot once printed
${p.shipsLast30Days} ships in 30d for every project, so a bot copied from it told readers a project
"shipped 0" where HEY had no repository, changelog or feed to read at all — most token projects. A
count is a finding only when activityMeasured is true; print meaningfulShipsLast30Days then
(the figure behind the status), and print nothing where it is false.
Four things not to do
Do not translate the status yourself. Use activityLabel. Left to themselves, integrators
turn DORMANT into dead and UNKNOWN into suspicious. Dormant means HEY has observed no
public activity for a long time. It is not the same as abandoned, and unknown is a gap in our
knowledge, not a fault of the project. The labels ship in the payload so nobody has to guess.
Do not present it as a risk reading. HEY gives no score, no grade and no safe/unsafe
verdict, anywhere, by design — the moment there is one number on a card, people read it as a
rating. If you want a risk signal, put one from a tool that actually does that work beside
our line. Honeypot: clean · HEY: Shipping, 4 ships in 30d is a better card than either of us
alone, and neither is pretending to be the other.
Do not drop the link. Whatever you render, carry url. Someone who sees a HEY line should
be one tap from the evidence behind it. This is the one condition of use.
Do not poll POST /api/scan in a hot path. That second, heavier endpoint reads the chain
live for an address nobody has indexed. It allows ten requests an hour, is never cached,
and spends a budget HEY's scheduled indexing needs first. Link people to scanUrl instead —
that is a page, and it costs you nothing.
Limits, caching and failure
- 120 requests a minute per client. Over that,
429with aretry-afterin seconds. - Answers are cached 60 seconds and served stale for five minutes while they refresh. Caching on your side beyond that buys nothing; the catalogue moves on HEY's worker schedule, not per request.
- Dates are ISO 8601, UTC, always.
- Absent means unknown. A field HEY has no answer for is left out — never sent as
nullor0. Do not fill a gap with a zero: a missingdeployedAtmeans HEY has not read one, not that the contract was deployed at the epoch. Three legacy counts are the exception (corrected 2026-10-18): this call'sshipsLast30Daysand the card call'sactivity.ships_30dandactivity.releases_30dare always sent and read0where HEY measured nothing. They keep that meaning for the partners who read them (machine rule 10); read them only besideactivityMeasured: true(activity_measuredon the card call), or print the measuredmeaningfulShipsLast30Days/meaningful_ships_30d, which are absent when not measured. - If HEY is down, say nothing rather than guessing. A bot that omits a line is fine; one that invents one is not.
The builder call: GET /api/v1/builder?chain=4663&token=0x…
For a surface that already has the chart and wants the other half. Same two parameters as the card
call, HEY's own tables only. One difference in validation (2026-09-26, corrected): a chain other
than 4663 is a 400 here, where the card call answers 200 with found: false, reason: "chain";
an unpublished contract is a 404 here, with scan_url.
{
"status": "active", // active | stale | dormant | unknown
"hey_status_label": "Shipping", // print this, not your own word for it
"token_verification": "VERIFIED", // VERIFIED | UNVERIFIED | MISMATCH (2026-09-25)
"last_activity_at": "2026-09-19T10:00:00.000Z",
"repo_url": "https://github.com/…",
"latest_release": { "version": "v1.2.0", "url": "…", "timestamp": "…" },
"hey_project_url": "https://heyresearch.xyz/project/…"
}
Five things worth knowing before you render it:
- Print
hey_status_label, not a word of your own. Left to themselves, integrators turn a quiet project into "dead". HEY's vocabulary is deliberate and the labels travel with the enum. - There is no
abandoned. HEY sees silence, not intent. The most it will say isdormant. last_commitis alwaysnull. HEY aggregates commits into weekly summaries and stores no SHA. Uselast_code_activity, and treatcommitsas a floor whencommits_partialis true.- Carry
hey_project_url. Someone who sees a HEY line should be one tap from the evidence behind it. This is the one condition of use. - Check
token_verification(2026-09-25). The activity belongs to the project; the verification belongs to this address. OnMISMATCHthe project's own site names a different contract — do not print the activity as this token's. - Or read
activity_applies_to_token(2026-09-27, additive).falseexactly onMISMATCH. Every other field keeps its value; onfalse, print the activity as the project's, never as this token's, and do not link from the token tohey_project_url. - Check
activity_measured(2026-09-26).falsemeans HEY holds no repository, changelog or feed it can read for this project (or its status is unknown):unknownis then not a finding.research_levelsays how far HEY's research went;as_ofwhen the project was last scored.last_code_activity.commits_30d(withcommits_30d_partialandwindow_start) is the same thirty-day count as the card call'scommits_30d, so the two calls agree;commitsis the newest weekly summary's own figure, as that week recorded it.
The Partner Card (2026-09-30, additive). This call is now HEY's canonical partner card
(/developers/partners): it also sends verified_builder,
latest_meaningful_ship, meaningful_ships_30d (null, never 0, when not measured),
latest_change with latest_change_state, latest_signal, market_status (context only),
badge_url and project_link — the project page with utm_medium=partner_api, which is the link
to render. Name your integration with x-hey-integration: my-bot/1.0.0 (or ?integration=) and it
becomes project_link's utm_source, so HEY can report your click-through to you.
The card call: GET /api/v1/scan?chain=4663&token=0x…
The same answer as the one call above, in the shape a trading bot's card wants and with two more
figures. Built for the Chit bot's line above its buy and sell buttons —
HEY: Shipping · 12 commits · 2 releases · Verified Builder · see on HEY ↗ — and open to any bot.
{
"found": true,
"chainId": 4663,
"contractAddress": "0x…",
"status": "shipping", // shipping | active | resumed | quiet | dormant | unknown
"status_label": "Shipping", // HEY's own words for it — print these
"status_help": "Shipped something meaningful in the last 7 days.",
"verified_builder": true,
"token_verification": "VERIFIED", // MISMATCH: the project's own site names another contract (2026-09-25)
"activity_applies_to_token": true, // false exactly on MISMATCH; the card then has no cta (2026-09-27)
"activity": { "commits_30d": 12, "releases_30d": 2, "ships_30d": 4, "last_ship": "2026-09-16", "last_ship_title": "v0.9.1" },
"project": { "slug": "darkroute", "name": "DarkRoute", "symbol": "dark" },
"project_url": "https://heyresearch.xyz/project/darkroute",
"logo_url": "https://heyresearch.xyz/api/logo?u=…&s=128",
"badge_url": "https://heyresearch.xyz/badge/darkroute.svg",
"cta": { "label": "See the builder on HEY", "url": "https://heyresearch.xyz/project/darkroute" },
"disclaimer": "…"
}
found: falsefor a token HEY has no published page for, and — as a 200, not a 400 — for a chain HEY does not index (reason: "chain"; Robinhood Chain 4663 only, the 46630 testnet included). Print nothing in both cases. A malformedtokenis a 400: that is a bug on your side.- What HEY holds for an unpublished token (2026-10-09, additive). Beside
scan_url, afound: falsefor a token HEY has no published page for says what HEY holds:indexed(truewhen the token is in HEY's own index — a reading of that index, sofalseis a finding),research_state(not_researched: a launch record HEY indexed and has not published;not_published: held, with no launch record HEY shows;not_indexed: nothing held) andlaunched_via(the launchpad, when the launch record names it).foundkeeps its meaning — print nothing as before, or say "Launch record — not researched by HEY · via Virtuals" as the site does. Never a verdict on the token. Absent on another chain, the zero address and an issuer's token. chainis optional and defaults to 4663.commits_30d_partial: true(2026-09-18) means HEY read a full page of a hundred commits that started inside the window, socommits_30dis a floor; print it as100+, or drop the figure.commits_30dis absent when the project has no repository HEY reads — never a zero that reads as "nobody committed". It counts commits by day across the project's repositories, deduplicated;releases_30dcounts releases, launches and feature releases;ships_30dis every meaningful ship, the figure the project page shows.- A database read only: answers in tens of milliseconds,
public, max-age=60so your own cache can be short, 120 a minute without a key, your tier's bucket with one. NotPOST /api/scan, which is the on-demand chain scan (ten an hour) — the name here is the partner's spelling. - The
ctaalways points at the project page, and is absent whenactivity_applies_to_tokenisfalse(2026-09-27): the project's own site names a different contract, so nothing on the card invites a reader from this token to the project. Every other field is still sent, with its value. There is no risk field, no score and no verdict; put a risk read from a tool that does that work beside this line. - Is a zero a measurement? (2026-09-26, additive.)
activity_measured: falsemeans HEY holds no repository, changelog or feed it can read for this project, or cannot decide its status:ships_30d: 0andreleases_30d: 0are then not findings — print nothing rather than "0 ships".coveragesays why (measured,no_source,not_researched),research_levelhow far HEY's research went, andas_ofwhen the project was last scored.activity.meaningful_ships_30dcounts ships by the rule behind the status (a week of prereleases or code summaries counts once);ships_30dstays the ship-record count the project page shows.activity.last_ship_urlis the last ship's public source. - The zero address (
0x000…000), which bots send for a native coin, answers 200{"found": false, "reason": "not_a_token", …}from 2026-09-27 (it was a 400): print nothing. It costs you nothing — no allowance, no rate bucket — on the single call and, item by item, in the bulk call (tokens=); a batch of nothing else is free. The burn address (0x…dead) and any malformed token are still a 400.
The partner contract: what never changes
Every change to these three calls is additive. What is below keeps its meaning; a change of
meaning would ship as a new versioned path, beside the old one for at least ninety days, with a
migration note. /api/v1/ is the partner namespace, not an API version.
/api/v1/scan: every key and type above; the lower-case six-statestatus;found: falseas a 200 for an unpublished token and for another chain; 400 for a malformed token;last_shipasYYYY-MM-DD;commits_30dabsent (not 0) without a readable repository;ships_30dandreleases_30das numbers with their meaning; the CTA (left out only whenactivity_applies_to_tokenisfalse, 2026-09-27);found: false, reason: "not_a_token"for the zero address (2026-09-27); the 60-second public cache; CORS*./api/v1/builder: the four-valuestatuswith noabandoned;last_commit: null; explicitnullfor a field HEY does not hold; 404 withscan_urlfor an unpublished contract; 400 for another chain./api/token/{chainId}/{address}: the camelCase shape;status: "unknown"as a 200; the chain id in the path, digits only;shipsLast30Daysas a number.researchLevelis sent for every published project (the docs once said it was absent for a researched page; it never was).
Additive fields so far: the Partner Card fields on /api/v1/builder and project_link on both
/api/v1/* calls (2026-09-30); token_verification / tokenVerification (2026-09-25); research_level,
activity_measured, coverage, as_of, meaningful_ships_30d, last_ship_url,
activityMeasured, meaningfulShipsLast30Days, asOf, and the builder call's commits_30d,
commits_30d_partial, window_start (2026-09-26); activity_applies_to_token /
activityAppliesToToken and reason: "not_a_token" (2026-09-27). The one change that is not purely
additive was agreed as a founder decision on 2026-09-27: on a MISMATCH token the card leaves out
cta, and the zero address answers 200 instead of 400.
If you want more
The same public API carries the rest of what HEY knows, on the same terms — no key, 120 a minute. See the public read API for every endpoint, field and limit, and the MCP server to give an assistant the same index, read-only.
If you write TypeScript or JavaScript, the card call above is one typed method in HEY's SDK
(2026-09-19), on npm since 2026-09-27 as @hey-research-lab/sdk — npm i @hey-research-lab/sdk.
The shapes come with it:
import { HeyClient } from '@hey-research-lab/sdk';
const hey = new HeyClient();
const card = await hey.scanCard(4663, address);
if (!card.found) return null; // unpublished token or another chain: print nothing
// releases_30d is always sent and reads 0 where HEY measured nothing: a count only when activity_measured.
const releases = card.activity_measured ? ` · ${card.activity.releases_30d} releases` : '';
return `HEY: ${card.status_label} · ${card.activity.commits_30d ?? '–'} commits${releases}${card.verified_builder ? ' · Verified Builder' : ''} · see on HEY ↗ ${card.project_url}`;
The client adds no field the route does not send and caches nothing, so everything on this page
about the three answers, the limits and the four things not to do applies to it unchanged. How it
meets a 429 depends on the version npm serves (corrected 2026-10-18): 0.3.2, the version
npm i installs today, does not retry — a 429 is a HeyApiError with code: 'rate_limited' and
retryAfterSeconds, for your code to wait out. 0.3.3, prepared in the repository and not yet
on npm, waits out a temporary 429 in its walks (pages(), items(), sync()) by its
retry-after, at most three times and never longer than 60 seconds; a single call such as
scanCard still throws, unless the client is built with rateLimit: { wait: 'always' }.
What HEY will never send you
Worth stating plainly, so you do not design around something that is not coming.
- No wallet analytics, no PnL, no smart-money labels, no holder lists read as a claim about
people. HEY does not build it. The one thing it does keep is a daily snapshot of a single
token's largest balances, which draws the distribution map on that token's market page. No
payload carries a balance or an address, and it is never an input to activity status, Build
Momentum, the Discovery Gap or the Builder Radar. What it does reach is
/api/signals, as theconcentration_rosekind — a Nakamoto count with its before and after, in a feed of measured changes (2026-09-19). - No risk score, no safety grade, no verdict.
- No paid placement in any ordering. Nothing anyone pays HEY changes a status, a rank or a figure.
- No claim that a project which keeps building will be worth more. Those are two different things and HEY keeps them apart.
What is left is narrow on purpose: who is building, what they shipped, when, and where HEY read it.