API
Six calls. A tracker asks its sources through exactly the interface it offers a reader, so there is no privileged path into a source, and a source can sit on another machine without anything about the answer changing.
1. The calls
On a tracker, as JSON, on the same port as its pages: everything the pages show, and nothing they do not.
| Call | What it answers |
|---|---|
/api/describe | The tracker, its sources, the kinds they hold, their views and the state of each one's last update. |
/api/search | q, view, kind, sort, limit, offset. |
/api/thing/… | One thing by scheme and value: every source's words, what they mean here, whether the sources disagree, and every claim with its receipt. |
/api/facet | A property, and its counts under the current query. |
/api/changes | since, which is a mark, not a timestamp. |
/api/mark | The mark to hand back next time. |
A source on its own answers the same six, with /api/fetch?id=… in place of the
thing, because a source has claims and a tracker has things. An address naming no call is answered
with the list of calls and a 404, rather than with a search of everything.
On a published tracker the calls are for subscribers, with a key:
Authorization: Bearer zk_…. What its overview and its thing pages show anyone is also
at /demo.json, open to any site, which is where the front page of zetlyn.com takes its
live example from.
2. The same answer in a terminal
Every page on a tracker has a call behind it and every call has a command behind that. Nothing is computed for the web that is not computed for the command line, so a number on a page is a number you can reproduce. One binary, Rust and SQLite, one process, no daemon to install.
$ zetlyn tracker things trackers/cve "conflict:cvss and has:kev"
cve:cve-2025-39682
cve:cve-2025-39964
cve:cve-2026-53266
$ curl -H "Authorization: Bearer zk_…" \
"localhost:8080/api/thing/cve/CVE-2025-39682"
{
"identifier": {"scheme": "cve", "value": "CVE-2025-39682"},
"properties": {
"cvss": {"by": {"zetlyn/cve-nvd": ["9.8"],
"zetlyn/cve-redhat": ["7"]},
"conflict": true},
"exploited": {"by": {"zetlyn/cve-kev": ["yes"]},
"conflict": false},
// … and every other property, with what it means here
},
"claims": [
{"source": "zetlyn/cve-kev", "kind": "vulnerability",
"known": "2026-09-18"},
{"source": "zetlyn/cve-redhat", "known": "2025-09-05",
"url": "https://access.redhat.com/security/cve/CVE-2025-39682"},
// … NVD's, with its receipt
]
}
3. What a total means
Two counts, and the answer says which. Without a filter the total is claims, summed over the
sources. With one it is things, because a question like exploited=yes and
severity=critical is answered by no source alone.
A filter of values, sources and kinds joined by and, which is what clicking the
counts on a tracker's page makes, is answered from the tracker's own store by an index and counted
exactly. Any other filter is answered from the sources: each selecting source is read up to twenty
thousand candidates deep, and past that the count is a floor and at_least says so.
4. Being told what changed
A watch is told four ways:
| Page | /changes, the tracker's inbox by day, with what is new since your last visit. |
| Feed | Atom: every change, one thing's, a question's, a watch's. |
| Webhook | JSON, for a system. |
| Command | The report on standard input, for everything else. |
A watch mails through a command: name mail, msmtp or whatever already
knows how to reach you, and a thing that goes wrong there goes wrong in one place you already
understand. (The mailer a workspace sends its sign-in links with is set in its
workspace.yaml.)
What a tracker notices is a numbered signal: a new thing, a source speaking about one for the first time, a value changed, a disagreement appearing or ending, a source's health. A watch keeps the number of the last one it was told about. Numbers only grow, a rebuild included, and the mark only advances after every delivery succeeded.
Next: declare a source, or install Zetlyn and try it.