overlay.social
↯ RSS· Draft · specs in the open

SPEC · TKN Token-state · v0.3 draft

Token-state assertions.

Ownership-bearing entities live as stateful UTXOs whose update rules are enforced by Bitcoin Script. The locking-script template, canonical data layout, spend rules, and overlay submission contract.

Last revised2026-05-09
Editoroverlay.social wg
StatusWorking draft
LicenseMIT
Raw markdown →

peck-social-token — v1

Status: v1, implementation-ready. Canonical-implementable subset of peck-social-token-v1-DESIGN.md (full design rationale lives there). Companion spec to peck-social-v1.md (legacy OP_RETURN bridge profile) and peck-bio-profile-token-v1.md (sibling stateful-token spec at the identity layer). Author: Thomas (kryp2) and contributors. Started: 2026-05-05 (draft) → promoted to v1 on 2026-05-17. Reference design: peck-social-token-v1-DESIGN.md. License: MIT.

0. What this spec is

This is the concrete on-chain shape for stateful social content tokens on BSV — the PostToken family. It is the canonical- implementable subset of peck-social-token-v1-DESIGN.md, with all must-decide questions from that design doc resolved and the v1 surface frozen.

It is not the abstract design document. For full rationale, trade-off discussion, marketplace deep-dives, cross-app subtype matrices, and bridge architecture, read peck-social-token-v1-DESIGN.md. This document is what an indexer team, wallet implementer, or app developer needs to read to add PostToken support end-to-end at v1.

It composes BRC-100 / BRC-43 / BRC-52 / BRC-77 / BRC-104 / BRC-22 / BRC-24 / BRC-95. No new BRC is introduced.

It supersedes the MAP type=post OP_RETURN form from peck-social-v1.md for any consumer that wants stateful, transferable, cryptographically-enforceable posts. Both forms coexist on the same chain — consumers MAY render either, MUST prefer PostToken when both are present for the same logical artefact.


1. Design principles

  1. Subject ≠ owner. subject is the immutable author identityKey (peck-bio HandleToken-derived). owner is the current spender (initially equal to subject, but transferable). This separation enables transferable posts, paid-content owner-rotation, and custodial-recovery without breaking attribution.
  2. HandleToken-canonical identity. A PostToken's subject MUST be the owner of a canonical HandleToken (per ls_peck-bio-handle). Bare-pubkey-subjects are forbidden — the handle binding is required to mint. This is a deliberate tightening vs the ProfileToken posture (which allows bare-pubkey).
  3. User-choice content storage. Both inline (Layer A) and ref-based (UHRP/external URL) storage are first-class. The user picks per-post. MAP content_mode={inline|ref} records the choice; MAP content_hash=<sha256> always pins integrity.
  4. One canonical envelope, app discriminator. Every post in every peck-app (peck.to, peck.ink, peck.press, peck.world, peck.events, peck.channel) is a PostToken-family token sharing the three-layer envelope. app MAP key + Layer C subtype contract differentiates.
  5. Edit = burn + mint child. contentRef is immutable. Edits produce a new PostToken naming parent_outpoint. The original is frozen — required for marketplace integrity (buyer must verify the content they saw in the listing).
  6. Marketplace as state-machine extension. Transfer, sale, auction are @methods on the same spine — no new contract type needed. Value flows ship in tiers (v1 = transfer + paywall + simple tips; v1.1 = royalty mechanisms).

2. Token shape — three layers + UHRP

Every PostToken UTXO output has this structure (mirrors peck-bio-profile-token-v1.md §2):

output value: 1 satoshi (1Sat ordinal convention)

output.lockingScript:
    +--------------------------------------------------------+
    |  Layer A: ord-inscription envelope                     |
    |    OP_FALSE OP_IF                                      |
    |      "ord"                                             |
    |      OP_1 "application/json"                           |
    |      OP_0 <state-json>                                 |
    |    OP_ENDIF                                            |
    +--------------------------------------------------------+
    |  Layer B: MAP tags (Bitcoin Schema discovery)          |
    |    OP_FALSE OP_RETURN                                  |
    |      MAP_PREFIX                                        |
    |      "SET"                                             |
    |      "app" <app>                                       |
    |      "type" <kind>                                     |
    |      "subject" <pubkey-hex>                            |
    |      "content_mode" <"inline"|"ref">                   |
    |      "content_hash" <sha256-hex>                       |
    |      ... (see §6)                                      |
    |    | (pipe push, 0x01 0x7c)                            |
    |      AIP_PREFIX (optional)                             |
    +--------------------------------------------------------+
    |  Layer C: sCrypt state-machine bytecode                |
    |    @prop encodings + state pushes                      |
    |    + PostToken contract template                       |
    |    OP_PUSH_TX-validated continuation enforcement       |
    +--------------------------------------------------------+

  UHRP (off-chain, hash-locked from Layer C `contentRef`)
  ── content blobs > 4 KB, all binary media, and any text the
     author chose to store ref-style (§5).

