PG code — identifier format
This document specifies the PRE-GEN identifier format completely enough to implement from scratch, without reading any registry source code. A PG code is assigned once and cited forever — in a license, a dispute, a court filing — which is why the format is frozen: changing how the check character is computed would invalidate every code already issued, with no way to tell an old-but-valid code from a corrupted one.
1 · Two shapes, and only two
| Shape | Example | Meaning |
|---|---|---|
| PG-<serial><check> | PG-000042* | A subject’s permanent registry number |
| PG-<CLASS>-<serial>-<tail><check> | PG-STD-000001-K7M2QX9 | A license identifier |
A subject code carries no class letters — a subject has no license class; its licenses
do. A bare PG- followed by digits is always a subject; PG-
followed by letters is always a license.
Historical subject shapes (permanently valid)
| Shape | Era |
|---|---|
| PG-000042* | current (2026-08-19 onward) |
| PG-STD-000123 | 2026-08 → 2026-08-19 (shared the STD license counter) |
| PG-SUB-000123 | pre-2026-08 (dedicated SUB counter, retired) |
The two legacy shapes have no check character — all digits after the prefix. An implementation must recognize them for resolution and must not attempt to validate a check character on them.
2 · Alphabet
0123456789ABCDEFGHJKMNPQRSTVWXYZ
32 symbols: Crockford base32 — digits plus uppercase Latin letters
excluding I, L, O, U
(I/L misread as 1, O as 0;
U excluded by Crockford’s convention). A character’s value is its zero-based
index in that string.
Check symbols
* ~ $ = !
Five symbols, valid only in the check position, never inside a body. They exist because the modulus is 37 while the alphabet holds 32 — the five overflow values need somewhere to go (mirroring Crockford’s optional check-symbol convention). All five are URL-path-safe unencoded (RFC 3986). The full check alphabet, indexed 0–36:
0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=!
3 · Check character
Given a body (alphabet characters, dashes removed, no check character):
sum = Σ value(body[i]) × (i + 1) for i = 0 … len(body)−1
check = CHECK_ALPHABET[ sum mod 37 ]
Positions are 1-based weighted left to right. Worked example — body 000042:
| i | char | value | weight | product |
|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 0 |
| 1 | 0 | 0 | 2 | 0 |
| 2 | 0 | 0 | 3 | 0 |
| 3 | 0 | 0 | 4 | 0 |
| 4 | 4 | 4 | 5 | 20 |
| 5 | 2 | 2 | 6 | 12 |
sum = 32, 32 mod 37 = 32, CHECK_ALPHABET[32] = '*' → PG-000042*.
Why 37 and not 32
32 is composite (2⁵): a weighted sum modulo a composite has zero divisors. Brute-forcing
a naive mod-32 variant over every single-character substitution in a 13-symbol body left
roughly 4% of substitutions undetected — collisions by construction, not
bad luck. 37 is the next prime at or above the alphabet size: the difference any
substitution makes is weight × delta mod 37, which can only be 0 if the
weight itself is 0 mod 37 — no position weight here ever is.
Guarantees (verified by exhaustive brute force, not asserted):
- every single-character substitution changes the check character;
- every adjacent transposition of two different characters changes it.
Transposing two identical characters changes nothing — correctly, since it changes no code.
What the check covers
| Code | Body fed to the algorithm |
|---|---|
| PG-000042* | 000042 |
| PG-STD-000001-K7M2QX9 | STD000001K7M2QX |
Class letters are part of the body — a wrong class is as much “a wrong code” as a wrong
serial. The literal PG- prefix is not: it is a fixed marker, identical in every code.
4 · Subject codes
PG-<serial, zero-padded to at least 6 digits><check>
The serial comes from a registry-wide counter, padded to a minimum of 6
digits — six is not a maximum: serial 1 234 567 renders as 1234567 and stays
valid. Implementations must not assume a fixed length.
| Serial | Code |
|---|---|
| 1 | PG-0000016 |
| 42 | PG-000042* |
| 99999 | PG-099999* |
| 999999 | PG-9999994 |
| 1000000 | PG-10000001 |
| 12345678 | PG-12345678K |
5 · License identifiers
PG-<CLASS>-<subject serial, ≥6 digits>-<tail><check>
- CLASS — three letters. Currently
STD,RND,PRM. - subject serial — the serial of the subject the license is for, so an id names its subject without a lookup.
- tail — 6 random alphabet characters from a cryptographically secure source. Random, not sequential, so the number of licenses on one subject cannot be inferred from an id.
| Class | Serial | Tail | License id |
|---|---|---|---|
| STD | 1 | K7M2QX | PG-STD-000001-K7M2QX9 |
| RND | 42 | ZZZZZZ | PG-RND-000042-ZZZZZZ1 |
| PRM | 999999 | 000000 | PG-PRM-999999-0000000 |
EDT, EST,
ENT, PER, OWN. A code whose meaning depends on when
it was issued defeats the point of a permanent citation.
6 · Resolution rules
- Case-insensitive.
pg-000042*andPG-000042*are the same code; canonical form is uppercase. People retype these from screenshots. - Whitespace is stripped before matching.
- A code whose check character fails must be reported as mistyped, distinctly from not found — the two mean different things to whoever holds the code.
7 · The load-bearing rule: resolved, never parsed
An identifier is resolved by database lookup. It is never parsed to extract
meaning. The serial inside a license id exists so a human can see which subject
it belongs to — not so software can split('-') and skip the lookup. Anything
derived by parsing becomes wrong, silently, the moment the format widens (a 7-digit
serial, a future issuer prefix). The reference implementation enforces this on itself
with a repository-wide guard test that fails the build if any code path starts slicing an id.
8 · Room left for the future
Two extensions are possible without invalidating any issued code:
- Longer serials — the pad width is a minimum; nothing breaks at a million subjects.
- A second issuer — if PRE-GEN is ever operated by more than one
registry,
PG-<ISSUER>-<serial><check>can be defined for other issuers while a barePG-<serial><check>permanently means the origin registry. No collision with the legacy shapes: those are all-digits and carry no check character.
Neither is implemented today; both are recorded so a future implementer sees the format was designed with the room.
9 · Conformance
An implementation conforms if it reproduces every case in the pg_code
section of the test vectors:
check_char— all 37 possible outputs, including all five overflow symbols;subject_code— serials spanning the 6-digit boundary in both directions;license_id— every current class, plus a serial past the pad width;rejects— corrupted codes that must fail verification.
Vectors live in the SDK repository:
spec/test-vectors/vectors.json.
Regenerate only after an intentional format change — the generator is deterministic, so
a noisy diff means the format moved.
