# Nostr Kinds · Balanced Wallets: the full text for LLMs Source: https://nostr-kinds.letsbalance.life/ · version 1.0 · 27 Oct 2026 · definitions 0.3.0 DRAFT (round 5). Keys are 64-character lowercase HEX, never npub. Amounts are integer minor units. No secrets here. Contents: 1. the two layers; 2. guide for AI agents and developers; 3. the kinds; 4. the directory registry and API. ====================================================================== # PART 1. THE TWO LAYERS ====================================================================== # The two layers Two different jobs are done by two different kinds of server. Keep them apart in your head and in your code. - **Layer 1 · Directory infrastructure.** Many equal directories and relays that only **store and find** signed records. They are the phone book. - **Layer 2 · Balanced wallets and circles.** Every circle keeps **its own ledger** of numbered blocks on **its own relays**. They are the books. A wallet balance is an account line in that ledger. A third thing, **BalanceID and the Compliance Passport** (identity checks for banks), is a **separate, future layer** and is described on [its own page](/balanceid/). It is not part of either layer above and is not built. ## Side by side
| | **Layer 1 · Directory infrastructure** | **Layer 2 · Balanced wallets / circles** | |---|---|---| | **Job** | Find circles and people. Say where to look | Be the ledger. Say what is true | | **What is stored** | Profiles (KIND 0), circle records (30910), heads (30915), member homes (30916), the **present state**: the newest day seals (43917) and snapshot chunks (43918) of the last 7 days | Blocks (43911) with notes (43910) and acts (43916), day seals (43917), snapshots (43918), rate quotes (43919), references (43921), statements (43924). The **whole history** | | **Who writes** | People (their own profile and home), a circle's key and witnesses (record, head, seals), the directory's own collector (copies after it verified the chain) | The circle's node and witnesses (blocks, seals, snapshots), members (notes and acts), the circle (references, quotes) | | **Who can verify** | Anyone: every record is signed. A directory also checks and may say `verified` | Anyone: the chain by `prev` digest, the witnesses' quorum, the genesis pin, the Merkle roots | | **Deletion** | Impossible: the relay refuses KIND 5 and KIND 62. Old seals and snapshots (older than 9 days) are removed by an hourly cleanup because a directory is the present, not an archive | Impossible: circle relays never delete anything and refuse KIND 5 and KIND 62 for ledger kinds | | **Copies** | Many equal directories, each with the same index; the target is as many as people want (up to 1,000) | At least **4 relays at 3 operators** per circle, plus archives; a payment counts when a majority of witnesses has signed and **3 relays** hold the block | | **If one server dies** | Ask another directory. Nothing is lost: it is rebuilt from the circles' relays | Read another relay of the circle. If the circle's node dies, the circle is restored from its relays and a new node takes over by a public decision | | **What it can get wrong** | It can **omit or be late**. It cannot forge | It cannot be wrong silently: every witness recomputes every block; a wrong block has no quorum | | **Who is trusted for what** | For finding, for convenience | For the truth about a balance, and only through verification | ## One key links the two The same 64-character HEX key appears in three places: 1. the **profile** (KIND 0) stored by directories (layer 1), 2. the **member home** (KIND 30916) that says which circle you belong to, written by you and attested by a witness (it bridges both layers), 3. your **account line** `s3||…` in the circle's ledger (layer 2). Nothing is linked by hand and no name enters the ledger. A directory shows a person as: profile, circle, block joined. Whether the circle's own ledger confirms the join is `ledgerChecked`. ## Which kind belongs to which layer Taken from the record definitions 0.3.0 ([kinds page](/kinds/), [kinds.json](/kinds.json)). **Directory** = accepted and kept by the directory relay. **Circle ledger** = lives on the circle's own relays as part of its book. | KIND | Name | Layer 1 directory | Layer 2 circle ledger | |---|---|---|---| | 0 | Profile (plain Nostr) | yes | | | 30910 | Circle Record | yes | | | 30915 | Circle Head | yes | | | 30916 | Member Home | yes | | | 43910 | Payment Note | | yes | | 43911 | Circle Block | | yes | | 43916 | Circle Act | | yes | | 43917 | Day Seal | yes (newest 7 days) | yes | | 43918 | Snapshot Chunk | yes (newest 7 days) | yes | | 43919 | Rate Quote | | yes | | 43921 | Reference | | yes | | 43924 | Statement | | yes | | 43920 | BSA Rules | | retired (decision E-1) | Notes: - **30910, 30915, 30916** are written by a circle's own parties (circle key, witness, member) but are **stored by directories**: they bridge the layers. Every version of the circle record is also carried in the ledger, so its history is never lost. - **43917 and 43918** exist in both: the circle keeps all of them for ever; a directory keeps copies of the **newest 7 days** (the present state). - **43910** is a payment note. It sits inside a block; a standalone copy is only evidence. - **0** is the ordinary Nostr profile. It is not a BSA kind. - **Retired:** 43920 (the world-wide rules record; each circle now sets its own rules, decision E-1, never published). **Retired since 0.1.0:** 30911, 30912, 30913, 30914, 43912, 43913, 43914, 43915. **Reserved, unused:** 43922, 43923. **Not defined:** 43925 (free text beside the chain was dropped). **Reserved for later:** 30917 (a service offer for relay, witness, archive or index operators). ## Why separate them? Because they fail differently and they are trusted differently. A directory may be wrong by omission and nobody loses money. A ledger must never be wrong. Because many directories are cheap and one circle's ledger is the circle's own. And because the principles say so: everything is public; everything is on relays in at least three copies; there are no central authorities; each circle is an independent, balanced cell; cells connect by themselves into more. Read the [six principles](/reference/principles/). ====================================================================== # PART 2. GUIDE FOR AI AGENTS AND DEVELOPERS ====================================================================== # Guide for AI agents and developers This guide is for an **AI agent or a developer** who must **understand balanced wallets** and **pull their data correctly**. It is written to be quoted: each section is self-contained, numbers are exact, and every statement says whether it is **decided**, a **proposal** or **not built**. The whole guide is also available as plain text in [llms-full.txt](/llms-full.txt). > **Status.** Definitions **0.3.0** are a **DRAFT** (round 5, 23 Oct 2026), not yet published as a Nostr event. The directory described here is **live** (pilot) and read-only for you. Answers shown as "real" were captured from it on the day of checking, **27 Oct 2026**, and may have changed since. Where this guide and the definitions differ, the definitions win ([kinds.json](/kinds.json), [kinds page](/kinds/)). ## 0. The ten rules, in one screen 1. **A balance is a line in a circle's own ledger.** The only authority about it is that circle's chain of blocks on its own relays. A directory is an index, not an authority. 2. **Keys are 64 lowercase HEX characters, never npub.** The API refuses npub with HTTP 400. Do not print or accept npub in your answers. 3. **Amounts are integers in minor units** (cents), carried as base-10 **strings** of at most 15 digits. Rates are whole **ppm**. Times are unix **seconds**. Use BigInt. Never use floating point. 4. **Two layers.** Directory (kinds 0, 30910, 30915, 30916, and copies of 43917/43918) finds things. The circle ledger (43910, 43911, 43916, 43917, 43918, 43919, 43921, 43924) holds the truth. See [two layers](/layers/). 5. **Verify before you say.** Event id, signature, the chain by `prev` digest, the witnesses' quorum `qs`, the genesis pin, the circle id. Silence from a relay is not an empty answer. 6. **A relay answers at most 500 events per REQ.** Page with `until` **inclusive** and de-duplicate by id. Same-second crowds need a tag filter instead of paging. 7. **Never trust `verified: true` alone.** It means "this directory's collector checked the chain", not "you checked". 8. **State your freshness.** Name the block height, the seal day and the time you read. A directory answer is at most about 15 seconds old plus the collector's round. 9. **Say what is decided, proposed or not built.** Definitions are a draft. 10. **Never write.** Nothing in this guide requires publishing to any relay. ## 1. The data model on one page ### 1.1 Objects | Object | What it is | Kind | Identified by | |---|---|---|---| | **Circle** | A community with one currency and one ledger | | **circle id** = the event id of its *found act* (also called the **birth number** in the directory); 64 HEX | | **Found act** | The founding charter, signed by the founder. Its first tag is `["c",""]`, so it cannot contain its own id | 43916 (`t = found`) | embedded in block 1 | | **Genesis digest** | The digest of block 1; the circle's second anchor, with the circle id | | 64 HEX | | **Circle key** | A key that only stamps the circle record (30910). Replaceable by a Heart act | | HEX | | **Account** | One line per key in the ledger. A person's key is the account number (**BSA ID**) | | key | | **System accounts** | `reserve`, `world`, `world-step`, `origin`, and one wallet per project. **Keyless**: nobody can sign for them | | derived from `sha256("lb-system/3|role|circle id|i")` | | **Note** | A payment, signed by the payer | 43910 | event id; lives inside a block | | **Act** | A signed deed: join, leave, vote, propose, exclude, found ... | 43916 | event id; lives inside a block | | **Block** | A numbered page of the ledger: entries and the account lines they touched | 43911 | **digest** (not the event id) | | **Day seal** | A daily checkpoint after the morning, with roots over the day and the state | 43917 | **digest**, and `day` | | **Snapshot chunk** | A part of the full public state at a seal | 43918 | `m` = `:`, `i` = `::` | | **Head** | A pointer to the newest seal, for directories | 30915 (addressable, `d` = circle id) | | | **Circle record** | The circle as the directory lists it: name, currency, Heart, witnesses, relays | 30910 (addressable, `d` = circle id) | | | **Member home** | "This key belongs to this circle since block h", attested by a witness | 30916 (addressable, `d` = `letsbalance`) | author key | | **Reference** | A good-standing letter issued by the ledger when a member leaves | 43921 | event id | | **Rate quote** | A currency rate for a payment between circles, valid for `quote_ttl_s` | 43919 | event id | | **Statement** | A witness or verifier says: checked, error, or restore test | 43924 | event id | | **Profile** | Name and picture of a person (plain Nostr) | 0 | author key | `scope16` = the first 16 hex characters of the circle id. It scopes the tag values `h`, `m`, `i` so they are unique across circles. ### 1.2 The account line Every block ends with a line for each account it touched; every seal's snapshot holds a line for every account. The line (kind 43911, `line_format`) has these fields, all strings: `[index, balance, wave, cap, carry_micro, debt, aseq, last_h, note_n, note_head, last_gift_day, first_receipt_day, status, rules_v]` | Field | Meaning | |---|---| | `index` | the position of the account in the block's `p` tags (in a snapshot line this field is replaced by the key) | | `balance` | minor units, **may be negative** (down to minus the wave) | | `wave` | minor units; grows by what the account receives, up to `cap` | | `cap` | the highest the wave may grow; fixed at joining from the circle's `wave_cap` (default `100000000` = 1,000,000.00) | | `carry_micro` | millionths of a minor unit carried by gravity (0 to 999,999) | | `debt` | what gravity wanted but could not take; at most the wave | | `aseq` | the account's own gap-free entry counter | | `last_h` | height of the previous block that touched the account | | `note_n`, `note_head` | the payer's note counter and last booked note id | | `last_gift_day`, `first_receipt_day` | local dates that drive gravity's quiet-day switch | | `status` | `member`, `frozen`, `excluded`, `left`, `closed`, `system`, or a project's status | | `rules_v` | version of the circle's own rules text the member accepted | The **spendable amount** is `balance + wave`. A **plus** is a positive balance; gravity only takes from a plus. ### 1.3 State lines and the state root The full **decision state** of a circle is a set of text lines, sorted by key (UTF-8 bytes): | Prefix | Content | Key | |---|---|---| | `s3\|\|…` | the account line of a key | `s3\|` | | `g3\|\|…` | governance and settings: `name`, `currency`, `tz`, `tzdata`, `morning`, `circle_key`, `signer:`, `steward:`, `stewards_required`, `quorum`, `relay:`, `param:`, `params_h`, `mirror_h`, `start` … | `g3\|` | | `o3\|\|…` | open items: `project`, `motion`, `vote`, `hold`, `request`, `change`, `join`, `grant`, `invite`, `exclude`, `leaving`, `reference`, `close`, `own` | `o3\|\|` (`vote` has two ids) | | `a3\|\|` | every booked act or note until it expires (duplicates are judged from the state) | `a3\|` | | `i3\|\|\|` | idempotency keys | `i3\|\|` | | `u3\|…` | every reference this circle granted a step to (decision E-13: never accepted twice) | | The **state root** is the root of a **sparse Merkle tree of depth 256** over these lines (section 4.3). A block's tag `["root", entries root, lines root, state root]` and a seal's tag `["state", root, line count]` commit to it. ### 1.4 Number and text rules (all kinds) - Amounts: base-10 strings, minor units, at most 15 digits; aggregates over many accounts up to 20 digits; carry in micro-units; rates in whole ppm; times in unix seconds. - `content` of ledger events is **canonical JSON** (RFC 8785) with **strings only**: no JSON numbers, booleans or null. - Events carry no `expiration` (NIP-40) and no `-` (NIP-70) tag. The last tag is `["v","3"]`. Tag order is fixed per kind; any other order makes the event invalid. - An event is at most 100,000 bytes and 1,000 tags; a tag value at most 2,048 bytes. - No ledger event carries a person's name or bank data. Free text is one line, with the limits given in the [kinds reference](/kinds/). - A **digest** is `sha256(JSON.stringify([0, pubkey, created_at, kind, tags without the "qs" tags, content]))`. Blocks and seals are named by digest because an event id changes with the set and order of witness signatures. ## 2. Finding circles and people Use the directory to **find**, then verify against the circle's own chain (section 3). The first directory: - relay: `wss://directory.letsbalance.world/relay` (read open) - API: `https://directory.letsbalance.world/v1` (GET only, CORS `*`, `Cache-Control: public, max-age=15` on 200) - machine-readable: [directories.json](/directories.json), [openapi.json](/openapi.json) ### 2.1 REST API | Route | Answer | |---|---| | `GET /v1/circles` | `{ indexBuiltAt, incompleteKinds?, count, circles: [CircleSummary] }`. Verified circles only. `?unverified=1` adds circles the collector has not verified (with `verified: false`) | | `GET /v1/circles/{birth}` | `{ indexBuiltAt, circle: CircleDetail }`. 400 for a malformed birth number, 404 when unknown | | `GET /v1/people/{hex}` | `{ indexBuiltAt, person: Person }`. 400 for anything but 64 lowercase HEX (npub is refused). 404 when neither the directory nor the Lana relays hold a profile or a home of that key | | `GET /v1/search?name=` | `{ indexBuiltAt, query, people: [{hex, name, circle}], circles: [{birth, name, currency}] }`. `name` is 2 to 64 characters, every word must match, accents and case ignored, at most 50 results each. People only if their profile or home is on the directory | | `GET /v1/health` | `{ ok, ready, directoryRelay, index, indexError, collector, presentDays }`. 503 while not ready | Other methods answer **405**; an unknown route **404**. After a restart every route answers **503** until the collector's first round has finished. When a kind could not be read completely, answers carry `incompleteKinds: ["0", …]` and everything else is still served. The `/v1` shapes change only by adding fields. **Fields.** Times: `indexBuiltAt`, `verifiedAt` are ISO 8601 UTC; `createdAt` is unix seconds; `day` and `since` are `YYYY-MM-DD`. Keys and ids are 64 lowercase HEX. `CircleSummary` (every field always present): | Field | Meaning | |---|---| | `birth` | the circle id | | `name`, `code`, `currency`, `tz`, `morning` | from the circle record (`currency`: EUR, USD or GBP; `morning`: local hour of the daily seal) | | `genesis`, `founder`, `circleKey` | digest of block 1; founder key; circle key | | `parent` | `{birth, genesis, checked}` or null; `checked` = this directory verified the parent's chain with that first block | | `heart`, `heartRequired` | Circle Heart keys and *k* (how many must agree) | | `witnesses`, `quorum`, `node` | `[{hex, operator, sinceBlock}]`, `{q, n}`, the proposing witness | | `relays`, `archives`, `api` | the circle's relays `[{url, operator}]`; archive relay URLs; the circle's own API URL or `''` | | `mirrorOf`, `record` | the ledger height the record mirrors; `{id, createdAt}` of the record | | `verified`, `verifiedAt`, `verifiedHeight` | the collector verified the chain from block 1, when, and up to which height | | `recordVerified` | the record shown is the one the collector checked against the chain | | `latest` | the newest head or seal `{kind, id, day, n, seal, height, block, stateRoot, lines, createdAt}` or `null` before the first seal | | `members`, `membersUnchecked` | keys whose home the circle's own ledger confirms; valid homes without a ledger verdict yet (not members until confirmed) | `CircleDetail` = `CircleSummary` plus `seals` (the day seals of the newest 7 days, each with `chunks`, `chunksExpected`, `snapshotVerified`) and `memberList` (`[{hex, name, joinedAtBlock, since, ledgerChecked}]`). `Person`: `{ hex, profile, profileSource, inCircle, circle, note }`. `profile` is `null` or `{eventId, createdAt}` plus, when present as strings, `name`, `display_name`, `about`, `picture`, `banner`, `website`, `nip05`, `lud16` (each cut to 2,000 characters; all other fields are left out, read the event for them). `profileSource` is `"directory"`, `"lana-relays"` (read-only fallback, nothing stored) or `null`. `circle` is `{birth, name, currency, joinedAtBlock, since, home:{id, createdAt}, ledgerChecked}` or `null`. `inCircle: false` with a profile means "has a profile, no circle yet". #### Real answers (captured 27 Oct 2026) `GET /v1/circles`: ```json { "indexBuiltAt": "2026-10-27T08:06:05.132Z", "count": 1, "circles": [ { "birth": "5be0a734c3020470204d6907f34906771d81971cca22fdead3f550eda87f3f27", "name": "Mejmo Se Fajn", "code": "mejmo-se-fajn", "currency": "EUR", "tz": "Europe/Ljubljana", "morning": 4, "genesis": "ba5b8a67510893b3c46e7a8dbf2d43ddcf5709aa7d177a283ee9bb6495f565b0", "founder": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", "circleKey": "6f68e576441b3bb5d9b9e235b7b3d2b29be9a0a346ea7a9633d40fe1bd15bc28", "parent": null, "heart": [ "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", "ce896dcbf1568709c3d89086279d7dbf69e95dea9d22af110be78febc4731602" ], "heartRequired": 3, "witnesses": [ { "hex": "15062dde27ccde9b8ef24c4cc39e0ad3be93b3ec94249087ea0b47b862e32122", "operator": "balance.mejmosefajn.org", "sinceBlock": 1 } ], "quorum": { "q": 1, "n": 1 }, "node": "15062dde27ccde9b8ef24c4cc39e0ad3be93b3ec94249087ea0b47b862e32122", "relays": [ { "url": "wss://balance.mejmosefajn.org/relay", "operator": "balance.mejmosefajn.org" }, { "url": "wss://relay2.balance.mejmosefajn.org", "operator": "placeholder-2.balance.mejmosefajn.org" }, { "url": "wss://relay3.balance.mejmosefajn.org", "operator": "placeholder-3.balance.mejmosefajn.org" }, { "url": "wss://relay4.balance.mejmosefajn.org", "operator": "placeholder-3.balance.mejmosefajn.org" } ], "archives": [], "api": "https://balance.mejmosefajn.org", "mirrorOf": 1, "record": { "id": "b00614b9bf6712a42f27063abafecb1f2a276c709f2d256e8c0234c3249ed26d", "createdAt": 1793086389 }, "verified": true, "verifiedAt": "2026-10-27T08:06:01.640Z", "verifiedHeight": 3, "recordVerified": true, "latest": null, "members": 1, "membersUnchecked": 0 } ] } ``` `GET /v1/circles/5be0a734c3020470204d6907f34906771d81971cca22fdead3f550eda87f3f27` (the `seals` array is empty because the circle has no daily seal yet): ```json { "indexBuiltAt": "2026-10-27T08:06:05.132Z", "circle": { "birth": "5be0a734c3020470204d6907f34906771d81971cca22fdead3f550eda87f3f27", "name": "Mejmo Se Fajn", "code": "mejmo-se-fajn", "currency": "EUR", "tz": "Europe/Ljubljana", "morning": 4, "genesis": "ba5b8a67510893b3c46e7a8dbf2d43ddcf5709aa7d177a283ee9bb6495f565b0", "founder": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", "circleKey": "6f68e576441b3bb5d9b9e235b7b3d2b29be9a0a346ea7a9633d40fe1bd15bc28", "parent": null, "heart": [ "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3", "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", "ce896dcbf1568709c3d89086279d7dbf69e95dea9d22af110be78febc4731602" ], "heartRequired": 3, "witnesses": [ { "hex": "15062dde27ccde9b8ef24c4cc39e0ad3be93b3ec94249087ea0b47b862e32122", "operator": "balance.mejmosefajn.org", "sinceBlock": 1 } ], "quorum": { "q": 1, "n": 1 }, "node": "15062dde27ccde9b8ef24c4cc39e0ad3be93b3ec94249087ea0b47b862e32122", "relays": [ { "url": "wss://balance.mejmosefajn.org/relay", "operator": "balance.mejmosefajn.org" }, { "url": "wss://relay2.balance.mejmosefajn.org", "operator": "placeholder-2.balance.mejmosefajn.org" }, { "url": "wss://relay3.balance.mejmosefajn.org", "operator": "placeholder-3.balance.mejmosefajn.org" }, { "url": "wss://relay4.balance.mejmosefajn.org", "operator": "placeholder-3.balance.mejmosefajn.org" } ], "archives": [], "api": "https://balance.mejmosefajn.org", "mirrorOf": 1, "record": { "id": "b00614b9bf6712a42f27063abafecb1f2a276c709f2d256e8c0234c3249ed26d", "createdAt": 1793086389 }, "verified": true, "verifiedAt": "2026-10-27T08:06:01.640Z", "verifiedHeight": 3, "recordVerified": true, "latest": null, "members": 1, "membersUnchecked": 0, "seals": [], "memberList": [ { "hex": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", "name": "", "joinedAtBlock": 2, "since": "2026-10-27", "ledgerChecked": true } ] } } ``` `GET /v1/people/b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2` (a member with no profile on the directory): ```json { "indexBuiltAt": "2026-10-27T08:06:05.132Z", "person": { "hex": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", "profile": null, "profileSource": null, "inCircle": true, "circle": { "birth": "5be0a734c3020470204d6907f34906771d81971cca22fdead3f550eda87f3f27", "name": "Mejmo Se Fajn", "currency": "EUR", "joinedAtBlock": 2, "since": "2026-10-27", "home": { "id": "1947849c2b872f9751484fc0107a3a18c280173610bcd56a5fbf822626ab3fc5", "createdAt": 1793087353 }, "ledgerChecked": true }, "note": null } } ``` `GET /v1/search?name=mejmo`: ```json { "indexBuiltAt": "2026-10-27T08:06:05.132Z", "query": "mejmo", "people": [], "circles": [ { "birth": "5be0a734c3020470204d6907f34906771d81971cca22fdead3f550eda87f3f27", "name": "Mejmo Se Fajn", "currency": "EUR" } ] } ``` #### Error cases (real) | Request | Status | Body | |---|---|---| | `GET /v1/people/not-a-key` | 400 | `{"error":"a person is named by the HEX key: 64 lowercase HEX characters (never npub)"}` | | `GET /v1/circles/zz` | 400 | `{"error":"a circle is named by its birth number: 64 lowercase HEX characters"}` | | `GET /v1/circles/aaaaaaaa…` (64 × a) | 404 | `{"error":"the directory holds no circle record for this birth number"}` | | `GET /v1/people/aaaaaaaa…` (64 × a) | 404 | `{"error":"neither the directory nor the Lana relays hold a profile or a member home of this key"}` | | `GET /v1/search?name=a` | 400 | `{"error":"name is 2 to 64 characters"}` | | `GET /v1/nothing` | 404 | `{"error":"not found"}` | A 404 for a person means "not found in the places this directory looked", **not** "does not exist". ### 2.2 Nostr REQ on the directory relay The relay is a standard Nostr relay (NIP-01). It accepts only the kinds below; **everything else, KIND 5 (deletion) and KIND 62 (vanish), is refused** with a message that starts with `blocked:`. You only **read**. | Kind | What | Accepted by the relay when | |---|---|---| | 30910 | circle record, `d` = circle id | version 3; signed by the circle key it names; the founder's signature in `g` verifies; the circle is admitted and the record matches a mirror of its verified chain | | 30915 | circle head, `d` = circle id | the relay holds the circle record; signed by a current witness; a valid quorum; it extends the head held | | 30916 | member home, `d` = `letsbalance`, `c` = circle id | attested by a witness in office at the join height | | 43917 | day seal | as a head; made on its own day; only the newest 7 days; never two seals for one day | | 43918 | snapshot chunk | from a current witness, for a held seal day; count equals the seal's `snap` count | | 0 | profile | anyone; JSON object at most 16 KB; at most 64 tags; no `expiration`/`-` tag; 10 per author and 600 per sender address an hour; at most 100 with the same `created_at` | Filters that work (tested against the live relay): ``` ["REQ","c",{"kinds":[30910]}] every circle record ["REQ","c",{"kinds":[30910],"#d":[""]}] one circle's record ["REQ","h",{"kinds":[30915],"#d":[""]}] the circle's newest head ["REQ","s",{"kinds":[43917],"#c":[""]}] the newest day seals ["REQ","k",{"kinds":[43918],"#c":[""],"#m":[":"]}] one day's snapshot chunks ["REQ","m",{"kinds":[30916],"#c":[""]}] the homes naming a circle ["REQ","p",{"kinds":[30916,0],"authors":[""]}] one person's homes and profile ``` A real circle record from the relay (kind 30910, abridged tags shown in full in the sample): ```json { "content": "", "created_at": 1793086389, "id": "b00614b9bf6712a42f27063abafecb1f2a276c709f2d256e8c0234c3249ed26d", "kind": 30910, "pubkey": "6f68e576441b3bb5d9b9e235b7b3d2b29be9a0a346ea7a9633d40fe1bd15bc28", "sig": "171e4a3dd7434929bb50d7d58e08a57f0f0c2d8d466946d53b9a2170875c8b79d0c8eca6c5e5c2688f093ce8bff5ebf9571b7eb4aa7d1a157d6952abe16bedb6", "tags": [ [ "d", "5be0a734c3020470204d6907f34906771d81971cca22fdead3f550eda87f3f27" ], [ "code", "mejmo-se-fajn" ], [ "name", "Mejmo Se Fajn" ], [ "currency", "EUR" ], [ "tz", "Europe/Ljubljana" ], [ "tzdata", "2026e" ], [ "morning", "4" ], [ "g", "ba5b8a67510893b3c46e7a8dbf2d43ddcf5709aa7d177a283ee9bb6495f565b0", "9b4a2170ba6e4317abcbc3dac504aa887a8e5d7fc1998241cbdc6ce31f340affc2945df8d0bab00ed29aa84140118c7a7373c65a42888be2444cf4305570d84b" ], [ "mirror_of", "1" ], [ "founder", "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" ], [ "circle_key", "6f68e576441b3bb5d9b9e235b7b3d2b29be9a0a346ea7a9633d40fe1bd15bc28" ], [ "signer", "15062dde27ccde9b8ef24c4cc39e0ad3be93b3ec94249087ea0b47b862e32122", "balance.mejmosefajn.org", "1", "" ], [ "quorum", "1", "1" ], [ "proposer", "15062dde27ccde9b8ef24c4cc39e0ad3be93b3ec94249087ea0b47b862e32122", "1" ], [ "steward", "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" ], [ "steward", "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3" ], [ "steward", "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4" ], [ "steward", "ce896dcbf1568709c3d89086279d7dbf69e95dea9d22af110be78febc4731602" ], [ "stewards_required", "3" ], [ "relay", "wss://balance.mejmosefajn.org/relay", "balance.mejmosefajn.org" ], [ "relay", "wss://relay2.balance.mejmosefajn.org", "placeholder-2.balance.mejmosefajn.org" ], [ "relay", "wss://relay3.balance.mejmosefajn.org", "placeholder-3.balance.mejmosefajn.org" ], [ "relay", "wss://relay4.balance.mejmosefajn.org", "placeholder-3.balance.mejmosefajn.org" ], [ "rate_source", "ecb-daily", "The euro foreign exchange reference rates of the European Central Bank (base EUR; USD and GBP used), published once per TARGET working day and read by the node once a day; the source time is the reference date at 00:00 UTC; the app shows the rate date" ], [ "api", "https://balance.mejmosefajn.org" ], [ "definitions", "0.3.0" ], [ "v", "3" ] ] } ``` A real member home (kind 30916): ```json { "content": "", "created_at": 1793087353, "id": "1947849c2b872f9751484fc0107a3a18c280173610bcd56a5fbf822626ab3fc5", "kind": 30916, "pubkey": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", "sig": "f4a863e2a6beb4a39ff5f1cd703b5f0be9ee405febcb36dc944bdc6fb909613f98ee9fb94003168921145cf5e6b8cf339d5047484ba5332afa090e1257fee4a1", "tags": [ [ "d", "letsbalance" ], [ "c", "5be0a734c3020470204d6907f34906771d81971cca22fdead3f550eda87f3f27" ], [ "g", "ba5b8a67510893b3c46e7a8dbf2d43ddcf5709aa7d177a283ee9bb6495f565b0" ], [ "join", "2" ], [ "attest", "15062dde27ccde9b8ef24c4cc39e0ad3be93b3ec94249087ea0b47b862e32122", "2026-10-27", "0e73cc5d970877a92051a066855d9e33e37418f710cbe8c5901ebfa39289bebbe704d2394b450c026ee09835f6cff61ecc3716ed75ad3efed9d3d41f21eb4798" ], [ "v", "3" ] ] } ``` **What a relay reader must check itself** (the JSON API does this for you, but you can do it too): the event id and signature; for a 30910, the founder's BIP-340 signature `g` over `lb-genesis/3||` and that the author is the `circle_key` tag; for a 30915 or 43917, at least `q` valid `qs` signatures of the record's current witnesses over `lb-seal/3|`; for a 30916, the `attest` signature over `lb-home/3|||||`. What the relay serves beyond the API: the whole signed event, so every field of a profile. **Paging.** At most **500 events** answer one REQ (with any `limit`). To read more, repeat the REQ with `until` = the oldest `created_at` you got, **inclusive**, and de-duplicate by id. If a page returns no new id, the whole second is full: use a narrower tag filter instead. The directory limits profiles to 100 with one `created_at` so that this always terminates. ### 2.3 Script [`examples/read-directory.mjs`](/examples/read-directory.mjs) lists the circles by API and by relay (Node 22+, read only): ``` node read-directory.mjs # circles node read-directory.mjs <64-hex key> # one person ``` ## 3. Reading a circle's own ledger The ledger of a circle lives on **its own relays**: at least four relays of at least three operators, the first one the circle's **box relay**. Get their URLs from `relays[]` in the circle summary, or from the `relay` tags of the circle record, or from the circle's link or QR code. Read from **at least two relays at once**. "Not found" counts only when **all** you asked have said they are done. ### 3.1 Filters on a circle's relay ``` ["REQ","b",{"kinds":[43911],"#c":[""]}] every block (page it) ["REQ","b",{"kinds":[43911],"#h":[":"]}] one block by height ["REQ","a",{"kinds":[43911],"#c":[""],"#p":[""]}] blocks that touched an account ["REQ","s",{"kinds":[43917],"#c":[""]}] day seals ["REQ","k",{"kinds":[43918],"#m":[":"]}] a day's snapshot chunks ["REQ","r",{"kinds":[43921],"#p":[""]}] a member's reference ["REQ","q",{"kinds":[43919],"#c":[""]}] rate quotes ``` Pitfalls specific to relays: a relay answers at most **500 events** per REQ; paging uses `until` **inclusive** (the oldest second is returned again, so de-duplicate by id); blocks may share a `created_at` second, so for one block use `#h`; a **silent relay is not an empty answer**. ### 3.2 What to verify, in this order 1. **Event id.** `id == sha256(JSON.stringify([0, pubkey, created_at, kind, tags, content]))`, and the BIP-340 Schnorr signature `sig` verifies for `pubkey` over `id`. 2. **Completeness.** The heights `h` run **1…N with no gap**. A missing height: look at other relays; if it exists nowhere, stop and report the height. Never guess. 3. **Digest and chain.** `digest = sha256(JSON.stringify([0, pubkey, created_at, 43911, tags without "qs", content]))`. Block *h* carries `prev` = the digest of block *h−1* (empty at height 1) and `genesis` = the digest of block 1 (empty in block 1 itself). 4. **The genesis pin.** The circle id is the id of the found act embedded in block 1. Rebuild the found act from block 1's `genesis` entry: `pubkey = found.k`, `created_at = found.t`, kind 43916, `tags = [["c",""], ...found.tags, ["v","3"]]`, `content = found.c`; its event id must **equal the circle id**. Also compare the digest of block 1 with the genesis digest you were given (from a link, or the `g` tag of the circle record). A thief cannot found a "false circle" with the same name: another found act has another id. 5. **The circle key.** The genesis entry's `circle_sig` is the circle key's BIP-340 signature over `sha256("lb-found/3|" + the 32 bytes of the found act id)`. 6. **The witnesses' quorum.** A block counts only with at least `q` valid `qs` tags, each `["qs", witnessHex, sig]` where `sig` signs `sha256("lb-block/3|" + the 32 digest bytes)` (seals use `lb-seal/3|`). Witnesses are those of the found act (`signer` and `quorum` tags), changed only by `roles` entries, which apply **from the next block** on. A block without the quorum is **not the circle's** and is ignored. 7. **Forks.** Two different digests at one height, each with a valid quorum, or two valid seals for one day, are a **fork alarm**: stop and report. 8. **Roots inside the block.** The tag `["root", entries, lines, state]`: `entries` is an RFC 6962 Merkle root over the canonical JSON bytes of each entry in `seq` order; `lines` is an RFC 6962 root over the leaves of the block's account lines (section 4.1); `state` is the sparse Merkle root of the whole state after the block. 9. **Time.** Booking times `at` never decrease; created_at of a block is the `at` of its last entry. A circle's mornings are computed with the **time-zone database version pinned in its found act** (`tzdata`); with another version a boundary-dependent result is *unverifiable*, never a guess. 10. **Reconcile with the directory.** `circleKey`, `genesis`, witnesses and relays shown by the directory must equal what the chain says; the chain wins. ### 3.3 Script [`examples/verify-circle-chain.mjs`](/examples/verify-circle-chain.mjs) does steps 1 to 6 (Node 22+; `npm i @noble/curves` for the signatures). Run against the live pilot circle: ``` node verify-circle-chain.mjs wss://balance.mejmosefajn.org/relay \ 5be0a734c3020470204d6907f34906771d81971cca22fdead3f550eda87f3f27 \ ba5b8a67510893b3c46e7a8dbf2d43ddcf5709aa7d177a283ee9bb6495f565b0 ``` Real output on 27 Oct 2026: ``` read 3 block events from wss://balance.mejmosefajn.org/relay ok heights 1..3 found, no gap ok circle id = id of the found act in block 1 ok genesis digest equals the pinned one ok the circle key signed the found act (circle_sig) ok prev/genesis chain intact; every block has 1 valid witness signature(s) tip: height 3, digest 4db4cfa189ccb430a77630a47079891086545ebb0f62db6b1c0e2d7a18dafc69 ``` The script uses the witnesses of the found act only. A full verifier also applies each `roles` entry (witness, Heart, key and relay changes) from the next block on, and restores from the newest seal rather than from block 1 for a long chain. ### 3.4 What a block looks like A real block (height 2 of the pilot circle: a member joined). Block `content` is `{"e": [entries], "l": [lines]}`; an entry is `[seq, type, at, body]`: ``` tags: c, h ("5be0a734c3020470:2"), prev, genesis, seq ["seq","3","4"], at ["at","1793087354","1793087354"], root [entries root, lines root, state root], qs [witness, signature], v "3" entries: ["3","act","1793087354",{"act":{"c":"","k":"","sig":"…","t":"1793087353","tags":[["t","join"],["expires","1793692153"]]}}] ["4","rule","1793087354",{"in":[""],"party":"0","rule":"member_join"}] lines: [["0","0","0","100000000","0","0","1","","0","","","","member",""]] ``` Entry types: `genesis`, `pay`, `give`, `xout`, `xin`, `xdone`, `xundo`, `act`, `rule`, `roles`, `grav`, `rej`. A `rej` is a **refusal written in public** with its reason. Replaying all entries in `seq` order from the previous state must give exactly the block's lines and roots; that is what every witness checks before signing. ## 4. Computing and verifying a balance ### 4.1 From the newest block that touched the account The simplest honest answer for "what is X's balance now": take the **newest block that names X in a `p` tag**; the account line of X in it is X's line **after** that block. (Its `last_h` is the previous block that touched the account.) The block commits to its lines through `["root", …, lines root, …]`, so the line can be checked: the leaf is `s3||balance|wave|cap|carry_micro|debt|aseq|last_h|note_n|note_head|last_gift_day|first_receipt_day|status|rules_v` for each party in `p` order (the line of that party with its full HEX key; `last_h` is the value from before the block), and the lines root is the **RFC 6962 Merkle tree hash** over those leaves (leaf = `sha256(0x00 ‖ leaf)`, node = `sha256(0x01 ‖ left ‖ right)`, split at the largest power of two below *n*). [`examples/balance-of.mjs`](/examples/balance-of.mjs) does this. Real run on the pilot circle for the key above: ``` newest block that touched this account: height 3 { index: '0', balance: '0', wave: '0', cap: '100000000', carry_micro: '0', debt: '0', aseq: '3', last_h: '2', note_n: '0', note_head: '', last_gift_day: '', first_receipt_day: '', status: 'left', rules_v: '' } balance + wave (what the member can still pay): 0 minor units lines root recomputed: bff4f77269c1a3e6b57d0daa0fcf4d7adce25224e6ee2bf31c895dea5846d53d lines root in block : bff4f77269c1a3e6b57d0daa0fcf4d7adce25224e6ee2bf31c895dea5846d53d (match) ``` This only shows the line is the one **the block committed to**. It is true if the block itself is part of a valid chain (section 3.2). It is **as of that block**: say the height and time. ### 4.2 From a day seal and its snapshot (no chain replay) 1. Read the newest seal 43917 from at least two relays and check its `qs` quorum (the witnesses of the office after its range). 2. Read **all** its snapshot chunks 43918 (`#m` = `:`; the seal's `snap` tag names the count). Check that the chunks' RFC 6962 root equals `snap`'s root and the lines they carry rebuild the seal's `state` root and sum to 0. 3. Find the line `s3||…` of the account: that is its state at the end of the seal's range. 4. Prove it: the **inclusion proof** is the list of non-empty siblings on the key's path in the sparse Merkle tree (4.3). A bank or a light client can verify it against the seal's `state` root without any server of the circle. ### 4.3 The sparse Merkle tree of the state - Depth **256**. The path of a line is `sha256(key)` read bit by bit from the most significant bit; the **key** is the line's key fields joined by `|` (`s3|`, `g3|`, `o3||`, …; table in 1.3). - Leaf hash `sha256(0x00 ‖ utf8(line))`. Node hash `sha256(0x01 ‖ left ‖ right)`. An empty subtree is **32 zero bytes**; a node over two empty subtrees is empty too. - An **inclusion proof** is `[(depth, siblingHex), …]` for every depth where the sibling subtree is not empty. [`examples/state-root.mjs`](/examples/state-root.mjs) computes a root, builds and verifies proofs, and checks the published test vector (76 lines, the whole state of an example circle after block 4): ``` $ node state-root.mjs --test lines 76 computed root df7981e5ec0191d848b06ca3ff2ce66f27a2385ef7ad39547689374b92efb359 published root df7981e5ec0191d848b06ca3ff2ce66f27a2385ef7ad39547689374b92efb359 root matches true proof for s3|5731510299edc4f75f7014023e739f351b3f9e461bc8dd3da07bcd0c798d7407 with 7 siblings proof verifies true ``` The test vector is **TEST data** from the definitions ([test-vector-state.json](/examples/test-vector-state.json)). ### 4.4 Meaning of the numbers - **Units.** Minor units of the circle's currency: for EUR, cents. `wave_cap = 100000000` is 1,000,000.00. The start of a new circle is `100000` = 1,000.00, moved from `origin` into the `reserve` in block 1. - **Balance** may be negative: a payer's balance falls when they pay and cover is `amount ≤ balance + wave`. A payment *to* a member below the wave cap moves `grow = min(amount, cap − wave)` from the receiver to the reserve and adds it to the receiver's wave; then `settled = min(debt, max(0, balance))` also moves to the reserve. So for a receiver below the cap **the balance does not rise; the wave does**. - **Plus** = a positive balance. Only a plus is subject to gravity. - **Carry** is the sub-cent remainder (micro-units). **Debt** is what gravity wanted but could not take, capped at the wave. - **Sum.** The sum of all balances in a circle (accounts, system accounts and project wallets) is exactly **0** after every block. `origin` is negative by the start (and what it later grants), `reserve` and project wallets are positive. - **Integer arithmetic only.** Parse strings to BigInt. Round with the rule stated for each formula. Never divide as floats. - **Status.** `frozen`, `excluded`, `left`, `closed` accounts exist and still appear in lines. A `left` account has a frozen history; do not call it "empty". ## 5. How gravity is computed Source: the ledger document, section 7.3 (definitions 0.3.0). Gravity is a **public rule over public data in whole numbers**, booked per account in *morning blocks* as a `grav` entry with the result, so phones never recompute it. A verifier can. **Inputs for one account and one morning D:** the window `[start, end)` between the previous and this morning's boundary (unix seconds, from the circle's morning hour in its pinned time zone); the account's balance, wave, `carry_micro`, `debt`, `last_gift_day`, `first_receipt_day` **at the cut-off** (after its last booking before the boundary); the balance moves inside the window; and the circle's parameters `band_ppm_day`, `excess_ppm_day`, `transition_days`, `rest_after_days`. **The rule:** ``` S = Σ balance_i × seconds_i over the window of W seconds (BigInt) avg = floor((2·S + W) / (2·W)) half rounds up; avg ≤ 0 → the rule is 0 rule_µ = min(avg, wave)·band + max(0, avg − wave)·excess (millionths of a minor unit) total_µ = rule_µ + carry_µ; cents = total_µ div 10^6; carry_µ' = total_µ mod 10^6 wanted = cents + debt; room = max(0, balance at the cut-off) deduct = min(wanted, room); debt' = min(wanted − deduct, wave) ``` **The quiet-day switch** (which `band` and `excess` apply). With D the morning's local date: - `quiet` = 0 if the line has neither `last_gift_day` nor `first_receipt_day`, else `max(0, D − the later of the two that are set)`. - `in_transition` = `first_receipt_day` is set and `D − first_receipt_day < transition_days`. - `resting` = `rest_after_days > 0` and `quiet ≥ rest_after_days`. - `excess` = `band_ppm_day` if in transition, else `excess_ppm_day`. - `band` = `excess` if resting, else `band_ppm_day`. **Start values** of a circle (also the maximum: gravity can never be made stronger): `band_ppm_day = 330` (about 1 % a month), `excess_ppm_day = 22840` (half in about 30 days), `transition_days = 60`, `rest_after_days = 183`. **Whole seconds, and where a deduct counts.** `at`, boundaries and windows are whole unix seconds. In the window that ends at the next boundary, a `grav` entry of morning D changes the balance **from D's boundary**, not from the entry's own time. A window may be 86,400 s, or 90,000 or 82,800 s on a clock-change day in a zone with daylight saving time. **Order of the morning.** Payments never wait for the morning. "Visit before touch": before any entry after the boundary touches an account whose `grav` for D is pending, that account's `grav` is booked first, from the cut-off state, so the order of payments never changes anyone's gravity. After an outage the missed mornings are booked in date order, each reading the results of the earlier ones. ### Worked example 1: a plus above the wave Wave 1,000.00 (`100000`), steady balance 1,500.00 (`150000`) all day, carry 250,000 µ, no debt; last gift long ago but not 183 days, first receipt more than 60 days ago. ``` avg = 150000 rule_µ = min(150000, 100000)·330 + (150000 − 100000)·22840 = 33,000,000 + 1,142,000,000 = 1,175,000,000 total_µ = 1,175,000,000 + 250,000 = 1,175,250,000 cents = 1175 carry' = 250,000 deduct = min(1175 + 0, 150000) = 1175 → 11.75 flows to the reserve ``` ### Worked example 2: an average over a payment Same wave. The balance is 1,200.00 until noon, then a payment of 600.00 leaves, so 600.00 until the next boundary; carry 0. ``` S = 120000·43200 + 60000·43200 avg = floor((2S + 86400) / (2·86400)) = 90000 (below the wave) rule_µ = min(90000, 100000)·330 + 0 = 29,700,000 cents = 29 carry' = 700,000 deduct = 29 → 0.29 ``` Both examples are produced and checked by [`examples/gravity.mjs`](/examples/gravity.mjs) and were cross-checked against the reference implementation of the definitions: ``` $ node gravity.mjs … OK: both worked examples match the guide ``` ## 6. The trust model | Who | What they can prove | What they cannot | |---|---|---| | **The author of an event** | "this key signed this event" (BIP-340 signature) | that the content is true | | **A witness** | that it recomputed a block or seal itself and signed the digest | anything about a circle it is not a witness of; two blocks at one height would leave public proof against it | | **The circle** (witnesses' quorum, ≥ *q*) | that a block is the circle's block at that height | that the circle is honest about the outside world (there is none: no bank data enters) | | **A relay** | nothing: it stores and serves | it can **hide or be late; it cannot forge**; it can be silent | | **A directory** | that it received signed events and checked them; that its collector checked a circle's chain from block 1 | anything beyond signatures it verified; **it can omit or be late**, and it may list only the circles its operator admitted | **What `verified: true` means in the API.** `CircleSummary.verified` = this directory's **collector** read the circle's relays and verified the chain from block 1 (signatures, witnesses, first block). `recordVerified` = the record shown matches a mirror of that chain. `Person.circle.ledgerChecked` / `memberList[].ledgerChecked` = the circle's own ledger confirms this home is current. `snapshotVerified` = the chunks rebuild the sealed state. **None of this is your verification.** It tells you that someone checked, with code you cannot see. **Rule: never trust a directory alone.** Use the directory to find relays and anchors, then verify against the circle's own chain (section 3). When the directory and the chain disagree, the chain wins and the directory is stale or wrong. **Also true:** a relay that does not answer is not "no record"; never answer "balance 0" from silence. A directory cannot see a circle it was not told about; "not listed" is not "not a circle". Two keys of one person are two accounts. The pilot circles run on test infrastructure with a single witness, publicly labelled. ## 7. Checklist: how to answer questions about a person's wallet 1. **Normalise the key.** Exactly 64 lowercase HEX. If given npub, say you cannot use npub and ask for the HEX (the Lana apps show it). 2. **Find the circle.** `GET /v1/people/{hex}`: note `inCircle`, `circle.birth`, `ledgerChecked`, `profileSource`. If `inCircle` is false, they may have a profile but no wallet; say so. 3. **Fetch the circle.** `GET /v1/circles/{birth}`: relays, genesis digest, witnesses, quorum, latest seal. 4. **Read at least two of the circle's relays.** Verify the chain (section 3.2), or run `verify-circle-chain.mjs`. 5. **Get the account line** from the newest block that names the key (4.1), or from the newest seal's snapshot with a Merkle proof (4.2, 4.3). 6. **Interpret with integers.** balance, wave, spendable = balance + wave, status, cap. Convert minor units to a currency only for display, with the circle's currency. 7. **State the freshness:** circle id (first 12 characters), block height, digest of the tip, seal day, the time you read, and which relays answered ("2 of 4"). 8. **Say what you did not verify** (for example "signatures were not checked"; "single witness, not decentralised"; "relay 3 did not answer"). 9. **Never invent:** no name from nowhere (the ledger has none; a name comes from the profile), no bank, no identity claims, no future dates. 10. **Be honest about status:** definitions are a draft; pilot circles are test infrastructure. ## 8. Pitfalls and anti-patterns - **Reading npub or printing npub.** Use HEX everywhere. - **Floats.** `0.1 + 0.2` has no place here. Use BigInt on strings. - **Trusting `verified: true`, the API's `members` count, or the directory's `latest` as the truth.** - **Treating silence as zero or as "no".** - **Ignoring the 500 cap.** A list of exactly 500 is almost certainly **truncated**. Page with `until` inclusive and de-duplicate; if a second is full, narrow with tags. - **Paging with `since` or exclusive `until`** and losing events that share a second. - **Using the event id as a block's name.** Blocks are named by **digest**; the event id changes with the set of `qs` signatures. - **Applying the wrong witness set.** `roles` entries change witnesses from the *next* block. - **Forgetting the `tzdata` pin.** A boundary computed with another time-zone release can differ on a clock-change day. - **Believing a 30910 because it names a circle id.** Anyone can publish one; only the one signed by the circle key the ledger names, matching its mirror and pinned genesis, counts. The first-seen rule is only extra safety. - **Reading a copy as the original.** The directory holds the newest seals for 7 days only; it is an index of the present, not an archive. - **Mixing the layers.** Asking a directory for a transaction history, or asking a circle relay for the people of the world. - **Putting names, bank details or documents anywhere near the ledger.** The ledger is public for ever. - **Calling a draft a standard.** Definitions 0.3.0 are a draft and still change. - **Writing.** You do not need to publish anything. Do not send events to directories or relays to "test" them. ## 9. Machine-readable aids All are static files on this site. | File | What | |---|---| | [/llms.txt](/llms.txt) | a short map of the site for language models, with links | | [/llms-full.txt](/llms-full.txt) | this guide, the two layers and the kinds in one plain-text file | | [/directories.json](/directories.json) | the registry of known directories (the first is *Balance directory 1*) | | [/kinds.json](/kinds.json) | all BSA kinds of definitions 0.3.0: layer, replaceability, publishers, tags | | [/openapi.json](/openapi.json) | OpenAPI 3.1 of the directory REST API with real example answers | | [/sitemap.xml](/sitemap.xml), [/robots.txt](/robots.txt) | for crawlers | | [/examples/](/examples/) | five scripts, run against the live pilot: `read-directory.mjs`, `verify-circle-chain.mjs`, `balance-of.mjs`, `state-root.mjs`, `gravity.mjs`, and the test vector | Scripts need **Node 22 or newer** (global `WebSocket` and `fetch`). Only `verify-circle-chain.mjs` optionally uses the npm package `@noble/curves` for Schnorr signatures. None of them writes anything. ## 10. Where to read more - [Two layers](/layers/) with the comparison table and which kind belongs where - [Kinds reference](/kinds/) with tags, validation rules and signed examples - [The directory](/directory/) and how to run your own - The full texts: [ledger](/reference/ledger/) and its [technical specification](/reference/ledger/technical/), [governance](/reference/governance/), [relays](/reference/relays/), [BSA ID](/reference/bsa-id/) - [Glossary](/glossary/) ====================================================================== # PART 3. THE KINDS (definitions 0.3.0, draft) ====================================================================== Full tags, validation rules and signed examples: https://nostr-kinds.letsbalance.life/kinds/ and https://nostr-kinds.letsbalance.life/kinds.json ### KIND 0 · Profile Layer: directory. Class: replaceable. Signed by: the person (plain Nostr, not a BSA kind). Required tags: n/a Optional tags: none What is public: a profile is public Details: https://nostr-kinds.letsbalance.life/kinds/#kind-0 ### KIND 30910 · Circle Record Layer: directory. Class: addressable. Signed by: The circle key in office (held by the app and the creator; replaced by circle_key_change); it signs only this mirror and, once, the found act's binding in block 1 (proposal).. Required tags: d; code; name; currency; tz; tzdata; morning; g; mirror_of; founder; circle_key; signer; quorum; proposer; stewards_required; relay; rate_source; api; definitions; v Optional tags: parent; steward; archive; bootstrap What is public: Public: the circle's description, founder, parent circle, witnesses and their operators, Heart members by HEX key, relays and rate source. No person's name and no bank data; the app shows "public for ever; do not write names, personal or bank data" at the circle name, the operator names and the rate-source description. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-30910 ### KIND 30915 · Circle Head Layer: directory. Class: addressable. Signed by: The node (a current witness) that signed the seal.. Required tags: all seal tags; d; seal Optional tags: none What is public: Public: roots, counts, the circle's world, reserve and origin numbers, its circle key and the height of its parameters in force. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-30915 ### KIND 30916 · Member Home Layer: directory. Class: addressable. Signed by: The member's own key; a witness of the circle attests inside the tags.. Required tags: d; c; g; join; attest; v Optional tags: n; w What is public: Public: the member's BSA ID, circle, join height, day and name words. No balances (those are in the ledger) and nothing else. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-30916 ### KIND 43910 · Payment Note Layer: circle. Class: regular. Signed by: The payer (any member).. Required tags: p; c (from); amount; currency; n; prev; expires; v Optional tags: c (to); to_amount; e (quote); idem; e (request); e (project) What is public: Public for ever: payer, receiver, amounts, purpose. No name, no bank data. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-43910 ### KIND 43911 · Circle Block Layer: circle. Class: regular. Signed by: The node (a current witness) of the height, with at least q witness signatures inside (qs).. Required tags: c; h; prev; genesis; seq; at; root; qs; v Optional tags: p; m; morning; more What is public: Public for ever: every booking with every touched account's line after it, every move into and out of project wallets and the reserve, every refusal with its reason (never the refused event's text). Keys only, never names; no bank data. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-43911 ### KIND 43916 · Circle Act Layer: circle. Class: regular. Signed by: The acting person: member, invitee, recipient, proposer, Heart member (signer "steward"), witness, founder or guardian.. Required tags: c; t; expires; v Optional tags: the type's tags; after; idem What is public: Public for ever: who acted (HEX key), what, when, and the text. No names, no bank data. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-43916 ### KIND 43917 · Day Seal Layer: both. Class: regular. Signed by: The node (a current witness), with at least q witness signatures inside (qs).. Required tags: c; m; day; boundary; prev; covers; range; head; pay; state; snap; gravity; sum; world; quotes; reserve; origin; signers; circle_key; params; qs; v Optional tags: none What is public: Public: roots, counts, the circle's world, reserve and origin numbers, its circle key and the height of its parameters in force. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-43917 ### KIND 43918 · Snapshot Chunk Layer: both. Class: regular. Signed by: The node (a current witness); valid through the seal's roots.. Required tags: c; m; i; chunk; range; v Optional tags: none What is public: Public: every account's line by HEX key, every project and hold. No names. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-43918 ### KIND 43919 · Rate Quote Layer: circle. Class: regular. Signed by: The node (proposer) of the quoting circle at created_at, as its ledger names it.. Required tags: c; source; base; rate; expires; prev; v Optional tags: none What is public: Public: rates, times and the quoting circle. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-43919 ### KIND 43921 · Reference Layer: circle. Class: regular. Signed by: The node (a current witness) of the issuing circle, right after the block with the reference entry has its quorum.. Required tags: c; p; block; g; currency; wave; own; joined; left; published; took_part; v Optional tags: e (published); e (took_part) What is public: Public: the person's HEX key, the circle, the step built, the times the membership began and ended and the project ids. No name, no bank data, nothing of an OWN process but that none is open. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-43921 ### KIND 43924 · Statement Layer: circle. Class: regular. Signed by: Any witness, verifier or auditor.. Required tags: c; about; target; result; v Optional tags: range; state; votes; evidence What is public: Public: the author's key, the result and its evidence. Details: https://nostr-kinds.letsbalance.life/kinds/#kind-43924 ====================================================================== # PART 4. DIRECTORY REGISTRY AND API ====================================================================== { "schema": "https://nostr-kinds.letsbalance.life/directories.json", "updated": "2026-10-27", "note": "The registry of known Balance directories. A directory can only omit or be late, never forge; ask several and take the union. How a new directory is listed here is not yet specified.", "directories": [ { "name": "Balance directory 1", "status": "pilot", "operator": "LetsBalance", "relay": "wss://directory.letsbalance.world/relay", "api": "https://directory.letsbalance.world/v1", "health": "https://directory.letsbalance.world/v1/health", "read": "open", "write": "policy-gated: only the kinds below, only what matches a verified circle chain; profiles (KIND 0) from anyone with limits; deletion refused", "kinds_accepted": [ 0, 30910, 30915, 30916, 43917, 43918 ], "present_days": 7, "openapi": "https://nostr-kinds.letsbalance.life/openapi.json", "docs": "https://nostr-kinds.letsbalance.life/directory/" } ] } OpenAPI: https://nostr-kinds.letsbalance.life/openapi.json