# PeerAssets Media v1

**Status:** draft proposal for the Peercoin community, November 2026. In use by Peeragon (peeragon.com).

PeerAssets Media (PAM) stores an NFT's media and description **on the Peercoin blockchain**. An NFT built this way depends on nothing except Peercoin: no web server, no IPFS. It builds on PeerAssets v1, Peercoin's token protocol (github.com/PeerAssets, reference implementation pypeerassets 0.4.8). PAM adds no new rules to PeerAssets: a PAM NFT is an ordinary PeerAssets deck and card, and PeerAssets software that knows nothing about PAM still sees a valid deck.

Key words MUST, SHOULD and MAY are used as in RFC 2119.

## 1. Overview

A PAM NFT is a PeerAssets **SINGLET** deck (`issue_mode` 10, ONCE | MONO, `number_of_decimals` 0) whose `asset_specific_data` commits to a **payload**: the media type, a description and the media bytes. The payload is cut into **pieces**, one per transaction, written **before** the deck spawn in one or more chains of transactions. The deck spawn spends the end of every chain, so a reader starting from the deck spawn txid finds every piece by walking backwards through transaction inputs. That needs only "get transaction by id", which any node with `txindex=1` and any block explorer provides.

```
 funding ─▶ piece 0 ─▶ piece 1 ─▶ … ─▶ piece k ──┐
 funding ─▶ piece k+1 ─▶ …        ─▶ piece n-1 ──┼─▶ deck spawn ─▶ card ─▶ holder
            (one chain per input of the deck)    ┘
```

## 2. Payload

| Field | Size | Content |
|---|---|---|
| type length | 1 byte | length of the next field |
| media type | type length | ASCII MIME type, e.g. `image/png` |
| description length | 2 bytes, big-endian | length of the next field |
| description | description length | UTF-8 text, MAY be empty |
| media | the rest | the file's bytes |

## 3. Pieces

The payload is split into consecutive slices of at most 247 bytes. Slice *i* of *n* is carried by one transaction whose **first output (vout[0])** is an `OP_RETURN` with exactly this data:

| Field | Size | Content |
|---|---|---|
| magic | 3 bytes | ASCII `PAM` (`50 41 4d`) |
| version | 1 byte | `01` |
| index | 2 bytes, big-endian | *i*, from 0 |
| count | 2 bytes, big-endian | *n*, the same in every piece |
| data | ≤ 247 bytes | slice *i* |

The OP_RETURN data is at most 255 bytes, pushed with `OP_PUSHDATA1`: a 258-byte script, within Peercoin's default relay limit (`-datacarriersize=259`). A 256-byte push would need `OP_PUSHDATA2` and a 260-byte script, which nodes refuse. A transaction MUST NOT carry more than one piece. *n* ≤ 65,535.

**Chains.** Pieces are grouped into one or more chains of consecutive indices. Within a chain, the transaction carrying piece *i* MUST spend, as its **vin[0]**, an output of the transaction carrying piece *i − 1*. The first piece of a chain spends any other output (a coin of the issuer, or a "fan-out" transaction that funds several chains).

*Why chains:* Peercoin nodes keep at most 25 unconfirmed transactions in a chain, so a writer moves about 20 pieces per chain per block; big media (Peeragon allows 1 MiB, about 4,250 pieces) uses up to 40 chains side by side.

## 4. The deck spawn

A standard PeerAssets v1 deck spawn (vout[0] pays the network's deck-spawn tag address, vout[1] is the `DeckSpawn` protobuf), with:

- `version` = 1, `issue_mode` = 10 (SINGLET), `number_of_decimals` = 0, `name` = the NFT's name;
- `asset_specific_data` = the ASCII string `pam:1:sha256:` followed by the lowercase hex SHA-256 of the whole payload (77 bytes).

Its inputs MUST be the ends of the piece chains, each input spending the last transaction of one chain. As in PeerAssets, the deck's **issuer** is the address of the output spent by vin[0]. Writers SHOULD therefore send each piece's change to the issuer's address.

Deck-spawn tag addresses (pypeerassets `pa_constants.py`):

| Network | Address |
|---|---|
| mainnet | `PAprodbYvZqf4vjhef49aThB9rSZRxXsM6` |
| testnet | `miHhMLaMWubq4Wx6SdTEqZcUHEGp8RKMZt` |

## 5. The card and ownership

Ownership follows PeerAssets v1 unchanged. The deck's **tag address** is the P2PKH address of the compressed public key whose private key is the 32-byte deck spawn txid (as displayed, big-endian). Every card transaction pays the tag address in vout[0], carries a `CardTransfer` protobuf in vout[1] and names its receiver in vout[2]. The sender is the address of the output spent by vin[0].

For a SINGLET deck: the issuer's first card issuance (sender = issuer, receiver ≠ issuer, amount 1) creates the one card; later issuances are ignored. A card transfer counts only when its sender currently holds the card; a card sent back to the issuer is burnt. Receivers SHOULD be P2PKH addresses, which all PeerAssets software reads.

## 6. Reading

1. Get the deck spawn. Check vout[0] pays the deck-spawn tag address and decode vout[1]; check the deck is SINGLET and `asset_specific_data` starts with `pam:1:sha256:`.
2. For each input of the deck spawn, get the transaction it spends. While that transaction's vout[0] is a PAM piece with the same *n* and the index expected next (one less than the last), keep it and move to the transaction spent by its vin[0].
3. Every index 0 … *n*−1 MUST be found exactly once. Join the slices in index order.
4. The SHA-256 of the joined payload MUST equal the hash in `asset_specific_data`; otherwise the NFT MUST be shown as invalid.
5. Decode the payload (section 2).
6. For the holder, list the transactions paying the deck's tag address (an explorer's address history, a node wallet watching the tag address, or a scan of the blocks from the deck's onwards for transactions whose vout[0] pays it) and apply section 5 in block order.

To list every PAM NFT on the network, list the transactions paying the deck-spawn tag address and keep the SINGLET decks whose asset data starts with `pam:1:sha256:`.

## 7. Reference implementation

- `peerassets-media.mjs`: a dependency-free reader for browsers and Node 19+ (`readNft`, `holderOf`, `deckTagAddress`, `readCard`), MIT licence. https://peeragon.com/pam/peerassets-media.mjs
- `read-nft.mjs`: a command-line reader for a local Peercoin node. https://peeragon.com/pam/read-nft.mjs

## 8. Notes

- Cost: each piece is one small transaction (about 460 bytes, 0.0046 PPC at the 0.01 PPC/kB minimum fee), so media costs about 19 PPC per MiB in fees.
- Pieces are never spendable data outputs: they add nothing to the UTXO set.
- Any media type is allowed; viewers SHOULD only render types they can display safely (Peeragon accepts PNG, JPEG, GIF and WebP) and SHOULD NOT render SVG or HTML from untrusted NFTs inline.
