Executive Summary
| Why we did it | Account recovery should be fast, readable, and calm without becoming permissive |
| Saved factor | Recovery Code: XXXXXX-XXXXXX-CC, where 12 characters are secret and 2 are checksum |
| Email factor | Verification Code: XXXXCC, where 4 characters are secret and 2 are checksum |
| Alphabet | ABCDEFGHJKLMNPQRTUWXYZ346789, chosen to avoid visually confusing characters |
| Server limits | 10-minute email-code window, one-time use, attempt limits, resend cooldown, transaction TTL, and abuse telemetry |
| Security boundary | Email code proves inbox access. Recovery Code proves saved-account-secret possession. Neither is enough alone for protected recovery. |
The Problem We Were Solving
Recovery is not a normal login. People reach it when something has already gone wrong: a lost device, a missing passkey, a new phone, or a deadline. A recovery design can be cryptographically sound and still fail users if the code is too long, hard to read, hard to copy, or easy to mistype.
The goal was not to make recovery casual. The goal was to make the honest path less painful while keeping the server strict. That meant reducing transcription mistakes, keeping all important fields on one screen, making paste forgiving, and validating obvious typos before the user waits on a server response.
The New Codes
Both codes use the same PayCal accessibility alphabet:
ABCDEFGHJKLMNPQRTUWXYZ346789
We intentionally exclude characters that are commonly confused when read, copied, printed, or typed. Inputs are normalized by uppercasing and removing spaces and hyphens, so users can paste codes in grouped or ungrouped form.
| Code | Display format | Secret characters | Checksum characters |
|---|---|---|---|
| Recovery Code | XXXXXX-XXXXXX-CC |
12 | 2 |
| Verification Code | XXXXCC |
4 | 2 |
The checksum is not a secret and does not add security entropy. It exists to catch honest mistakes. If the last two characters do not match the secret portion, the browser can immediately say, "Check the last two characters," before the user burns a server attempt.
The Math
The Recovery Code has 12 secret characters drawn from 28 possible symbols. That gives:
28^12 = 232,218,265,089,212,416 possibilities
log2(28^12) ~= 57.7 bits
The email Verification Code has 4 secret characters:
28^4 = 614,656 possibilities
log2(28^4) ~= 19.2 bits
When the two factors are considered together, the secret search space is:
28^16 = 142,734,349,946,674,946,768,896 possibilities
log2(28^16) ~= 76.9 bits
A single random guess against the saved Recovery Code is about 1 in 232 quadrillion. A single random guess against both factors together is about 1 in 142 septillion. With five combined guesses, the chance is still about 3.5 x 10^-23, or roughly 1 in 28.5 sextillion.
Why Online Brute Force Takes So Long
The important phrase is online recovery. Attackers do not get to run unlimited guesses through PayCal as fast as their hardware can compute. The server controls the pace, counts failures, expires codes, records abuse telemetry, and requires transaction state to line up.
Using the current conservative defaults, recovery is bounded by controls such as:
- email Verification Codes expire after 10 minutes;
- email Verification Codes are single-use;
- verification and proof endpoints are attempt-limited;
- recovery starts are limited per day;
- resends are limited per hour and have a cooldown;
- server-seen checksum failures count toward abuse telemetry and attempt policy;
- recovery transactions and bootstrap windows expire;
- the server, not the browser, remains authoritative.
At five guesses per hour, trying half of the Recovery Code space would take about 2.65 trillion years. Trying half of the combined Recovery Code plus email-code space at the same online pace would take about 1.63 quintillion years. That is the "really, really long time" we mean: not because the user has to carry a huge code, but because online recovery has multiple factors and the server will not let guesses run freely.
The 10-Minute Window
The email code is intentionally short because it is not meant to protect the account by itself. It is time-limited, delivered to the user's inbox, attempt-limited, and consumed on success.
With 614,656 possible email-code secrets, five guesses inside one 10-minute code window gives at most a 0.000813% chance of guessing the email code by chance, or about 1 in 122,931. That still would not complete protected recovery, because the saved Recovery Code and recovery proof must also pass.
What Changed in the User Experience
We reduced the recovery screen to one centered form. The user sees the email field, the Verification Code field, the Recovery Code field, and one primary action: Verify and continue.
- The user can paste the saved Recovery Code while waiting for email.
- Spaces and hyphens are accepted and normalized.
- Recovery Codes are auto-formatted as
XXXXXX-XXXXXX-CC. - Verification Codes are auto-formatted as
XXXXCC. - Invalid alphabet characters show a clear inline message.
- Checksum failures are caught locally before the server request.
- When both formats look right, the form shows "Checking..." and submits automatically after a short debounce.
- The Verify and continue button remains available as a fallback.
The result is faster for the normal user: fewer steps, fewer surprises, less hidden state, and less punishment for copying a code with spaces or hyphens.
What Changed in the Security Model
The redesign also tightened several recovery boundaries:
- Raw Recovery Codes are not emailed. They are displayed once when created and must be saved by the user.
- PayCal stores recovery-wrapped material and verifier state, never the raw Recovery Code.
- Magic links cannot make an existing protected account passkey-ready by themselves.
- Email-only bootstrap is allowed only for first-passkey setup when there is no existing protected crypto material and no existing passkey.
- Completing recovery requires the passkey credential ID registered in the recovery transaction to match the completion request.
- Old passkeys are revoked after successful account recovery so the new passkey becomes the trusted credential.
What This Does Not Mean
This is not a claim that a 12-character human-readable code is a 256-bit offline secret. PayCal account recovery is an online, server-mediated, two-factor recovery flow. The Recovery Code is one factor; inbox access and transaction state are another; passkey registration is the final device-bound step.
That distinction matters. We made the code readable because people need to save it and type it correctly. We kept the server strict because readable recovery should not mean weak recovery.
The Rule We Now Enforce
Email code proves inbox access.
Recovery Code proves saved-account-secret possession.
Neither one is enough alone for protected account recovery.
That is the balance we wanted: recovery that feels easy, fast, and simple for the account owner, while remaining strict enough that guessing through the server is not a practical path.