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
  2. The same answer in a terminal
  3. What a total means
  4. Being told what changed

1. The calls

On a tracker, as JSON, on the same port as its pages: everything the pages show, and nothing they do not.

CallWhat it answers
/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, 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.
FeedAtom: every change, one thing's, a question's, a watch's.
WebhookJSON, for a system.
CommandThe 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.