Why all three layers + UHRP in this configuration:

  • Layer A: drop-in compatibility with 1Sat-ordinals indexers (junglebus, 1sat-api, gorillapool). In inline-mode the post content lives here directly as JSON; in ref-mode the manifest records the UHRP/external pointer.
  • Layer B: drop-in compatibility with Bitcoin Schema indexers (peck-indexer-go, bitbus). MAP tags route the TX to the PostToken handler without sCrypt eval.
  • Layer C: canonical truth. Mutable economic state, version monotonic, transfer/sale enforcement.
  • UHRP: large or binary payloads, integrity-pinned via content_hash.

Disagreement between layers is a malformed token.


3. PostToken — canonical state

3.1 Layer C @prop fields

Field Type Mutability Notes
subject PubKey Immutable Author identityKey, MUST own a canonical HandleToken (§4)
app ByteString Immutable UTF-8 app discriminator (peck.to, peck.ink, …)
kind ByteString Immutable post, reply, repost, canvas, event, edit, …
contentRef ByteString Immutable Per content_mode: inline UTF-8 bytes OR uhrp://<sha256> OR absolute URL
contentHash ByteString Immutable SHA-256 of the content payload (always set)
mediaTypeTag ByteString Immutable text/plain, application/json, image/png, …
parentOutpoint ByteString Immutable <txid>.<vout> of parent, or 36 zero bytes if root
topic ByteString Immutable Optional URN (url:, tx:, image:, cat:, …)
birthHeight bigint Immutable Author-claimed block height at mint
owner PubKey Mutable Spend authority — initially subject, transferable
version bigint Mutable Monotonic, +1 per state-changing call
priceSats bigint Mutable 0 = no offer; >0 = fixed-price paywall/for-sale (§8)
flags ByteString Mutable Bit-packed: locked, hidden, locked-replies

3.2 @methods (Wave 1 surface)

Pseudo-shape. Real impl uses OrdinalNFT + OP_PUSH_TX hashOutputs.

  • update(newPriceSats, newFlags, ownerSig, trailing) — owner-only economic mutation; immutables preserved; requires !flags.locked; version += 1. (Note: no royaltyBps in v1 — see §7.)
  • transfer(newOwner, ownerSig, trailing) — pure owner rotation, no value movement. Same shape as shipped HandleToken.transfer.
  • reply(replyOutputRaw, ownerSig, trailing) — only enforced when flags.locked-replies is set; gates threads. Open replies are fresh mints naming parentOutpoint, no parent participation.
  • burn(ownerSig) — no continuation, SigHash.ANYONECANPAY_ALL. Also the first half of an edit (§5.5).

sell() and bid()/settle() (marketplace primitives) are reserved in the contract surface but ship in Wave 3+ per §7 unbundling.

3.3 BRC-100 owner-authority derivation

owner is a derived pubkey, mirroring peck-cat-owner-authority.md:

protocolID:    [2, 'peck post-authority']
keyID:         outpointHash.toLowerCase().slice(0, 16)
counterparty:  'self'
Field Value Rationale
protocolID[0] 2 BRC-43 security level "every use" (wallet prompts per call)
protocolID[1] "peck post-authority" Family-wide namespace (no .to suffix — covers all PostToken subtypes incl. CanvasToken, EventToken, ChannelToken, AuctionToken)
keyID first 16 hex chars of sha256(outpoint) Per-token scoping; outpoint == <txid>.<vout> of the mint TX
counterparty "self" Derivation is owner-internal

Per-token derivation lets a buyer spend without exposing their root identity. The protocolID is family-wide (single string covers all PostToken subtypes) so a single peck-marketplace frontend can derive owner keys uniformly across posts, canvases, events, and auctions.

Constant location: TBD — Wave 1 plants POST_AUTHORITY_PROTOCOL_ID and postAuthorityKeyId(outpoint) in the shared contracts module (likely peck-bio/src/contracts/ PostToken.ts alongside OWNER_AUTHORITY_PROTOCOL_ID from peck-cat-owner-authority.md). Other peck-* repos import from there — do not duplicate the string.


