An open format for curated, subscribable, Bitcoin-verifiable catalogues of publishers and places. Anyone can publish one; any client speaking the standard can consume it. The BE_TAM app ships with the BE_TAM catalogue as its pre-installed primary source and lets users add any other catalogue by URL.
Redefined 2026-07-21 (pre-publication, no documents existed in the wild): unified entry model with a
kinddiscriminator and a required cataloguetitle. One theme = one catalogue — "Kam na pivo" IS a catalogue; a curator with N themes publishes N catalogues.
1. Document #
A catalogue is a single JSON document served over HTTPS:
{
"standard": "betam-catalog/1",
"publishedAt": "2026-09-01T12:00:00Z",
"publisher": { "name": "BE_TAM", "npub": "<64-hex, optional>" },
"title": "Kam na pivo",
"description": "optional, ≤500 chars",
"entries": [
{
"kind": "creator",
"npub": "<64-hex publisher pubkey>",
"registryKey": "<64-hex channel-registry pointer>",
"displayName": "Cvernovka",
"category": "venue",
"description": "optional, ≤500 chars",
"placeId": "<64-hex, optional — links the entry to a BE_TAM place>",
"geohash": "u2s1v (optional, precision 4–8)",
"promoted": { "rank": 1 }
},
{
"kind": "place",
"id": "cvernovka-taproom",
"name": "Cvernovka Taproom",
"lat": 48.1543,
"lng": 17.1348,
"type": "pub",
"note": "optional curator sentence, ≤280",
"castle:history": "rich extensions attach as namespaced fields"
},
{
"kind": "catalog",
"url": "https://curator.example/kam-na-kavu/catalog.json",
"name": "Kam na kávu"
}
]
}
titleis required — the catalogue's theme.descriptionoptional.- Every entry carries a required
kind. Two kinds are defined:kind: "creator"— a subscribable publisher. Required:npub,registryKey,displayName,category.npub+registryKeyform the subscribe handle (tam://<npub>?r=<registryKey>); both are 64-hex. Optional:description(≤500),placeId,geohash,promoted.kind: "place"— a named point in space; no betam identity required (a curator lists a venue before the venue ever claims one). Required core is exactlyid(free string, unique within the document),name(≤80),lat,lng,type(free string, ≤32) — a motivated developer must produce a valid catalogue from a CSV in one afternoon. Optional:description(≤1000),address(≤200),photo(URL),note(≤280),hours(opening-hours string; interpretation is the client's concern),links(URL list),npub(the venue's own identity once claimed),placeId(explicit join to an in-app BE_TAM place).kind: "catalog"— a reference to anotherbetam-catalog/1document: catalogues compose (playlists of playlists). Required:url(https only, ≤500) andname(≤80); optionaldescription(≤500). A referenced catalogue is an independent document — its verify state is its own. Readers resolving references MUST guard against cycles and SHOULD cap resolution depth (the BE_TAM app caps at 3 levels).
- Per-entry forward compatibility: an entry with an unknown
kindMUST be skipped (readers may surface a warning) — it never invalidates the document. Future kinds extend the same list. - Unknown extra fields MUST be tolerated everywhere (read-tolerance). Rich optional data attaches as namespaced fields (
"castle:history","gov:heritage_id"); validators warn on unknown non-namespaced keys but never fail. Validation schema:packages/protocol/schemas/curated-catalog.js. - Canonical encoding: the published bytes are canonical JSON — objects with keys sorted lexicographically, no insignificant whitespace (
packages/protocol/schemas/canonical-json.js). The timestamp proof commits to these exact bytes.
2. Ordering, promotion & consumption #
entriesarray order = the publisher's editorial order, across kinds.- Creator entries MAY carry
promoted.rank(integer ≥ 1). Clients render promoted entries first (ascending rank), then the rest in array order, and MUST label promoted entries visibly (the BE_TAM app renders[PROMOTED]+ a paid-placement caption). - In multi-source clients, catalogues MUST NOT be interleaved: each source renders as its own attributed section. Promotion is only honoured for the client's primary source.
- Consumption by kind (how the BE_TAM app does it):
creatorentries render in the catalogue/Discover surface;placeentries flow into geo surfaces (Nearby) with provenance back to the catalogue. Other clients may choose differently — the document just carries both.
3. Timestamp proof (detached OpenTimestamps) #
- The proof lives at
<catalog-url>.otsand is a standard OpenTimestamps detached proof forsha256(canonical-json bytes). - Detached is mandatory — a proof cannot live inside the document whose hash it commits to. Re-stamping or upgrading the proof never rewrites
catalog.json. - Publishing flow (see
tools/catalog-publisher/):build(editorial list → canonical JSON) → optionalsign(§4) →stamp(submit to OTS calendars → pending proof) → host both files →upgradeonce the calendars anchor into Bitcoin (hours) → re-host the.ots.
4. Publisher signature (optional) #
The timestamp proof answers when — it cannot answer who. A publisher MAY additionally sign the document with a secp256k1 key:
{
"standard": "betam-catalog/1",
"…": "…",
"signature": {
"npub": "<64-hex BIP-340 x-only public key>",
"sig": "<128-hex BIP-340 schnorr signature>"
}
}
- Fully additive.
signatureis an optional top-level object; readers that predate it treat it as an unknown extra field (read-tolerance, §1) and lose nothing. An unsigned document is exactly as valid as before. - Signed message: the sha256 digest of the canonical-JSON bytes (§1) of the document with the
signaturefield removed. Because canonical encoding is key-order independent, verification survives any JSON re-serialisation that preserves content. - Scheme: BIP-340 schnorr over secp256k1 — the same key family as
npubidentities elsewhere in the standard.npubis the signer's x-only public key (64-hex),sigthe 128-hex signature. - Ordering with the timestamp proof: the detached OTS proof commits to the full served bytes, signature included — sign before
stamp. Re-signing changes the bytes and requires a re-stamp. Verifying the signature never requires the proof and vice versa: the two are independent axes. - Verification verdict (separate from document validity):
| Verdict | Meaning |
|---|---|
unsigned |
No signature field. Absence of a claim — not a defect, not a downgrade of the document. |
valid |
The signature verifies over the signature-stripped canonical bytes against the embedded npub. |
invalid |
The signature object is malformed or does not verify. The document remains schema-valid — a tampered signature is not an invalid document (same honesty split as no-proof vs failed in §5). |
- What
validmeans — and does not mean. A valid signature proves integrity: these exact bytes were produced by the holder ofnpub. It does not prove identity: anyone can mint a key and put any name inpublisher. A signature whosenpubmerely matches the document's ownpublisher.npubis self-attestation. Clients MUST NOT present a valid signature as "official" or as endorsement; the honest client claim is "signed by<npub>" (the BE_TAM app renders aSIGNEDpill suffix and a# signed by <short-npub>caption). "Official" labels require an out-of-band trust anchor — a client-side list of known keys — which is outside this document's scope.
5. Verification levels (what clients should display) #
| State | Meaning |
|---|---|
verified (block H) |
Proof parses, commits to the fetched bytes, and its Bitcoin attestation digest equals the merkle root of block H (checked against independent block explorers). |
pending |
Proof commits to the bytes but only carries calendar (pending) attestations — not yet anchored. |
structural-only |
Proof commits to the bytes; the chain check was unavailable (offline / explorers unreachable or disagreeing). Show the last successful full verification if known. |
no-proof |
No .ots published. An unstamped catalogue is unverifiable, not tampered. |
failed |
The proof does not commit to the fetched bytes, or the merkle root does not match — the snapshot is not what was anchored. |
Honesty rule: never render verified without the chain check, and never claim tamper (failed) for a merely missing/unconfirmed proof.
6. Serving #
Static hosting is sufficient: two files (catalog.json, catalog.json.ots), any CDN or web server, CORS open (Access-Control-Allow-Origin: *) so in-app fetch works. Cache headers short enough for edits to propagate (the proof pins content, not the URL).