# 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`](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 `@method`s 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`:

```python
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:
   - `kind = edit`
   - `parentOutpoint = <original-outpoint>`
   - `MAP parent_outpoint=<original-outpoint>` (also in Layer B for
     indexer discovery)
   - 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:

```ts
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.

```sql
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_mode** — `inline` (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.
