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
- Subject ≠ owner.
subjectis the immutable author identityKey (peck-bio HandleToken-derived).owneris the current spender (initially equal tosubject, but transferable). This separation enables transferable posts, paid-content owner-rotation, and custodial-recovery without breaking attribution. - HandleToken-canonical identity. A PostToken's
subjectMUST be theownerof a canonical HandleToken (perls_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). - 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. - 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.
appMAP key + Layer C subtype contract differentiates. - Edit = burn + mint child.
contentRefis immutable. Edits produce a new PostToken namingparent_outpoint. The original is frozen — required for marketplace integrity (buyer must verify the content they saw in the listing). - 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,
versionmonotonic, 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: noroyaltyBpsin v1 — see §7.)transfer(newOwner, ownerSig, trailing)— pure owner rotation, no value movement. Same shape as shippedHandleToken.transfer.reply(replyOutputRaw, ownerSig, trailing)— only enforced whenflags.locked-repliesis set; gates threads. Open replies are fresh mints namingparentOutpoint, 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:
- User initiates mint in peck.to / peck.ink / etc.
- Wallet queries
ls_peck-bio-handle.byOwner(<identityKey>). - If no handle: surface "mint a @handle first" CTA, link to peck.bio. Mint flow blocks.
- 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:
- Extract
@<handle>patterns from content - Lookup each via
ls_peck-bio-handle.byHandle - 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_hashis 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_urlis set inrefmode. Neither ininlinemode. - For
inlinemode, the content payload is the UTF-8-decoded bytes of Layer A's JSONcontentfield (media_type=text/plain) or the raw bytes (media_type=application/octet-streamstyle).
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 oninlinedue to fee inflation) - Binary (image, audio, video, anything non-UTF-8) →
refonly;inlinerejected 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:
- Burn: spend the original UTXO via
burn(ownerSig)— no continuation. - Mint child: in the same TX (or a follow-on), mint a fresh PostToken with:
kind = editparentOutpoint = <original-outpoint>MAP parent_outpoint=<original-outpoint>(also in Layer B for indexer discovery)- same
subject, newcontentRef/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
priceSatsto 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
subjectcut) — 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:
- Content (UHRP) AES-encrypted with key K at mint time.
- Layer C
priceSats > 0declares the per-read price. - Reader pays
priceSatstoP2PKH(owner)(one-shot) or via BRC-104 channel (streaming). - Overlay observes the payment, derives buyer's access-key (BRC-42
with
owneras 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:
outputAdmittedByTopicparses 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 withupdate,transfer,burn,reply@methods. Nosell(), nobid()/settle(), noroyaltyBpsfield.- 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_tokenstable — 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/postGET /v1/post/:outpointGET /v1/post/:outpoint/historyGET /v1/post/:outpoint/threadGET /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)
PeckSocialTipTopicManagerfortm_peck-social-tip.tip_eventstable — flat, no state-machine.GET /v1/post/:outpoint/tipsaggregate endpoint.
12.6 NOT in Wave 1
sell()method and listing-discovery — Wave 3.AuctionToken— Wave 4.- CanvasToken / EventToken / ChannelToken — Wave 5.
royaltyBpsfield 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:
- Track UTXO-set per PostToken. Maintain "currently unspent"
view keyed by outpoint. For author lookups, secondary index on
subject. - 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.
- 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.
- Validate HandleToken binding at admit-time (§4.1).
- Validate content_hash at admit-time (§5.2).
- Combine with OP_RETURN parsing. A single TX may have PostToken outputs AND tip OP_RETURN outputs; both parse independently.
- 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 —
RoyaltySplittercontract vs multi-output insell(). To be decided alongsidesell()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
subjectvs original- author co-signs via BRC-52 cert. Defer to bridge-design (Wave 6). - [??] Cross-app trust-root for
appMAP key — anyone can claimapp=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).
subjectplaintext (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-posttopic. - subject — Immutable author identityKey. MUST own a canonical HandleToken.
- owner — Current spend-authority pubkey, derived per-token under
[2, 'peck post-authority']protocolID. - content_mode —
inline(Layer A) orref(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=editandparent_outpointnaming 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.