The interface

Six calls.  A tracker uses them, and so can you.

A tracker asks its sources through exactly the interface it offers a reader. There is no privileged path into a source, which is why a source can sit on another machine and nothing about the answer changes.

ON A TRACKER, AS JSON, ON THE SAME PORT AS THE PAGES

Everything the pages show,
and nothing they do not.

THE CALLS

  • /api/describeThe tracker, its sources, the kinds they hold, their views and the state of each one's last update
  • /api/searchq, 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/facetA property, and its counts under the current query
  • /api/changessince, which is a mark, not a timestamp
  • /api/markThe 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; 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 this one takes its live example from.

ONE BINARY

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.

rust sqlite one process no daemon to install
localhost · 2026-09-29
$ 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
  ]
}
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, counted after the things were assembled, because a question like exploited=yes and severity>=high is answered by no source alone. A filtered query reads each selecting source twenty thousand candidates deep; past that the count is a floor and at_least says so.

A WATCH

Told what changed,
four ways.

DELIVERY

  • page/changes, the tracker's inbox by day, with what is new since your last visit
  • feedAtom: every change, one thing's, a question's, a watch's
  • webhookJSON, for a system
  • commandThe report on standard input, for everything else

Mail is the command. Zetlyn holds no SMTP credentials and ships no mail client: 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.

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.

ZETLYN

One interface, whichever side of it you are on.