PlayStation 1 Memory Cards
PlayStation 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", "<card-id>"]
["d", "<card-id>-<block>"]<card-id>— the card id; the exact value of themtag.<block>— the block index, a decimal integer0–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 -<digits>. Block 0 is the card header/directory; blocks 1–15 hold saves.
Tags
| Tag | Role | Required |
|---|---|---|
d | Addressable identifier, <card-id>-<block>. | 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":["<pubkey>"],"#m":["<card-id>"]} 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:
- Fetch
{"kinds":[38192],"authors":["<pubkey>"],"#m":["<card-id>"]}. - 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.
- For each event, take the block index from the
blocktag (or the tail of
d), decode the hex content, and copy the 8192 bytes to offset block × 8192.
- If an
xtag 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:
{
"kind": 38192,
"content": "4d43000000000000...<16384 hex chars total>...",
"tags": [
["d", "main-0"],
["m", "main"],
["block", "0"],
["state", "header"],
["x", "9f2c...<sha256>"],
["alt", "PS1 memory card 'main' — header/directory block"]
]
}Block 1 — a single-block save:
{
"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...<sha256>"],
["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.