One drawn glyph is one byte.
Draw the first bytes of a nostr event id or pubkey on any surface with any pen. The alphabet has sixteen glyphs; each one sits in a square or a circle, outline or filled, in one of four rotations, so every byte is exactly one drawing and every drawing is one byte. A phone reads the row back on the device, and a relay resolves the bytes to the event or the profile they name.
How it works
-
Draw
Take the first six bytes of an id or a pubkey. Each byte names one glyph, a rotation, outline or filled, and a square or a circle frame. Draw them left to right, each in its own frame, underline the whole row, and put a fat dot at the start of the line.
e8ed3798c6ffas the reference renderer draws it, one byte per frame:e8trapezoid rotated 180 deg, outline, square frameedtrapezoid rotated 270 deg, outline, circle frame37crown rotated 90 deg, filled, circle frame98heart rotated 180 deg, outline, square framec6snowman rotated 90 deg, filled, square frameffpacman rotated 270 deg, filled, circle frame
-
Read
Open the reader on a phone and take a photo. It finds the frames, the underline and the dot, straightens each frame, and classifies the glyph, its rotation and its fill with a small neural network, all in JavaScript on the device. The underline tells it which way is up, so a photo taken sideways reads the same.
Where a glyph is unclear the reader keeps the alternatives instead of guessing, and returns the most probable whole readings, ranked. A missing glyph is never dropped silently, because that would shift every byte after it.
a marker on a notebook page: read as e8ed3798c6ffon a phone in under a second -
Look up
The bytes are a prefix. The reader asks a relay for every candidate reading at once, as an event id prefix and as a pubkey prefix, and shows what comes back with the author's profile. You confirm which one you meant. A wrong candidate matches nothing, which is why there is no checksum.
How well does it work
On the synthetic benchmark in BENCH.md (200 scenes, 996 symbols, random handwriting severity, perspective, lighting, blur and JPEG on photographed backdrops) the reference reader finds 0.862 of the frames, reads 0.922 of the symbol bytes correctly where the frame count matched, and gets the whole row right as its first answer 0.535 of the time. Missing frames, not misread glyphs, are the main loss. The classifier alone scores 98.6% on held-out patches, 97.6% for the smaller model that ships in the reader. Real handwriting has one measurement so far: the notebook photo above, which the reader returns exactly.
The alphabet
| bits | field | values |
|---|---|---|
| 7..4 | symbol | index 0 to 15, the row on the sheet |
| 3..2 | rotation | quarter turns clockwise from upright: 0, 90, 180, 270 degrees |
| 1 | fill | 0 outline, 1 filled |
| 0 | frame | 0 square, 1 circle |
byte = symbol << 4 | rotation << 2 | fill << 1 | frame, so 16 x 4 x 2 x 2 = 256. All 256 drawings are listed in the spec's test vectors.
Draw a row
- Frame first. Draw a square or a circle. Keep the frames about the same size along the row, with a gap of roughly a third of a frame between them.
- Glyph inside. Draw it in the middle of the frame at about six tenths of the frame's width, without touching the frame, rotated as the byte requires.
- Filled means scribbled solid. Fill the whole inside so it reads as a blob. An outline is one clean stroke. A filled glyph keeps its silhouette.
- Keep points pointy. The house's roof, the crown's points and the mountain's peaks are what separate them from a square or a rectangle; make points at least a third of the glyph tall.
- Snowman: the head must be clearly smaller than the body, about half its diameter, or it reads as a pac-man.
- Underline the row with one stroke a little below the frames, extending past the first and last frame. The line tells the reader which way is up.
- Fat start dot: a solid dot at the start end of the underline, about a quarter of a frame across. The dot tells the reader where to begin; without it the row has two readings, forward and reversed.
- Six bytes is the recommended length for an event id or pubkey prefix; eight for something meant to stay unambiguous for years; four only where the relay is small.
Look it up
The reader sends every candidate reading in one request, as an ids prefix and as an authors prefix for kind 0 profiles:
["REQ", "gb", {"ids": ["e8ed3798c6ff"]}, {"authors": ["e8ed3798c6ff"], "kinds": [0]}]
Prefix matching in ids and authors was part of the first NIP-01 and was later removed from the text, so it needs a relay that still supports it. As of 2026-09 grain honours prefixes of any length in both fields; strfry and khatru currently do not (strfry answers CLOSED, khatru returns nothing). The reader defaults to wss://wheat.oslim.dev, a grain relay, and tests whichever relay you enter once, by fetching one event and asking for it again by an 8-character prefix, then tells you when a relay rejects or ignores partial queries.
Results show the author's profile and which candidate they matched, and the person confirms which one they meant. A prefix is a pointer, not a proof: the resolved full id or pubkey is the reference, never the drawn bytes.
Get it
- The specification, with the 256 test vectors, the glyph polygons and the printable sheet.
- github.com/arkin0x/glyphbyte: the Python reference implementation, the JavaScript reader and this site.
- JavaScript:
glyphbyteon npm, dependency-free.npm install glyphbytegives you the reader, the row renderer and the relay lookup. - Python:
pip install git+https://github.com/arkin0x/glyphbytefor now (numpy, OpenCV, onnxruntime).glyphbyte decode photo.jpgprints the bytes;glyphbyte encode HEX --out row.pngrenders a row to copy from.
The whole reader runs in the browser: the detector, the network weights and the glyph shapes are inline in one page, and nothing leaves the device except the relay queries you can see on the page. The same file is also a napplet (NIP-5D) that a nostr shell can load in a sandboxed iframe.