a protocol to build the trusted network based on the real us
The protocol, in one page
A protocol, not a platform: open formats and shared test vectors that any implementation can check itself against.
One implementation exists today, in Python and in the browser. A second, independent implementation is what would test these formats properly. The vectors below are there for it.
Each person runs their own 3P (the node). It keeps their record on their own computer. A card is what they choose to show: a signed packet of some entries from one face, expiring on its own. Identities survive a lost key. An organisation can countersign one entry with a key it publishes at its own domain. Assistants connect over MCP and need a separate permission for each action.
The formats
Every object carries its format id. Each links to the file that pins it down: a shared vector file (inputs with the exact outputs every verifier must produce) or a JSON Schema.
| Format | What it is for | Pinned by |
|---|---|---|
3p.card/1 | A card: entries from one face, signed by the person, expiring on its own. | vectors.json |
3p.identity/1 | The start of an identity: the first key, and the recovery key committed by thumbprint. | chains.json |
3p.rotate/1 | Moves an identity to a new key. Signed by the old key and accepted by the new one. | chains.json |
3p.recover/1 | Replaces a lost or stolen key, signed by the paper recovery key. That key can only replace keys, never sign a card. | chains.json |
3p.salience/1 | What matters now: a signed update that supersedes the earlier one and goes stale on its own. | salience.json |
3p.domain-keys/1 | An organisation's key file, at https://<domain>/.well-known/3p-keys.json. | this site's demo file · countersigning |
3p.countersign/1 | An organisation's signature over one entry's content hash, made before the person signs the card. | countersign.json |
3p.head/1 | A record's length and the hash at its end, shown to someone you meet so they can witness it. Each entry's hash is salted; nothing readable is in it. | proof_vectors.json |
3p.witness/1 | Someone else's signature over your head: "I saw this record at this length". A card's written_before uses it to prove an entry was already there. | proof_vectors.json |
3p.withdraw/1 | Takes a published card back from a shared notebook. | No shared vector yet |
3p.capture/1 | A capture: what happened at the moment of writing, and the entries that cite it. See below. | capture.schema.json |
| constraint | A never-do or never-say rule, as it travels inside a card. | constraint.schema.json |
The vector files hold test keys' public halves only. They identify nobody.
The canonical form
A signature is over bytes, so every implementation must turn the same object into the same bytes. The rules:
- Keys sorted by UTF-16 code unit, the way JavaScript's
sort()does. This is not code point order: above U+FFFF the two differ (vectorkey-order-astral). - No whitespace, and strings written as raw UTF-8, never as
\uescapes. Persian half-spaces stay as they are (vectorpersian-zwnj). - No floats. Python writes
1.0where JavaScript writes1, so a float would sign differently in the two. A signer refuses one. - No unsafe integers. Anything beyond ±(253 − 1) is refused, because JavaScript cannot hold it exactly.
- The signature is ECDSA P-256 with SHA-256, over the canonical form without
proof.sig. It is written as rawr‖s, base64url. - Low-S. If
(r, s)is valid, so is(r, n − s), and anyone can make it without the key. The signer emits only the low form. Verifiers accept both, so a signature's bytes never identify anything (vectorslow-sandhigh-s). - The content hash is the ID: SHA-256 of the canonical form without
proof.sig. Nobody can change it without the key. - The subject is the RFC 7638 thumbprint of the identity's first key.
{"b":1,"a":[{"z":null,"y":true}],"_":3,"B":2}
→ {"B":2,"_":3,"a":[{"y":true,"z":null}],"b":1}
→ sha256 c75679b877c2402654baf84f2bf8d139567306febef9bcdfa4f9873a18a0b687
How to check a card
- Take the card, remove
proof.sig, and write the rest in canonical form. - Verify the signature with
proof.key. A short, long, zero or malformed signature is a failed check, never a crash. - Check that the key speaks for the
subject: its thumbprint, or a valid chain of rotations and recoveries from the first key. - Check the dates:
issued,expires, and the date of each key change. - For each countersignature, fetch the domain's key file and check it, as on the countersigning page.
There are three answers, never collapsed into two: valid (with the name you saved, or unknown signer), and invalid.
Anyone can check a card's signature in their own browser.
The verifier answers anything it is given, and accepts only what was signed.
Check your own implementation against the card vectors, the identity chains, the countersignatures and what matters now. Each case gives the input and the answer it must produce. The Python verifier and the browser verifier, in Chromium, Firefox and WebKit, have been run against the same files.
The verifier runs in your browser. It fetches two kinds of thing: this site's samples, when you try one, and the key file of any domain a card says countersigned it.
The assistant interface
An assistant talks to a person's 3P over MCP (JSON-RPC on stdio, no SDK needed). It asks for one face by name, and gets only that face.
claude mcp add 3p -- python3 /absolute/path/to/mcp_server.py
An assistant is not you. It asks before it reads, and you allow each thing separately: reading, writing, signing, publishing.
This holds for assistants that go through 3P. Software running as you on your computer can read your record file directly.
A permission (a grant) is for one assistant, one face, a set of openness levels and a length of time. It names its actions: read, log, sign, publish. read is always included; the rest must be named.
| Tool | What it does | Needs |
|---|---|---|
list_scopes | The faces this 3P will serve | read |
get_persona | Preferences, rules and context for one face, with a summary of its entries | read |
search_facts | Search entries in one face | read |
attested_facts | Only the entries someone else vouched for | read |
get_salience | What matters now, in one face | read |
published | What was published, and whether it is live, expired or taken back | read |
withdraw | Take a published card back | read: taking back only ever shows less |
log | Write an entry in the person's own words, marked as relayed by the assistant | log |
correct | Correct an entry; the earlier wording is kept | log |
mint_packet | Sign a card for one face | sign |
mint_salience | Sign an update to what matters now | sign |
publish | Publish the open part of one face to a shared notebook | publish |
python3 consent.py requests # what asked, and was refused
python3 consent.py grant claude-code work --tiers public --for 1h
python3 consent.py grant claude-code work --for 8h --allow log,publish
python3 consent.py revoke 3
Without a live grant, a call is refused and recorded as a request for the person to see. Every call is recorded, granted or not. private cannot be granted to anything.
An assistant can never widen what you allowed it.
An MCP client's name is whatever the client says it is. A grant is a boundary against software acting without being asked. It does not prove which software is asking.
The capture contract: 3p.capture/1
What a 3P exports, and what another tool (a capture app, a mirror) may hand one. It has two lists:
- acts: what happened at the moment of capture. Each has a kind (
typed,voice,photo,import,relayed,unrecorded), a time, a device and the SHA-256 of its content. A voice or photo act carries its media hash. An import names its source, and a relayed act names the assistant. - facts: the entries. Each cites one act and has its dates, its openness level (it starts at
private) and its tags.
Beyond the schema, the conformance check holds that every fact cites an act in the same document, that ids are unique, and that no field of the mirror appears anywhere, at any depth. The mirror never travels.
{"format": "3p.capture/1", "exported_at": "2026-10-19T09:00:00Z",
"acts": [{"id": "act:fixture05", "kind": "relayed", "captured_at": "2026-10-16T10:00:00Z",
"device": "mcp", "relayed_by": "claude-code",
"content_sha256": "28fea5dcc347d363cc771873322e9e66b4e8e17ada0c2ac0d624c03991c2d2f0"}],
"facts": [{"id": "fact:5", "text": "Load-tested retries at 5x #work", "occurred": "2026-10-16",
"recorded": "2026-10-16T10:00:00Z", "entry_mode": "live", "act": "act:fixture05",
"share_tier": "private", "tags": []}]}
The schema. The example is a test fixture, invented.
Not in the protocol yet
- The hour. Order is proven: your record is a chain, and the people you meet can witness it (
3p.witness/1), so an entry can prove it was there before a meeting, with no clock or server to trust. The exact time is not: every date in a card is still the signer's own. A public daily anchor of everyone's heads is possible later, if a court-grade date is ever needed. - Encryption at rest by 3P itself. The record is a plain SQLite file in
~/.3p, and the node's key is a file next to it. 3P does not encrypt them. They are protected by your operating system's disk encryption (FileVault on a Mac) if it is turned on, and by file permissions. - Passkey countersignatures by individuals, and keys held in hardware. Designed, not built.
- A second implementation. Everything here has been checked only against itself, in Python and in the browser.