← Back to the tool

Payments · Under the hood

Network tokenization

What each button in the tool actually computes, step by step — with the real formulas.

What's real here, and what isn't

The concepts this tool demonstrates — a token vault, domain-restricted tokens bound to a Token Requestor ID, detokenization happening only inside the network rather than at the merchant — are all real and come from EMVCo's public Payment Tokenisation specification. What isn't public is exactly how a real network generates a token's digits or derives a per-transaction token cryptogram; both are reproduced here only as representative constructions, flagged the same way the CAVV tool flags its own.

A Luhn-valid token in a reserved range

A real token looks exactly like a PAN — same length, same Luhn check digit — so it flows through existing card-rail systems unmodified; only the network's own vault can tell it apart from a real card number. This tool reserves a made-up BIN prefix for its demo tokens and computes a genuine Luhn check digit over the result, the one piece of this tool that is a real, standard algorithm.

Luhn check digit — the real, standard part
function luhnCheckDigit(digitsWithoutCheck) {
  let sum = 0, double = true;
  for (let i = digitsWithoutCheck.length - 1; i >= 0; i--) {
    let d = parseInt(digitsWithoutCheck[i], 10);
    if (double) { d *= 2; if (d > 9) d -= 9; }
    sum += d;
    double = !double;
  }
  return ((10 - (sum % 10)) % 10).toString();
}

The same checksum every real card PAN carries — implemented directly rather than simulated, since unlike token generation itself, Luhn has never been proprietary.

Domain binding: the actual security property

Tokenizing the same PAN for two different Token Requestor IDs produces two unrelated tokens, and the vault remembers which merchant each one belongs to. "Try merchant A's token at merchant B" replays a token under the wrong Token Requestor ID — exactly what a merchant data breach would hand an attacker — and the vault declines it on that binding check alone, before ever reaching the underlying PAN.

The check that actually matters
function attemptAuthorize(token, requestingTrid, vault) {
  const entry = vault[token];
  if (!entry) return "declined: unknown token";
  if (entry.trid !== requestingTrid) return "declined: wrong Token Requestor ID";
  return "approved: vault detokenizes internally for settlement";
}

A stolen token is far less useful than a stolen PAN precisely because of this one comparison — it only ever authorizes within the domain it was issued for.

Now try it

Back to Network tokenization →

Tokenize one PAN for two different merchants, see the tokens come out different and domain-bound, then try replaying one merchant's token at the other and watch the vault reject it.