BE_TAM Open standard

// catalog standard · live example: /catalog

The betam-catalog/1 standard

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 kind discriminator and a required catalogue title. 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"
    }
  ]
}
  • title is required — the catalogue's theme. description optional.
  • Every entry carries a required kind. Two kinds are defined:
    • kind: "creator" — a subscribable publisher. Required: npub, registryKey, displayName, category. npub + registryKey form 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 exactly id (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 another betam-catalog/1 document: catalogues compose (playlists of playlists). Required: url (https only, ≤500) and name (≤80); optional description (≤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 kind MUST 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 #

  • entries array 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): creator entries render in the catalogue/Discover surface; place entries 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>.ots and is a standard OpenTimestamps detached proof for sha256(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) → optional sign (§4) → stamp (submit to OTS calendars → pending proof) → host both files → upgrade once 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. signature is 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 signature field 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 npub identities elsewhere in the standard. npub is the signer's x-only public key (64-hex), sig the 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 valid means — and does not mean. A valid signature proves integrity: these exact bytes were produced by the holder of npub. It does not prove identity: anyone can mint a key and put any name in publisher. A signature whose npub merely matches the document's own publisher.npub is 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 a SIGNED pill 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).