An application generates an actual memory card file on your hard drive (same mcd format used by every emulator) from your nostr memory card, and there's a service watching for changes to that file. When any changes to the file happen, they are signed with your nsec and broadcasted to the configured relay.
NIP For the file format:
quoting naddr1qv…3pskPlayStation 1 Memory Cards
--------------------------
`draft` `optional`
This NIP describes a method by which Sony PlayStation (PS1) memory cards can be
stored, synchronized, versioned and shared over Nostr. A card is split into its
native 8 KB blocks, and each block is published as one addressable event, so a
card can be reconstructed, updated block-by-block, and rendered by clients
without parsing the binary.
## Motivation
A PS1 memory card is a small, fixed-shape binary artifact: 128 KB, divided into
16 blocks of 8192 bytes. Block 0 holds the header and directory; blocks 1–15
each hold one save. Emulators read and write the card as a flat file.
Publishing at block granularity means an in-game save touches only the events
for the blocks it occupies, events stay small, and updates are local. Using an
addressable event per block gives free "latest wins" versioning: re-publishing a
block after a save overwrites the previous one, and consumers converge on the
newest state.
## Block events
A block is published as an _addressable_ event (as defined in
[NIP-01](01.md)) of kind `38192`. A relay retains only the most recent event
for each `(kind, pubkey, d)` tuple, so a block is updated in place by
re-publishing it.
The `content` field is exactly one block: **16384 lowercase hexadecimal
characters** encoding the block's 8192 bytes, with no `0x` prefix, whitespace or
separators. It is the raw block image, byte-for-byte as it appears on the card
at offset `block × 8192`.
### Cards and the `d` tag
Every card has a **card id**: a stable, author-chosen identifier (e.g. `main`,
`zelda-run`) scoped to the author's pubkey. A single pubkey MAY own several
cards, distinguished by card id.
The card id is carried verbatim in the indexable `m` tag, and the `d` tag is the
card id joined to the block index by a hyphen:
```
["m", ""]
["d", "- "]
```
- `` — the card id; the exact value of the `m` tag.
- `` — the block index, a decimal integer `0`–`15`.
The block index is the substring after the **last** `-`; the card id is
everything before it. A card id therefore MUST match `^[^ ]+$` and MUST NOT end
with `-`. Block `0` is the card header/directory; blocks `1`–`15` hold
saves.
### Tags
| Tag | Role | Required |
| ---------- | --------------------------------------------------------------------------------------------- | ------------------------------- |
| `d` | Addressable identifier, `- `. | Yes |
| `m` | Card id, indexable so a whole card is fetched with `#m`. | Yes |
| `block` | Block index, decimal `0`–`15`. | Yes |
| `x` | SHA-256 (hex) of the block's 8192 raw bytes, for integrity. | Recommended |
| `state` | Block allocation state: `header`, `first`, `middle`, `last` or `free`. | Recommended |
| `alt` | Human-readable summary of the block, per [NIP-31](31.md). | Recommended |
| `name` | Human-friendly display name for the card. | No |
| `title` | Save title shown by the BIOS, decoded to UTF-8 (see below). | No |
| `filename` | Directory filename / product code of the save (e.g. `BASCUS-00001SOFTCARD`). | No |
| `region` | `America`, `Europe` or `Japan`. | No |
| `icon` | Base64-encoded rendering of the save's 16×16 icon, for previews. | No |
`m` and `block` duplicate information present in `d`, but let relays and clients
filter and index without parsing the `d` string. Because relays index
single-letter tags, `{"kinds":[38192],"authors":[""],"#m":[" "]}`
returns exactly one card.
`state`, `title`, `filename` and `region` are all derivable from the block
bytes; they are mirrored into tags so clients can list saves without decoding
the binary. Publishers SHOULD set them for blocks whose directory entry is known
(typically by reading block 0). Consumers MUST treat the binary `content` as
authoritative and these tags as advisory.
`title` is stored in the card's SC header in Shift-JIS. Publishers SHOULD decode
it to UTF-8. A title composed entirely of full-width Latin characters (a common
way games render English, e.g. `SPYRO THE DRAGON`) SHOULD be folded to
half-width (`SPYRO THE DRAGON`); a title containing kana or kanji SHOULD be left
as decoded.
`region` MAY be derived from the second character of `filename` (`I`→Japan,
`A`→America, `E`→Europe) or from the product-code prefix (`SLUS`/`SCUS`→America,
`SLES`/`SCES`→Europe, `SLPS`/`SLPM`/`SCPS`→Japan).
## Reconstructing a card
To rebuild the 131072-byte card image for a given author and card id:
1. Fetch `{"kinds":[38192],"authors":[""],"#m":[" "]}`.
2. Start from a 131072-byte buffer that is a freshly *formatted* card (valid
header and free directory), so that unpublished blocks read as empty rather
than as zeroes.
3. For each event, take the block index from the `block` tag (or the tail of
`d`), decode the hex `content`, and copy the 8192 bytes to offset
`block × 8192`.
4. If an `x` tag is present, verify the SHA-256 of the decoded bytes and reject
the block on mismatch.
Publishers SHOULD publish block `0` (its directory is what makes the card valid)
along with every in-use block. Free blocks MAY be omitted.
A save spanning several blocks marks its parts with `state` (`first`, then
`middle`, then `last`); the block chain itself is recorded in the binary
directory in block 0, which is authoritative.
## Synchronization
Because block events are addressable, the current state of a block is simply its
latest event. A host that owns the card file can watch for external writes (an
emulator saving a game) and re-publish each changed block, and a peer can
subscribe to the same filter and overlay incoming blocks onto its local card.
Conflicts resolve per block by `created_at` under the relay's replaceable-event
rules — last writer wins — which suits a single player moving a card between
machines rather than simultaneous play.
## Examples
Block 0 — the card header/directory:
```jsonc
{
"kind": 38192,
"content": "4d43000000000000...<16384 hex chars total>...",
"tags": [
["d", "main-0"],
["m", "main"],
["block", "0"],
["state", "header"],
["x", "9f2c..."],
["alt", "PS1 memory card 'main' — header/directory block"]
]
}
```
Block 1 — a single-block save:
```jsonc
{
"kind": 38192,
"content": "5343110153...<16384 hex chars total>...",
"tags": [
["d", "main-1"],
["m", "main"],
["block", "1"],
["state", "first"],
["title", "Software Defined Card"],
["filename", "BASCUS-00001SOFTCARD"],
["region", "America"],
["x", "3ab8..."],
["alt", "PS1 save 'Software Defined Card' (BASCUS-00001SOFTCARD)"]
]
}
```
## Security considerations
Save data is arbitrary game state; publishing it to relays makes it public. This
NIP does not define encryption — memory cards are treated as public content. A
publisher wanting privacy SHOULD keep those blocks off public relays and use a
general-purpose mechanism such as a [NIP-59](59.md) gift wrap, rather than
encrypting the block event itself.
The `x` tag lets consumers detect corruption or truncation of the hex payload
before feeding bytes to an emulator.
Cards are scoped per pubkey; a card is only as trustworthy as the key that
signed its blocks. Consumers MUST NOT merge blocks from different authors into a
single card.