4. Identity binding — HandleToken-canonical (REQUIRED)

4.1 Rule

A PostToken's subject MUST be the owner of a canonical HandleToken (per ls_peck-bio-handle). The overlay topic-manager tm_peck-social-post REJECTS admissions where no canonical HandleToken can be resolved for subject.

This is stricter than peck-bio-profile-token-v1.md (which allows bare-pubkey profiles with paymail fallback). PostToken-content attribution is too load-bearing to allow ambiguous authors — @-mentions, mute lists, blocklists, marketplace reputation all key on canonical handle.

4.2 Mint pre-flight

Wallet UX flow:

  1. User initiates mint in peck.to / peck.ink / etc.
  2. Wallet queries ls_peck-bio-handle.byOwner(<identityKey>).
  3. If no handle: surface "mint a @handle first" CTA, link to peck.bio. Mint flow blocks.
  4. If handle present: proceed with PostToken mint; subject = <identityKey> (the handle's owner pubkey).

4.3 Render-time resolution

Render shim joins on subject:

def enrich_post(post):
    cat     = ls('ls_peck-cat',         agent_key=post.subject)
    profile = ls('ls_peck-bio-profile', subject=post.subject)
    handle  = ls('ls_peck-bio-handle',  owner=post.subject)
    # By §4.1, handle is guaranteed to exist for any admitted post.
    if cat:
        post.author_display = f"@{cat.name} (cat)"
        post.author_link    = f"/cat/{cat.agent_key}"
    else:
        post.author_display = f"@{handle.handle}"
        post.author_link    = f"/u/{handle.handle}"
        if profile:
            post.avatar_url      = uhrp(profile.avatar_ref)
            post.verified_badges = verify_certs(profile.cert_refs)
    return post

Cats as authors: when a CatToken's agentKey signs a post, the join finds cat first. The cat.owner field still resolves to a HandleToken (cat ownership flows through a handle) — subject itself can be the agentKey because the cat has its own handle- bound identity via the CatToken's relationship to its owner.

4.4 Handle-transfer at display

If @thomas's HandleToken transfers to a new owner, subject does NOT change (it's the immutable author identityKey). The old posts render under whichever handle the original author currently controls (or no handle, in which case the post becomes un-admittable for new state-changes but stays readable). Handle transfer is namespace- reattribution at display layer.

4.5 @-mentions

Render-time resolution only:

  1. Extract @<handle> patterns from content
  2. Lookup each via ls_peck-bio-handle.byHandle
  3. Linkify to /u/<handle>

Mentions are NOT canonical refs in Layer C. For hard references, use topic = identity:<pubkey> (URN dispatch per peck-social-v1.md §6).


5. Content storage modes

Both storage modes are first-class. Authors choose per-post.

5.1 Two modes

Mode Storage Layer C contentRef Layer A JSON content field
inline Layer A (on-chain) UTF-8 bytes of content Full content inline in manifest
ref UHRP or external URL uhrp://<sha256> OR absolute https://… URL Pointer field, content fetched out-of-band

5.2 MAP tag shape (Layer B)

For all PostTokens:

MAP SET
  app=<app>
  type=<kind>
  subject=<pubkey-hex>
  content_mode=<"inline"|"ref">
  content_hash=<sha256-hex>           # ALWAYS set, regardless of mode
  content_ref=<uhrp-hash>             # SET if mode=ref and storage=UHRP
  content_url=<absolute-url>          # SET if mode=ref and storage=external
  ...
  • content_hash is always set. It pins the integrity of the content payload independently of where the bytes live. Indexers MUST recompute on ingest and reject malformed.
  • Exactly one of content_ref / content_url is set in ref mode. Neither in inline mode.
  • For inline mode, the content payload is the UTF-8-decoded bytes of Layer A's JSON content field (media_type=text/plain) or the raw bytes (media_type=application/octet-stream style).

5.3 Decision flowchart

                content payload
                       |
                  size, type?
              /        |        \
        text          text       binary
        < 1 KB        1-4 KB      (any size)
            |          |              |
        inline      inline OR ref   ref (UHRP)
        (suggested)  (user choice)  (forced)
                       |
                  >  4 KB text
                       |
                  ref (suggested,
                   user can override)

