1 Overview
This NIP defines kind 64 events that carry chess games. The content field holds text in Portable Game Notation (PGN). PGN is the standard interchange format for recorded chess games. Humans can read it. Most chess software can parse it.
The event wrapper adds what PGN alone lacks: an author signature, a timestamp, and relay distribution. A chess client publishes a game once. Any subscribed client can then fetch the game, replay it, and render it on a chessboard.
The NIP defines no new tags and no new messages. It assigns one kind number and points at the existing PGN standard for the payload. Everything else follows the base rules in NIP-01.
One kind-64 event can hold more than one game. PGN defines a database: a sequence of games in one text file. The content string represents exactly such a PGN database.
2 The Kind 64 Event
A chess game event is a normal NIP-01 event with kind set to 64. The content is a string that represents a PGN database. The smallest valid event needs nothing else:
{
"kind": 64,
"content": "1. e4 *"
// other NIP-01 fields: id, pubkey, created_at, tags, sig
}
| Field | Rule |
|---|---|
| kind | Always 64. |
| content | A string that represents a PGN database: one or more games, each with optional tag pairs and a movetext section. |
| tags | No required tags. Clients may add extra tags such as alt from NIP-31. See §3. |
PGN content in brief
A PGN game has two parts. First come optional tag pairs in square brackets, such as [White "Fischer, Robert J."]. The PGN standard names seven of them the Seven Tag Roster: Event, Site, Date, Round, White, Black, Result. Then comes the movetext: numbered moves, optional comments in braces, and a game termination marker. The markers are:
| Marker | Meaning |
|---|---|
| 1-0 | White wins. |
| 0-1 | Black wins. |
| 1/2-1/2 | Draw. |
| * | Result unknown: the game is in progress, abandoned, or otherwise undecided. |
The full grammar lives in the PGN specification and its formal syntax section. This NIP does not restate it. It requires only that content parses as a PGN database.
Kind 64 sits outside every storage-class range that NIP-01 names (regular is 1, 2, 4-44, and 1000-9999). The NIP does not state a storage class, so behavior is by convention: relays treat kind 64 as a regular stored event. Note also that the proposal originally used kind 30, inside the addressable range; the kind changed to 64 in February 2024.
3 Client Behavior
The NIP gives clients three SHOULD rules and one MAY rule:
| Level | Rule |
|---|---|
| SHOULD | Display the content as a chessboard, not as raw text. |
| SHOULD | Publish PGN in export format ("strict mode": machine-generated, canonical layout). Accept incoming PGN in import format ("lax mode": human-written, loose layout). |
| SHOULD | Check that the PGN formatting is valid and that every move complies with the rules of chess. |
| MAY | Add extra tags, for example an alt tag (NIP-31), so non-supporting clients can show a readable summary. |
Do not skip move validation. A signed event proves who published the game. It does not prove the moves are legal. Illegal movetext can crash naive board renderers or show a false position.
4 Relay Behavior
Relays MAY validate the PGN content of a kind-64 event. A relay that validates MAY reject an invalid event. Rejection follows NIP-01: reply ["OK", <event_id>, false, "invalid: ..."]. Relays are not required to understand PGN at all. Clients therefore cannot rely on relays to filter out broken games.
5 Worked Examples
The NIP ships a ladder of valid PGN payloads, from empty to a full database. All of the following are valid content strings.
-- Nothing is known. Game in progress, abandoned, or result -- otherwise unknown. Maybe players died before a move was made. * -- One move, result still open -- 1. e4 * -- Some tag pairs, a comment, no result yet -- [White "Fischer, Robert J."] [Black "Spassky, Boris V."] 1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} *
A complete historical game, with the Seven Tag Roster filled in, wrapped in a full event. The alt tag gives non-chess clients a human-readable line:
{
"kind": 64,
"tags": [
["alt", "Fischer vs. Spassky in Belgrade on 1992-11-04 (F/S Return Match, Round 29)"]
// rest of tags...
],
"content": "[Event \"F/S Return Match\"]\n[Site \"Belgrade, Serbia JUG\"]\n
[Date \"1992.11.04\"]\n[Round \"29\"]\n[White \"Fischer, Robert J.\"]\n
[Black \"Spassky, Boris V.\"]\n[Result \"1/2-1/2\"]\n\n
1. e4 e5 2. Nf3 Nc6 3. Bb5 {This opening is called the Ruy Lopez.} 3... a6\n
4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7\n
11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. Nb1 h6 16. Bh4 c5 17. dxe5\n
Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. Nc4 Nxc4 22. Bxc4 Nb6\n
23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5\n
hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5\n
35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6\n
Nf2 42. g4 Bd3 43. Re6 1/2-1/2"
// other fields...
}
The real content is one long JSON string with \n escapes; the block above wraps lines for print only. The source NIP labels its event examples as YAML code fences, yet shows JSON with // comments. Treat them as illustrative JSON fragments, not literal wire bytes.
Finally, a two-game database in one content string. Extra tag pairs beyond the Seven Tag Roster (Elo ratings, time control, ECO code, termination) are normal PGN and pass through unchanged:
[Event "Hourly HyperBullet Arena"] [Site "https://lichess.org/wxx4GldJ"] [Date "2017.04.01"] [White "T_LUKE"] [Black "decidement"] [Result "1-0"] [UTCDate "2017.04.01"] [UTCTime "11:56:14"] [WhiteElo "2047"] [BlackElo "1984"] [WhiteRatingDiff "+10"] [BlackRatingDiff "-7"] [Variant "Standard"] [TimeControl "30+0"] [ECO "B00"] [Termination "Abandoned"] 1. e4 1-0 [Event "Hourly HyperBullet Arena"] [Site "https://lichess.org/rospUdSk"] [Date "2017.04.01"] [White "Bastel"] [Black "oslochess"] [Result "1-0"] [UTCDate "2017.04.01"] [UTCTime "11:55:56"] [WhiteElo "2212"] [BlackElo "2000"] [WhiteRatingDiff "+6"] [BlackRatingDiff "-4"] [Variant "Standard"] [TimeControl "30+0"] [ECO "A01"] [Termination "Normal"] 1. b3 d5 2. Bb2 c6 3. Nc3 Bf5 4. d4 Nf6 5. e3 Nbd7 6. f4 Bg6 7. Nf3 Bh5 8. Bd3 e6 9. O-O Be7 10. Qe1 O-O 11. Ne5 Bg6 12. Nxg6 hxg6 13. e4 dxe4 14. Nxe4 Nxe4 15. Bxe4 Nf6 16. c4 Bd6 17. Bc2 Qc7 18. f5 Be7 19. fxe6 fxe6 20. Qxe6+ Kh8 21. Qh3+ Kg8 22. Bxg6 Qd7 23. Qe3 Bd6 24. Bf5 Qe7 25. Be6+ Kh8 26. Qh3+ Nh7 27. Bf5 Rf6 28. Qxh7# 1-0
The publish round trip itself is plain NIP-01:
-- publish a game -- → ["EVENT", {"id":"3ac1f2b0…","kind":64,"content":"1. e4 *", …}] ← ["OK", "3ac1f2b0…", true, ""] -- fetch games by an author -- → ["REQ", "chess", {"kinds":[64], "authors":["22a12a12…"]}] ← ["EVENT", "chess", {"id":"3ac1f2b0…", "kind":64, …}] ← ["EOSE", "chess"] -- a validating relay rejects broken PGN -- ← ["OK", "9f2c11ab…", false, "invalid: content is not a valid PGN database"]
Resources
The normative payload grammar lives outside Nostr. The NIP links these references:
- PGN Specification — the Portable Game Notation standard.
- PGN Specification Supplement — additions for graphical elements, clock values, and evaluations.
- PGN Formal Syntax — the grammar a validator implements.
- Seven Tag Roster — the seven standard tag pairs.
- Import Format and Export Format — the lax and strict PGN modes.
- lichess pgn-viewer — an embeddable PGN board widget.
6 Version History
NIP-64 arrived late in 2023 and settled quickly. Notable changes, newest first:
- 20262026-06Example code-fence labels switched from
jsonctoyamlrepo-wide. - 2024-252024-2025Formatting cleanups: JSON style, header style, and cross-reference link fixes.
- 20242024-03"Chess" added to the NIP title.
- 20242024-02Kind changed from 30 to 64. FEN position tags removed; the spec narrowed to pure PGN content. Examples moved to the bottom.
- Origin2023-12theborakompanioni proposed Portable Game Notation events.
Original Proposal - Credits
| Author | theborakompanioni |
| First commit | 2023-12-28 — “docs(NIP-64): Portable Game Notation” |
| Status | draft - optional |
| Source | 64.md on GitHub - full change history |