peck-identity-v1 — DRAFT
Status: Draft v1, implementation-ready. The unifying identity model for the overlay.social protocol family.
Author: Thomas (kryp2), drafted with Claude.
Started: 2026-06-08.
Companion specs: peck-bio-profile-token-v1.md (ProfileToken on-chain shape), peck-social-protocol.md (post/social vocabulary), peck-social-overlay.md (overlay discovery). Design rationale: KEY_BINDING_DESIGN_2026-06-08.md.
License: Open BSV License v5. (Chain-bound protocol spec — see §0.4.)
0. What this spec is
0.1
This is the identity model for overlay.social: how a human-meaningful, multi-key, cert-bearing identity is rooted in BSV keys and resolved over an open overlay — additively, on top of standard AIP, without changing how content is signed.
It ties together four pieces that already exist in isolation — the BRC-100 identity key, the ProfileToken (peck-bio-profile-token-v1.md), handles (handle_tokens), and AIP-signed posts (peck-social-protocol.md) — and adds the one missing primitive that makes them cohere: the signing-key binding.
0.2 — The problem it solves
BRC-100 wallets derive per-protocol/counterparty keys (BRC-42/43). So the key that AIP-signs a post (pecks.author) is normally not the wallet's root identity key, and users rotate wallets over time. Today the overlay assumes address == P2PKH(ProfileToken.subject) — exactly one signing key per identity — so posts signed by any other key become "ghost authors": silently dropped from identity resolution. This spec removes that assumption.
0.3 — What it is NOT
- Not a change to AIP. Posts stay raw AIP/Bitcoin Schema;
pecks.authoris untouched. Any existing indexer reads posts as before. - Not a handle-namespace spec. Handle ownership/uniqueness is Zooko's Triangle and is deliberately decoupled (§10) into its own future spec.
- Not the ProfileToken on-chain shape — that is
peck-bio-profile-token-v1.md. This spec leaves the ProfileToken UTXO format unchanged (subjectstays immutable, 1:1).
0.4 — Why Open BSV License
This spec is intrinsically chain-bound: binding attestations are on-chain BSV transactions, verification is BRC-52/BRC-3, resolution is over a BSV overlay. Per the ecosystem licensing policy (license by chain-binding, not name), it is Open BSV License v5 — the same as the rest of the overlay.social protocol family.
1. Design principles
-
Identity = pubkey. The universal, immutable root of an identity is a BSV public key — the BRC-100 identity key (
identity_subject). Everything else (profile, handles, signing keys, posts) hangs off it. The key is the only globally-unique, decentralized, secure identifier (Zooko); human-meaningful names are a separate, pluggable layer (§10). -
Binding is additive over AIP. Posts remain raw AIP. The binding layer is a new, optional attestation + index. Apps that don't understand it still read posts; apps that do get the richer, multi-key identity. Progressive, backwards-compatible.
-
Mutual attestation — no key hijacking. A binding is valid only if both the identity key (
certifier) and the signing key (subject) sign it. The identity says "this key is mine"; the key says "I consent." Neither alone is trusted. (BRC-103 mutual-auth, distilled into one self-contained, publicly-verifiable record.) -
Federated trust, not walled garden. Any party can be a
certifier— the verification is issuer-agnostic (BRC-52 signature check). Self-issued (you vouch for your own keys) is the default; third parties MAY also vouch. Trust is a consumer policy (which certifiers to accept), not a protocol capability gate. -
Public, not encrypted. Unlike identity attribute certs (email, KYC — encrypted for selective disclosure per BRC-52/53), a binding attestation is a public claim ("this key is mine"). Its fields are plaintext, so it is directly MAP-discoverable and overlay-servable without keyrings.
-
Self-contained on-chain. A binding is small (~400–500 bytes) and lives directly in its transaction (not UHRP-referenced). The chain is the universal source every overlay reads, so a binding has zero off-chain availability dependency. (UHRP is reserved for large content — avatars, encrypted attribute certs.)
-
Overlay is the trustless public truth; wallet is the private/peer path. Public resolution is served by the overlay index (no wallet/handshake). In authenticated contexts,
discoverByIdentityKey(BRC-100) complements it for the user's own + peer certs with selective disclosure. Both resolve to the sameidentity_subject.
2. The identity model
identity_subject ← BRC-100 identity key (pubkey)
(universal root) the ONE immutable identifier
/ | \
___________/ | \___________________
/ | \
ProfileToken handle_tokens signing_keys
(profile + cert_refs) (human name → subject) (bound posting keys → subject)
subject == root points AT root §10 certifier == root, §4
| |
attributes, each post is AIP-signed
avatar, certs by a bound signing key
|
pecks.author (raw AIP addr)
|
resolve: pecks.author → signing_keys → identity_subject
Reading it: the root is a pubkey. The ProfileToken carries the mutable profile + attribute certs for that root (subject == identity_subject). Handles point at the root (§10). Signing keys are bound to the root (§4). A post is plain AIP, authored by a signing key; resolution maps that key back to the root, so every surface — follow, @mention, notifications, feed — keys on identity_subject, coherent across all bound keys and all rotations.
3. The canonical identity key (normative)
The identity_subject of a user is the pubkey returned by the BRC-100 wallet as its identity key:
identitySubject = wallet.getPublicKey({ identityKey: true }).publicKey // 33-byte compressed hex
- A ProfileToken's
subjectMUST equal theidentity_subjectit represents. - A binding's
certifierMUST equal theidentity_subjectit binds to. - Consumers and issuers MUST use the BRC-100 identity key as the root, NOT a connection/session key, a derived posting key, or a stored
address. (The historical failure that motivated this spec was capturing a non-identity wallet pubkey as the root.)
Note on counterparty: BRC-100
getPublicKey/createSignaturedefault counterparties differ (selfvsanyone). Implementations MUST set counterparty explicitly when deriving the identity key and when verifying signatures, or verification will fail.
4. Signing-key binding attestation (normative)
A binding attestation proves that one signing key belongs to one identity. It is a BRC-52-shaped certificate with a mandatory mutual counter-signature, published directly on-chain as a Bitcoin Schema (MAP) output so it is both indexer-discoverable and TopicManager-parseable.
4.1 — On-chain shape (one OP_RETURN output)
OP_FALSE OP_RETURN
MAP_PREFIX "SET"
"app" "overlay.social"
"type" "keybinding"
"schema_version" "1"
"subject" <signing-pubkey hex> // 33-byte compressed — the key being bound
"certifier" <identity-pubkey hex> // == identity_subject — the root claiming it
"serial" <serialNumber> // 16+ random bytes, hex
"purpose" "social-posting" // binding scope
"revocation" "<txid>.<vout>" // BRC-52 revocation UTXO (spend ⇒ revoked)
"boundAt" <blockHeightHint | iso> // advisory
"sig" <certifier signature> // identity (certifier) over preimage, DER hex
"subjectsig" <subject signature> // signing key (subject) over SAME preimage, DER hex
Values are pushed as plaintext (§1.5). A binding output carries 1 satoshi (ordinal convention) or may be a 0-value OP_RETURN; the revocation UTXO is a separate spendable output the owner retains.
4.2 — Preimage (what both keys sign)
preimage = utf8( canonicalJSON({
app, type, schema_version, subject, certifier, serial, purpose, revocation, boundAt
}) )
canonicalJSON = keys sorted lexicographically, no whitespace, UTF-8. sig and
subjectsig are excluded. Both signatures are BRC-3 ECDSA over these exact
preimage bytes (the BRC-100 createSignature/verifySignature methods apply
their own hashing — issuer and verifier MUST pass the same raw preimage bytes).
Why BRC-3, not raw ECDSA. A BRC-100 wallet never signs with the root identity key directly (
createSignature({identityKey:true})is rejected). It signs with BRC-42-derived child keys, verified against the root + invoice number (BRC-3). The two binding signatures follow this, confirmed against a live BSV-Desktop wallet 2026-06-08.
4.3 — Validity rules (a binding is valid iff ALL hold)
type == "keybinding",schema_version == "1".subjectandcertifierare well-formed compressed pubkeys;subject != certifier.sig(certifier attests): the identity ROOT signs viacreateSignature({data:preimage, protocolID:[1,'keybinding'], keyID:serial})(counterparty defaults toanyone→ publicly verifiable). Verify withProtoWallet('anyone').verifySignature({data:preimage, signature:sig, protocolID:[1,'keybinding'], keyID:serial, counterparty:certifier})→.valid. Proves the identity root signed, without exposing its private key.subjectsig(subject consents): the posting key signs viacreateSignature({data:preimage, protocolID:[1,'identity'], keyID:'1', counterparty:'self'}). Verify RAW:PublicKey(subject).verify(preimage, subjectsig). Proves control of the post-author key (=pecks.author).revocationoutpoint is unspent (see §7).
Rule 4 prevents claiming a key you do not control. Rule 3 prevents binding a key to someone else's identity. Together: the mutual proof.
5. Overlay mechanics (normative)
5.1 — KeyBindingTopicManager (admit on tm_key-binding)
Admits an output iff it parses as a §4.1 binding and passes all §4.3 rules (signatures verified at admit-time). A binding MAY be admitted before/without a ProfileToken existing for certifier (the profile hydrates later); the binding itself is self-contained.
5.2 — KeyBindingLookupService → signing_keys
CREATE TABLE IF NOT EXISTS signing_keys (
signing_pubkey TEXT NOT NULL, -- subject
signing_address TEXT NOT NULL, -- P2PKH(subject) — JOIN key vs pecks.author
identity_subject TEXT NOT NULL, -- certifier == ProfileToken.subject
binding_outpoint TEXT NOT NULL, -- where admitted
revocation_outpoint TEXT, -- spend ⇒ revoked
bound_at BIGINT,
spent BOOLEAN DEFAULT FALSE, -- UTXO lifecycle / revoked
PRIMARY KEY (signing_pubkey, identity_subject)
);
CREATE INDEX IF NOT EXISTS idx_signing_keys_address ON signing_keys (signing_address);
CREATE INDEX IF NOT EXISTS idx_signing_keys_identity ON signing_keys (identity_subject);
5.3 — Canonical rule (one key → one identity)
A signing_pubkey MUST resolve to at most ONE identity_subject at a time. If two valid bindings exist for the same signing_pubkey, earliest-admitted-unspent wins (the ProfileToken canonical pattern). Competing later bindings are ignored until the first is revoked.
5.4 — Lifecycle
Spending the binding_outpoint OR the revocation_outpoint sets spent = true; the binding stops resolving.
6. Resolution contract (normative)
pecks.author is unchanged (raw AIP address). Identity resolution joins through signing_keys:
pecks.author = signing_keys.signing_address WHERE NOT spent
→ identity_subject
→ ProfileToken (profile, certs) + handles
6.1 — Endpoints (extend existing)
POST /v1/identities/resolve— for each input address: first look upsigning_keys.signing_address → identity_subjectand hydrate; fall back to the legacyaddress == P2PKH(subject)match. Ends "ghost authors."GET /identity/:pubkey— addsigningKeys: [{ pubkey, address, boundAt, bindingOutpoint }]to the response.GET /identity/:subject/signing-keys— list bound keys for an identity.
6.2 — Social surfaces
Follow / @mention / notifications / following-feed MUST key on identity_subject (resolved via §6) rather than raw pecks.author, so they cohere across all bound keys.
6.3 — Dual discovery
The overlay endpoints are the trustless public truth. In authenticated contexts, consumers MAY additionally use the wallet's discoverByIdentityKey (BRC-100) for the user's own and peer certs with selective disclosure. Both resolve to the same identity_subject.
7. Revocation
Each binding carries a revocation outpoint (a UTXO the identity owner controls). Spending it revokes the binding (BRC-52 standard). The overlay marks the row spent and stops resolving it. Reuses the existing checkRevocation path (WoC /tx/<txid>/<vout>/spent, cached). To disconnect a key (e.g. after a wallet change), burn its revocation UTXO.
8. Backwards-compatibility
- Posts are unchanged raw AIP;
pecks.authorand all existing indexers are unaffected. - The binding layer is additive: an app ignorant of it still renders posts (falling back to raw address / paymail). An app that understands it resolves the full identity.
- Adoption is per-key and opt-in: a user attests the keys they want bound, when they want.
- An unbound key is simply unresolved (renders as raw address) — an honest, graceful degradation, not an error.
9. Security
- No key hijack: mutual
subjectsig(§4.3 rule 4). - No identity hijack:
certifiersignature (§4.3 rule 3) — you can't bind to a root you don't own. - No double-claim: canonical earliest-unspent rule (§5.3).
- Trustless serving: the TopicManager verifies both signatures before admit; the LookupService indexes only verified bindings. Consumers trust the overlay, not any single app.
- Revocable: UTXO-spend, the BRC-52 trust model.
- Federated trust:
certifieris open; consumers choose which certifiers they accept. Self-certification is the floor; third-party vouching composes on the same structure.
10. Handles & Zooko — deliberately decoupled
Handle ownership/uniqueness is Zooko's Triangle (human-meaningful / decentralized / globally-unique — pick 2). It is a fundamental tradeoff, not solvable here, and MUST NOT block key-binding.
Principle this spec commits to: identity = pubkey (root); handle = a pluggable layer that points AT an identity; value/trust comes from attestation + reputation, not a global ownership lock. A handle is claimed by an identity and MAY be vouched by certifiers; multiple claims across namespaces can coexist, and the canonical one is decided by attestation/reputation — the same shape as key-binding.
handle_tokens (first-come on-chain) is one namespace and MAY coexist with others (paymail, third-party registries); all resolve handle → identity_subject. The full namespace model (authority, dispute, reputation) is a separate future spec. The bridge: once key-binding yields a coherent identity_subject, handle_tokens anchors on the identity rather than a single key.
11. Consumer integration contract
To resolve the identity behind a post:
1. Read pecks.author (raw AIP address) — unchanged.
2. POST /v1/identities/resolve { addresses: [author] } → { identity_subject, handle, displayName, avatarRef, ... } or empty.
3. If empty: render the raw address / paymail (graceful fallback).
4. For richer identity (certs, all signing keys): GET /identity/:identity_subject.
5. Key all relational logic (follow/mention/block/feed) on identity_subject, never raw address.
To bind a key (issuer/wallet flow):
1. certifier = getPublicKey({ identityKey: true }).publicKey (the identity root).
subject = getPublicKey({ protocolID:[1,'identity'], keyID:'1', counterparty:'self' }).publicKey
(the posting key the wallet AIP-signs with = pecks.author).
2. Build §4.1 fields; compute the §4.2 preimage bytes.
3. sig = createSignature({ data:preimage, protocolID:[1,'keybinding'], keyID:serial })
(certifier attests). subjectsig = createSignature({ data:preimage,
protocolID:[1,'identity'], keyID:'1', counterparty:'self' }) (subject consents).
Both are DER hex.
4. createAction: one OP_RETURN binding output (bindingToScript) + one retained
revocation UTXO → wallet funds + broadcasts → submit BEEF to overlay /submit
with x-topics: tm_key-binding (or let the broadcaster forward it) → admitted.
12. Conformance — what an implementer MUST do
- Indexer/overlay: implement
KeyBindingTopicManager(verify both sigs at admit) +KeyBindingLookupService(signing_keys, canonical rule, revocation lifecycle); extend the three §6.1 endpoints. - Wallet/issuer: implement the §11 binding flow with dual signing and explicit counterparty.
- Consumer app: resolve via §11 and key relational logic on
identity_subject. - Everyone: treat
pecks.authoras raw AIP; never assumeaddress == P2PKH(subject)— always resolve throughsigning_keys.
Core: identity is a pubkey; everything human (profile, name, the posts you write across many keys and many wallets) hangs off it by public, mutual, revocable, federated attestation — served trustless over an open overlay, additive over plain AIP. That is BRC > AIP made real: not a signature, but an open, multi-key, verifiable identity layer over Bitcoin Schema content.