PRE-GEN

PG code — identifier format

Status: frozen 2026-08-20, issuer prefix added 2026-09-28 · Vectors: pg_code, vectors v2 · License: Apache-2.0 · The text is normative; a vector that disagrees with it is an erratum.

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

ShapeExampleMeaning
PG-<serial><check>PG-000042*A subject’s permanent registry number
PG-<CLASS>-<serial>-<tail><check>PG-STD-000001-K7M2QX9A 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)

ShapeEra
PG-000042*current (2026-08-19 onward)
PG-STD-0001232026-08 → 2026-08-19 (shared the STD license counter)
PG-SUB-000123pre-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:

icharvalueweightproduct
00010
10020
20030
30040
444520
522612

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. Weights 1 to 36 never are; weight 37 is.

So a body is at most 36 characters; a longer one is invalid. Every code issued so far is far shorter (the longest today is 19).

Guarantees (verified by exhaustive brute force, not asserted), for bodies of at most 36 characters:

  • 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

CodeBody fed to the algorithm
PG-000042*000042
PG-STD-000001-K7M2QX9STD000001K7M2QX

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.

SerialCode
1PG-0000016
42PG-000042*
99999PG-099999*
999999PG-9999994
1000000PG-10000001
12345678PG-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.
ClassSerialTailLicense id
STD1K7M2QXPG-STD-000001-K7M2QX9
RND42ZZZZZZPG-RND-000042-ZZZZZZ1
PRM999999000000PG-PRM-999999-0000000
Retired forever A class code that has ever appeared in an issued license id is never re-admitted and never given a different meaning. Currently retired: 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* and PG-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 · Issuers: more than one registry

Any company may run a PRE-GEN registry. So that two registries never issue the same code, every registry other than the origin one puts its issuer prefix in every code it issues.

ShapeExampleMeaning
PG-<ISSUER>-<serial><check>PG-NWRD-000042=A subject in registry NWRD
PG-<ISSUER>-<CLASS>-<serial>-<tail><check>PG-NWRD-RND-000042-ZZZZZZJA licence issued by registry NWRD
  1. Exactly four letters from the PG alphabet, ABCDEFGHJKMNPQRSTVWXYZ — no digits, no I, L, O, U. Four, never three, so a prefix can't be mistaken for a licence class or a legacy PG-STD- code. 234 256 possible prefixes.
  2. The prefix is checked. The check character covers ISSUER + serial (subject) or ISSUER + CLASS + serial + tail (licence), so a mistyped prefix is reported as mistyped.
  3. Bare codes mean the origin registry, forever. Every code issued without a prefix keeps its meaning.
  4. Prefixes are assigned in public, through the registry list, and never reassigned.
  5. The issuer is the one thing software may read from a code — to know which registry to ask. Everything else is resolved, never parsed.

A registry that receives a code with a prefix it doesn't hold answers that the code belongs to another registry, not "not found". Longer serials stay free as well: the pad width is a minimum.

The signed registry directory

A prefix by itself protects nothing: anyone can print one. What decides which codes software accepts is the directory at registries.json, signed by the PRE-GEN steward key (PG-CODE.md §9.1). It lists each registry with its namespace (bare for the origin registry, four letters for every other), its API and key-set addresses (HTTPS only) and the fingerprints of every operator key it may sign with. A code is valid only when a key listed for its namespace signed it — the same rule for every registry, the origin one included.

  • A client checks the signature with a steward key it already holds, never with a key taken from the directory, and refuses a directory older than one it has seen, one that removes a registry or key or undoes a revocation, or a different one with the same sequence.
  • A registry announces its next operator key in the directory before using it, so a planned rotation changes nothing for clients.
  • The steward lists a registry only after admission conditions, and can revoke a registry (revoked in its entry) or a single stolen key (revoked_keys). Nothing signed by a revoked registry or key authorizes a generation again (V-14); a revocation is never undone.
  • A listing proves which registry may issue a code, not that it holds any right over a person or a work.
  • v5 does not define how fresh a directory must be, so a verifier learns of a revocation only when it fetches the directory again.

Steward keys

KeyFingerprintPublic key
Stewardpg-ed25519:dcdd244659e6d0e2426f2b504cb115905cb949aab04186e3e216ec541b847c912fc3f78138c0ec3cb2560b2dad0d1f1b
Successor (unused until a handover)pg-ed25519:3e5e20729ce8485eea274c4cb33721d6658544163d6abd5fef046407980d249817875d87d8cf4b67879a2e265fa5612f

Both keys are held offline by the editor of the standard, not by any registry. The same values are in the specification, the v5 paper, the repository README and the DNS TXT record _pregen-steward.pregen.org. A directory signed by the successor replaces those of the steward key whatever their sequence. @pregen/verify and pregen have both keys built in.

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.