PRE-GEN
Docs Ecosystem / PG code format

PG code — identifier format

Status: frozen 2026-08-20 · Vectors: pg_code, vectors v2 · License: Apache-2.0 · Where prose and vectors disagree, the vectors win.

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 — no position weight here ever is.

Guarantees (verified by exhaustive brute force, not asserted):

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>
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

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:

  1. Longer serials — the pad width is a minimum; nothing breaks at a million subjects.
  2. 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 bare PG-<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:

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.