Soft defaults (wallet UI nudge, not enforced):

  • < 1 KB text → inline
  • 1–4 KB text → user-prompted, either mode acceptable
  • 4 KB text → ref (warn on inline due to fee inflation)

  • Binary (image, audio, video, anything non-UTF-8) → ref only; inline rejected by mint validator

Thresholds are advisory in v1; the protocol accepts any size up to node policy limits. Soft-default suggestions live in peck-bio/web/postWizard.ts (planted in Wave 2).

5.4 Display-equality principle

peck.to display routes (peck.to/b/ord/<txid> and family) MUST render inline and ref content identically. The viewer experience is uniform — the only place storage-mode surfaces is in a "post details" / "provenance" inspector.

Rationale: the storage choice is an author's economic / longevity preference, not a viewer concern. A reader scrolling a feed should not be able to tell which posts are inline vs ref-stored without opening the inspector.

5.5 Edit semantics — burn + mint child

contentRef is immutable. To edit a post:

  1. Burn: spend the original UTXO via burn(ownerSig) — no continuation.
  2. Mint child: in the same TX (or a follow-on), mint a fresh PostToken with:
  3. kind = edit
  4. parentOutpoint = <original-outpoint>
  5. MAP parent_outpoint=<original-outpoint> (also in Layer B for indexer discovery)
  6. same subject, new contentRef / contentHash

Indexer rule: the latest-canonical token in the parent-chain from a root is "current"; old versions render as history. Edit-chain forks resolved by lowest-block-height + lex-min outpoint (same canonical rule as ProfileToken).

