English | 한국어 · Docs: https://algorix-hq.github.io/dealcode/
Collision-free, random-looking codes from a counter — like dealing cards from a shuffled deck. Every card comes out exactly once; the order looks random; the dealer only remembers how many cards have been dealt.
Anyone who has shipped short public codes — an airline-style booking
reference, an order number, a cus_xxxxxx shortcode — knows the trap:
- Random? The birthday problem bites absurdly early: draw random 6-digit codes and the first duplicate is expected around code #1,200 — in a space of a million. From then on, every insert carries a uniqueness check and a retry loop.
- UUID? Never collides, but 36 characters — not something you print on a boarding pass.
- nanoid? Shorter, yet still long — it has to be, because it is random. Shrink it and the birthday problem comes straight back.
- A raw sequence? Short and collision-free — and it broadcasts exactly how many orders you have.
dealcode is the missing option: keep the sequence your database already produces, and it packs the code space full — every code dealt exactly once, no repeats until all 1,000,000 codes (then all 10,000,000, …) are actually used — while the order stays cryptographically unpredictable from outside. All you need is a counter. (Full argument and alternatives table: Why dealcode exists.)
counter: 0 1 2 3 ... 16,777,216
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
code: d568e1 f7f229 0f868d f37ff8 ... 7b11743 ← grew to 7 chars
only when 6 ran out
Give dealcode a never-repeating integer (a database sequence, an auto-increment id) and a secret key; it gives you a short code that
- never collides — the mapping is a keyed permutation (FF1, NIST
SP 800-38G), so uniqueness is mathematical, not probabilistic. No retry
loops, no birthday problem, no
UNIQUE-violation handling as a code path. - doesn't leak your numbers — sequential inputs produce scattered, unpredictable outputs. Order volume, issue rate, and "how many came before me" stay private (no German tank problem).
- stays as short as possible — codes start at 6 characters (configurable) and grow by one character only when the current length is exhausted.
- decodes back — with the key, a code maps back to its counter. Look up
orders WHERE id = decode(code); no extra column index required, and obviously-invalid codes are rejected before touching the database.
The same key + config produces the same mapping in every language.
SPEC.md is normative and shared test vectors keep all implementations
bit-identical.
| Language | Directory | Install | Crypto dependency |
|---|---|---|---|
| Python | python/ |
pip install dealcode |
cryptography (PyCA) |
| TypeScript / JavaScript | js/ |
npm install dealcode |
node:crypto (built-in) |
| Go | go/ |
go get github.com/algorix-hq/dealcode/go |
standard library |
| Java | java/ |
Maven io.algorix:dealcode |
JCE (built-in) |
| Rust | rust/ |
cargo add dealcode |
RustCrypto aes, sha2 |
| C | c/ |
make install / vendored (GCC/Clang: needs __int128) |
OpenSSL libcrypto |
| C++ | cpp/ |
CMake (wraps the C core) | OpenSSL libcrypto |
v1.0.1 is live everywhere: PyPI, npm, crates.io, Maven Central; Go modules resolve from this repository.
Everything else is dependency-free by design: FF1 and the dealcode layer are
implemented from the NIST specification in each language and validated against
the official NIST sample vectors plus this repo's shared vectors
(testvectors/).
from dealcode import Dealcode
codec = Dealcode(key="use `openssl rand -hex 32` in production")
codec.encode(0) # 'd568e1'
codec.encode(1) # 'f7f229'
codec.decode("f7f229") # 1Pick the shape your product needs:
Dealcode(key, "crockford", domain="coupons") # e.g. 'ZV6NQ0' — human-friendly, confusables handled
Dealcode(key, "dec", domain="orders") # e.g. '839207' — digits only
Dealcode(key, "base62", min_length=8) # e.g. 'tHx93bQk'
Dealcode(key, "hex", min_length=16, max_length=16) # fixed-length tokens
Dealcode(key, "!@#$%^&*") # your own alphabet, why notalphabet—dec,hex,base32,crockford,base36,base58,base62,base64url, or any 2–94 distinct printable ASCII characters. Presets come with sensible decode normalization (hex is case-insensitive; Crockford mapsO→0,I/L→1).domain— namespaces. One key, unrelated code streams for"orders","coupons","invites".min_length/max_length— starting and maximum code length. Equal values give fixed-length codes. Need fixed-length codes that never grow, PNR-style? The cycling mode (CyclingDealcode, SPEC §11) refills the same space through a different permutation each cycle instead of adding a character — codes repeat across cycles, so scope uniqueness per cycle.key— raw AES key bytes (16/24/32) or any string/bytes (hex fromopenssl rand -hex 32, a passphrase, a KMS blob); non-AES-sized material is deterministically expanded, identically in every language.
dealcode is deliberately storage-agnostic: it needs a never-repeating integer, which your database already knows how to produce.
CREATE SEQUENCE order_code_seq AS bigint MINVALUE 0 START WITH 0;n = db.scalar("SELECT nextval('order_code_seq')") # no locks, gap-friendly
code = codec.encode(n) # pure computation
db.execute("INSERT INTO orders (id, code, ...) VALUES (%s, %s, ...)", (n, code))Sequences guarantee no reuse even across rollbacks (gaps are invisible —
codes look random anyway), and FF1 guarantees distinct inputs give distinct
outputs. A UNIQUE index on code is a tripwire, not a mechanism: if it ever
fires, someone changed the key or config mid-namespace — investigate, don't
retry. Per-language READMEs show the same recipe idiomatically; MySQL and
others work with AUTO_INCREMENT/identity columns the same way.
One practical note: decode never trims — a copy-pasted code with a stray
space or newline is rejected as invalid. strip()/trim() user input
before decoding.
Use dealcode for order numbers, coupon and invite codes, ticket numbers, support PINs, shortlinks: things that must be unique, short, and non-revealing, where you already have (or can trivially add) a counter.
Do not use it for session tokens, API keys, or password-reset links — the code space is deliberately small, so use ≥128-bit random tokens for anything that authenticates. Need coordination-free IDs across machines, sortable IDs, or OTPs? Use UUIDv7/ULID/Snowflake, or HOTP/TOTP respectively. The full reasoning, alternatives table, and operational guidance live in docs/philosophy.md.
One rule to remember: key, alphabet, lengths, and domain are frozen the moment the first code ships. Changing any of them for an existing namespace can collide with already-issued codes. New scheme → new domain (or new key + new namespace).
encode(n) picks the code length d by range (counter < 16^6 → 6 hex
chars, < 16^7 → 7, ...), writes n as a d-digit number, and encrypts
those digits with FF1 — format-preserving encryption that outputs another
d-digit number under your key. Same-length codes can't collide because
encryption is a bijection; different-length codes can't collide because they
have different lengths. decode runs it backwards and validates strictly.
Details: SPEC.md · rationale: docs/design.md.
Using an AI assistant to write code against dealcode? Install the usage skill so it knows the operational rules (frozen config, decode semantics, cycling mode):
npx skills add algorix-hq/dealcodeAnd point it at the docs in agent-readable form:
llms.txt (index) ·
llms-full.txt
(everything in one file).
SPEC.md the normative spec (format v1) — implementations are written from this
testvectors/ NIST FF1 samples + generated cross-language vectors; passing = conformance
python/ js/ go/ java/ rust/ c/ cpp/ independent, idiomatically packaged implementations
docs/ philosophy, design rationale (EN/KO)
scripts/ test-vector generator (runs against the Python reference)
Contributions and new language ports welcome — a port is conformant when it passes both vector files. Spec changes require regenerating vectors and a format-version bump. See CONTRIBUTING.md; please report suspected vulnerabilities privately per SECURITY.md, and be excellent to each other per the Code of Conduct.
MIT © Algorix Corporation