Marketplace integrity: Original PostTokens (referenced from a listing's content_hash) are immutable. A buyer who saw listing X at content_hash=H is guaranteed the underlying bytes at H haven't changed since listing. Edits are forward-only forks in the parent chain, never in-place mutation of the listed token.

5.6 Soft-delete via flags

flags.hidden toggle: indexer stops rendering, chain continues. The post can be un-hidden later. For irreversible removal, use burn without a mint-child (no edit, just delete).


6. Bitcoin Schema MAP-tag integration (Layer B)

Layer B reuses peck-social-v1.md §3.3 conventions plus PostToken additions:

Key Value Notes
app peck.to, peck.ink, … App discriminator (v1 existing)
type post, reply, canvas, event, edit Kind (v1 existing + edit new for child-mints)
subject <pubkey-hex> Author (== Layer C subject; HandleToken-bound per §4)
content_mode inline | ref NEW: storage choice
content_hash <sha256-hex> NEW: integrity binding, always set
content_ref <uhrp-hash> NEW: when content_mode=ref and storage=UHRP
content_url <absolute-url> NEW: when content_mode=ref and storage=external
parent_outpoint <txid>.<vout> NEW: for replies AND edits — outpoint, not just txid
context tx:<parent-txid> / url:… Legacy v1, kept for Bitcom-compat
state_hash <sha256-of-LayerA-JSON> NEW: ties Layer A ↔ B integrity
version <integer> Layer C version
action mint / update / transfer / burn / edit TX action discriminator
geo <lat>,<lng> Optional, for peck.world
topic URN per peck-social-v1.md §6 Cross-references
cert_ref <cert-txid> Existing BRC-52
schema_version 1 This spec version

BRC-43 namespaced form (2/peck-social/v1.<key>) emitted alongside bare keys per peck-social-v1.md §3.2.

AIP: canonical BSM-compact today, migrating to BRC-77 algorithm marker over time. AIP is OPTIONAL on PostToken mutations — sCrypt already proves spend-authority — but RECOMMENDED for indexer- friendly attribution.


7. Value flows (v1 vs v1.1)

The five distinct value-flow patterns are explicitly unbundled. v1 ships only the foundational primitives; royalty mechanisms defer to v1.1 once we have real-usage data from v1.

7.1 v1 — what ships now

Flow Mechanism Notes
Transfer transfer(newOwner, ownerSig) — pure owner rotation, no value Same shape as HandleToken.transfer. Gift / move.
Paywall priceSats state-field on PostToken → contract-enforced fee-to-read Reader pays sats; overlay derives access key. §8.
Simple tips Separate event-type, NOT PostToken state §7.3 below.

7.2 Paywall flow (v1)

update() mutates priceSats. When priceSats > 0:

  • Content payload is AES-encrypted with key K (UHRP-stored, K fetched via a paywall ack from the overlay).
  • Reader's wallet pays priceSats to a P2PKH(owner) output (or via BRC-104 channel for streaming subscribers).
  • Overlay verifies the payment, releases the decryption key.
  • No PostToken state-change required for a "read" — paywall is a fetch-time fee, not a UTXO spend.

For paid transfer (selling the post-as-asset), wait for v1.1 sell() — v1 supports only transfer (gift) + paywall (read fee).

7.3 Simple tips (v1)

Tips are a separate event-type, not a PostToken state-change. Mechanism:

TX:
  input 0: tipper's funding UTXO
  output 0: P2PKH(PostToken.owner), value = tipAmount sats
  output 1: OP_RETURN MAP "SET"
              "app" "peck.tips"
              "type" "tip"
              "target_outpoint" "<post-txid>.<vout>"
              "target_subject" "<author-pubkey>"
              "amount" "<sats>"
              ["msg" "<utf-8 short message>"]
  output 2: change

The PostToken UTXO is not touched by a tip. Indexers aggregate tips by target_outpoint for display ("@thomas tipped 1000 sats"). This keeps tip volume off the PostToken UTXO-contention path — tipping a hot post doesn't force version-chain churn.

Discovery via tm_peck-social-tip topic (light, OP_RETURN-only). Reuses peck-social-v1.md MAP scaffolding.

7.4 v1.1 — deferred royalty mechanisms

The following value flows require multi-output enforcement, collaborator-share tracking, or new state-machine complexity. They are explicitly deferred to v1.1 pending real-usage data from v1 primitives.

Flow Status Mechanism sketch Why deferred
Microtips ✅ v1 Random one-to-one appreciation (§7.3) Foundational primitive — ships now.
Like-as-payment ⏳ v1.1 Atomic sat per engagement, contract-enforced Needs measurement: does it dominate TX volume? Better as L2/channel?
Sale-royalties ⏳ v1.1 sell() with multi-output split: P2PKH(seller) + P2PKH(subject) cut royaltyBps field reserved; needs decision on splitter contract vs inline outputs (DESIGN §15)
Paywall-revenue-share ⏳ v1.1 Premium-content splits via BRC-104 channel extension Needs BRC-104 production maturity (OVERLAY_ECONOMICS_DISPATCH.md Phase 2)
Reply-rewards ⏳ v1.1+ Parent post gets a cut on reply-engagement Complex state-machine — defer until simpler flows have usage data

Explicit note: Royalty mechanisms are deferred to v1.1 after real-usage-data from v1 primitives. The PostToken Layer C state reserves the priceSats field (v1) but does NOT include royaltyBps in v1 — adding mutable economic fields without sale- flow is dead weight. v1.1 adds royaltyBps alongside sell() in a backwards-compatible way (Layer A JSON gets new optional field; indexers default to 0).

7.5 Why unbundling matters

Premature commitment to royalty-bps would lock in:

  • A single payee model (single subject cut) — but real CanvasToken collaborator-splits need multi-payee.
  • A specific split policy (e.g. fixed 10%) — but creator-economics data we don't have yet might favour tiered or capped models.
  • Contract complexity (sell() + RoyaltySplitter) shipping before marketplace UX validates demand.

v1 ships the substrate that all five flows compose on (PostToken spine + Layer C state + overlay indexing). v1.1 layers economics on top once primitives are battle-tested.


8. Paywall integration (BRC-104)

Per OVERLAY_ECONOMICS_DISPATCH.md: overlay default-on, content- display-app exception.

8.1 Read-paywall (overlay fetch fee)

/v1/post?… queries subject to BRC-104 channel paywall. Content- display apps configure free quotas:

mountPaywall({
  free_quotas: {
    '/v1/post': { limit: 20, per: 'session' },
    '/v1/post/:outpoint': { limit: 100, per: 'session' },
    …
  },
  exceptions: [/* …content-display routes… */],
})

Independent of per-post pricing — this is the read fee for hitting the indexer.

8.2 Per-post paywall (contract-enforced)

A priceSats-bearing PostToken is a paywall token:

  1. Content (UHRP) AES-encrypted with key K at mint time.
  2. Layer C priceSats > 0 declares the per-read price.
  3. Reader pays priceSats to P2PKH(owner) (one-shot) or via BRC-104 channel (streaming).
  4. Overlay observes the payment, derives buyer's access-key (BRC-42 with owner as counterparty), serves the decryption bundle.

No PostToken UTXO spend is required for a read. priceSats mutates only via update() (owner re-prices) or transfer() (new owner inherits price).

Subscriber pattern: BRC-104 channel between reader and owner; keys released per debit. peck.channel is the primary consumer (extends PostToken with ChannelToken).


9. Reply / thread / parent-ref

9.1 Binding

parentOutpoint is a Layer C @prop() (immutable). Same value is also emitted as Layer B MAP keys for indexer discovery:

MAP SET
  app=peck.to  type=reply
  parent_outpoint=<txid>.<vout>
  context=tx:<parent-txid>     // legacy v1 shape kept for Bitcom-compat
  …

Double-emission mirrors Profile/Handle's pattern for subject and handle.

9.2 Thread discovery

ls_peck-social-post indexes: subject, app, kind, parent_outpoint, root_outpoint, topic. Overlay computes root_outpoint at admit-time by walking the parentOutpoint chain and caches the root. O(depth) per insert; depth is small in practice.

CREATE INDEX idx_post_tokens_parent_outpoint
    ON post_tokens (parent_outpoint) WHERE spent = FALSE;
CREATE INDEX idx_post_tokens_root_outpoint
    ON post_tokens (root_outpoint) WHERE canonical = TRUE;

9.3 Locked-replies (optional)

If flags.locked-replies is set, the parent UTXO must be co-spent in the reply TX (author authorises each reply). Parent's reply() @method enforces. For the common case (open replies), parent is untouched — reply is a fresh mint naming parentOutpoint. Cheap.


10. Discovery / overlay indexing

10.1 Topic managers

Topic Admits Lookup service
tm_peck-social-post PostToken family, app ∈ {peck.to, peck.press, peck.world} ls_peck-social-post
tm_peck-social-tip OP_RETURN tip events (§7.3) ls_peck-social-tip
tm_peck-ink-canvas CanvasToken (app=peck.ink) — Wave 5 ls_peck-ink-canvas
tm_peck-events-event EventToken (app=peck.events) — Wave 5 ls_peck-events-event
tm_peck-bio-profile (existing) ProfileToken ls_peck-bio-profile
tm_peck-bio-handle (existing) HandleToken ls_peck-bio-handle
tm_peck-cat (existing) CatToken ls_peck-cat

Federation: any operator can run any subset. peck.to runs all. tm_peck-social-post REJECTS admissions where subject has no canonical HandleToken (§4.1).

10.2 Lookup-service shape

Mirrors PeckBioProfileLookupService:

  • outputAdmittedByTopic parses Layer A/B/C, inserts/updates row by outpoint, recomputes canonicality, validates HandleToken binding.
  • Atomic-BEEF persisted alongside row for self-verifying hydration.
  • NULL-block-height-sorts-last canonical rule.
  • Lookup query shape: subject?, owner?, app?, kind?, outpoint?, parent_outpoint?, root_outpoint?, topic?, geo?, priced?, content_mode?, before_height?, before_outpoint? — returns BRC-24 LookupFormula.

10.3 REST hydration

GET /v1/post                          → feed (cursor-paginated)
GET /v1/post/:outpoint                → single post
GET /v1/post/:outpoint/history        → version chain (edits via parent-chain)
GET /v1/post/:outpoint/thread         → full subtree
GET /v1/post?subject=<pk>             → author's posts
GET /v1/post?parent=<outpoint>        → direct replies
GET /v1/post?root=<outpoint>          → full thread (recursive)
GET /v1/post?app=peck.world&geo=NEAR(lat,lng,km)
GET /v1/post?priced=true              → paywalled posts (v1)
GET /v1/post?content_mode=ref         → ref-storage subset

Pagination per peck-atlas/conventions.md (structured cursor: before_height + before_outpoint tiebreaker; no offset).

10.4 Cross-app federated query

GET /v1/everything?subject=<pk>
→ { posts, profile, handle, cats, tips, … }

Single join, all token-types for one subject.


11. Backward compatibility — migration from legacy Bitcoin Schema

peck.to has 7+ years of OP_RETURN MAP type=post content. Migration is additive, never destructive (same shape as peck-bio-profile-token-v1.md §11).

11.1 Dual-render

For an author with both legacy v1 posts AND PostToken posts: render shim shows both, ordered by canonical timestamp. PostToken posts surface a "token-backed" badge; v1 posts untouched.

11.2 Per-author opt-in mint binding legacy posts

A user (or registrar) mints a PostToken with kind="legacy-bind", contentRef = uhrp://<hash-of-v1-content> (re-pinned), parent_outpoint = <v1-post-txid>.<vout>, subject matching the v1 AIP-signing key (which MUST have a canonical HandleToken per §4). Renderers prefer PostToken state when present.

11.3 No mandatory cutover

Phases: - 0 Legacy canonical (current). - 1 PostToken minting available; legacy continues default. - 2 peck.to UI nudges migration; legacy still writable. - 3 New writes default PostToken; legacy v1 opt-in. - 4 Legacy writes deprecated; reads continue forever.

Phase 1 gated on Wave 1+2 ship. Phase 3 gated on SDK + overlay scale-out.

11.4 Indexer dual-support

peck-overlay-schema already runs both: PeckSchemaLookupService for legacy v1 OP_RETURN posts; PeckSocialPostLookupService (Wave 1 deliverable) for PostToken- family. Render shim joins.


12. Wave 1 implementation scope

Wave 1 is the first runde of implementation work. Deliverables strictly limited to:

12.1 Contracts (peck-bio)

  • peck-bio/src/contracts/PostToken.ts — sCrypt contract with update, transfer, burn, reply @methods. No sell(), no bid()/settle(), no royaltyBps field.
  • Shared constants module exports:
  • POST_AUTHORITY_PROTOCOL_ID = [2, 'peck post-authority']
  • postAuthorityKeyId(outpoint) helper
  • Layer A/B/C extractor wired into peck-bio/scriptDecode.

12.2 Overlay (peck-overlay-schema)

  • PeckSocialPostTopicManager — admits PostToken outputs, rejects on missing HandleToken binding.
  • PeckSocialPostLookupService — implements lookup shape per §10.2.
  • post_tokens table — schema matches §3.1 + indexer metadata (outpoint, spent, canonical, block_height, atomic_beef, root_outpoint).
  • HandleToken-binding validator in admit pipeline.

12.3 REST (peck-overlay-schema)

/v1/post/* routes per §10.3. Mint-only first — no marketplace endpoints. Endpoints:

  • GET /v1/post
  • GET /v1/post/:outpoint
  • GET /v1/post/:outpoint/history
  • GET /v1/post/:outpoint/thread
  • GET /v1/post?subject=…
  • GET /v1/post?parent=…
  • GET /v1/post?root=…

Listing-discovery (?priced=true, ?kind=auction) ships Wave 3 as part of marketplace surface.

12.4 Wallet integration (peck-bio web)

  • postWizard.ts — UI flow for mint with content_mode toggle and soft-default suggestions per §5.3.
  • HandleToken pre-flight check per §4.2.
  • Free-quota paywall config for content-display routes.

12.5 Tip event-type (separate from PostToken)

  • PeckSocialTipTopicManager for tm_peck-social-tip.
  • tip_events table — flat, no state-machine.
  • GET /v1/post/:outpoint/tips aggregate endpoint.

12.6 NOT in Wave 1

  • sell() method and listing-discovery — Wave 3.
  • AuctionToken — Wave 4.
  • CanvasToken / EventToken / ChannelToken — Wave 5.
  • royaltyBps field and marketplace primitives — v1.1.
  • Bridge attribution to X / Mastodon / Nostr — Wave 6.
  • Aggregator contracts for likes — explicit non-goal.

13. Cross-app subtypes (preview)

Per-app contracts that share the PostToken spine. Full subtype matrix in peck-social-token-v1-DESIGN.md §6.2. Wave 1 ships only the base PostToken; per-app subtypes follow in later waves.

App Token Extends Wave
peck.to PostToken (base) 1
peck.press PostToken (base) 2
peck.world PostToken (base) 2
peck.ink CanvasToken PostToken 5
peck.events EventToken PostToken 5
peck.channel ChannelToken PostToken 5

Identity-axis tokens (ProfileToken, HandleToken, CatToken) are NOT PostToken-family — they have their own contracts and topic managers (already shipped per peck-bio-profile-token-v1.md and peck-cat-owner-authority.md).


14. Indexer expectations

A PostToken-aware indexer must, in addition to peck-social-v1.md capabilities:

  1. Track UTXO-set per PostToken. Maintain "currently unspent" view keyed by outpoint. For author lookups, secondary index on subject.
  2. Parse state out of locking scripts. Each token has known Layer A/B/C templates; indexer extracts state from data-pushes preceding the contract template.
  3. Detect state transitions. On parsing a TX that spends a PostToken UTXO, match old vs new (or detect burn = no continuation) and log the transition. Recompute canonical ordering.
  4. Validate HandleToken binding at admit-time (§4.1).
  5. Validate content_hash at admit-time (§5.2).
  6. Combine with OP_RETURN parsing. A single TX may have PostToken outputs AND tip OP_RETURN outputs; both parse independently.
  7. Re-verify contract validity in sandbox (optional, paranoia check — BSV nodes already enforce consensus).

Existing 1Sat-ordinals indexers already do (1) and (2); PostToken-aware indexers extend with (3)–(6).


15. Open questions

Carried forward from peck-social-token-v1-DESIGN.md §15 after Q1/Q3/Q4 confirmed and Q2/Q5 reframed. Remaining items:

Defer to v1.1

  • [??] Royalty-split strategy — RoyaltySplitter contract vs multi-output in sell(). To be decided alongside sell() flow in v1.1. Recommended starting point: multi-output for ≤ ~10 payees, splitter for collaborator-heavy CanvasToken.
  • [??] Like-as-payment scaling — atomic sat-per-like vs L2 channel rollup. Open until v1 tip-event volume data exists.

Defer to phase 2 / orthogonal

  • [??] Auction bid() refund — refund-via-output vs refund-bucket- claim. Defer to Wave 4.
  • [??] CanvasToken stroke-commit cost — one TX per stroke vs Merkle-rollup. Defer to Wave 5.
  • [??] Bridge attribution model — bridge as subject vs original- author co-signs via BRC-52 cert. Defer to bridge-design (Wave 6).
  • [??] Cross-app trust-root for app MAP key — anyone can claim app=peck.to. App-registry token-state contract? Open namespace today. Future consideration.
  • [??] BRC-77 vs BSM-compact AIP — new PostToken writes default BRC-77 from day one, or follow ecosystem rollout?

Validated assumptions

  • Three-layer envelope locked (peck-bio v1 production-proven).
  • subject plaintext (no privacy-preserving subject, flagged in peck-bio v1 §13.4).
  • sCrypt now, Rúnar later — toolchain-gated, mechanical port.
  • HandleToken-canonical for subject (§4).
  • Family-wide protocolID = [2, 'peck post-authority'] (§3.3).
  • Burn-and-mint-child edit semantics (§5.5).
  • Both content-storage modes first-class (§5).
  • Royalty mechanisms unbundled to v1.1 (§7).

16. Acknowledgments

Stands on: peck-bio-profile-token-v1.md (three-layer 1Sat envelope + subject ≠ owner pattern); peck-cat-owner-authority.md (derived- owner BRC-100 convention); peck-social-v1.md (three-channel write architecture, MAP-key namespace); peck-social-overlay.md (input- script-data tip pattern); PECK_ECOSYSTEM_VISION.md (cross-app goals, paymail-stance, HandleToken-canonical identity); OVERLAY_ECONOMICS_DISPATCH.md (paywall inversion); shipped contracts ProfileToken.ts, HandleToken.ts, CatToken.ts. Full rationale and trade-off discussion in peck-social-token-v1- DESIGN.md. Errors here are this document's; foundations are Thomas's and contributors'.


Appendix A: Glossary

  • PostToken — Stateful 1Sat UTXO holding social content with Layer A/B/C envelope. Base of the PostToken family.
  • PostToken family — PostToken plus per-app subtypes (CanvasToken, EventToken, ChannelToken, AuctionToken). All share Layer A/B/C spine and tm_peck-social-post topic.
  • subject — Immutable author identityKey. MUST own a canonical HandleToken.
  • owner — Current spend-authority pubkey, derived per-token under [2, 'peck post-authority'] protocolID.
  • content_modeinline (Layer A) or ref (UHRP/external URL).
  • content_hash — SHA-256 of the content payload, always set.
  • parent_outpoint — Outpoint of the parent token, used for both replies and edits.
  • edit — Burn original PostToken + mint child PostToken with kind=edit and parent_outpoint naming the original.
  • canonical PostToken — Latest in the parent-chain from a root, resolved by lowest-block-height + lex-min outpoint.
  • Layer A — Ord-inscription envelope (1Sat-ordinals visibility).
  • Layer B — MAP+AIP scaffolding (Bitcoin Schema indexer visibility).
  • Layer C — sCrypt state-machine bytecode (canonical truth).

Appendix B: Reference design

peck-social-token-v1-DESIGN.md — full design rationale, trade-offs, cross-app subtype matrix, bridge architecture. This v1 spec is the canonical-implementable subset.

DocumentToken-state assertion family — v0.3 draft
Drawing №OS-TKN-03
Rev.C
Date2026-05-09
LicenseMIT