diff --git a/api/create-challenge.mdx b/api/create-challenge.mdx
index 27727e4..c3befd5 100644
--- a/api/create-challenge.mdx
+++ b/api/create-challenge.mdx
@@ -48,7 +48,7 @@ You can call `createChallenge()` in either of two styles:
- Memory-hardness profile, only valid with `algorithm: 'argon2id'`. The profile abstracts the raw Argon2id parameters (`m`, `t`, `p`) so you never pass memory sizes yourself. Passing `argonProfile` with `algorithm: 'sha256'` throws.
+ Memory-hardness profile, only valid with `algorithm: 'argon2id'`. The profile abstracts the raw Argon2id parameters (`m`, `t`, `p`) so you never pass memory sizes yourself. Both profiles currently resolve to the same conservative tuning; pick the tier that matches your audience now, and future retuning will not break in-flight tokens because each token embeds its own parameters. Passing `argonProfile` with `algorithm: 'sha256'` throws.
@@ -271,12 +271,16 @@ Key differences from SHA-256:
- **Difficulty scale.** Each Argon2id hash takes milliseconds instead of microseconds, so difficulty caps at `8` and `"auto"` bounds default to `1`–`2` (versus `64` and `3`–`6` for SHA-256).
- **Profiles instead of raw parameters.** `argonProfile` resolves the Argon2id memory, iteration, and parallelism parameters for you. The library enforces a hard upper bound (`HARD_MAX`, exported from `ribaunt`) on those parameters, and tokens carrying values above it are rejected as `invalid-token`.
-- **Tokens are self-describing.** Each challenge token carries its algorithm and Argon2id parameters, signed into the JWT. `verifySolution()` detects the algorithm per token, so your verify endpoint needs no changes.
+- **Tokens are self-describing.** Each challenge token carries its algorithm, its Argon2id parameters, and a construction version (`v: 1`), signed into the JWT. `verifySolution()` detects the algorithm per token, so your verify endpoint needs no changes, and future profile retuning cannot break tokens that are already in flight.
- **Browser support is automatic.** The widget and its solver worker detect the algorithm per token and load the Argon2id solver on demand. The [`solver-backend` event](/widget/events#solver-backend) reports `argon2id` so you can confirm it in telemetry.
- **Testing.** The synchronous [`solveChallenge()`](/api/solve-challenge) helper supports SHA-256 only. Use `solveChallengeAsync()` in tests that solve `argon2id` tokens.
The adaptive engine works the same for both algorithms: `riskScore` and `calibration` remain raise-only signals within your configured bounds. [`assess()`](/api/assess) accepts `algorithm` and `argonProfile` in its `workload` options when you want the risk engine to produce Argon2id workloads.
+
+ Argon2id narrows the attacker's GPU and ASIC advantage but does not eliminate it. A well-funded adversary can still solve any proof-of-work challenge, just at a higher memory cost. Treat it as one abuse-cost layer alongside rate limiting and risk signals, not as human verification or the sole gate for sensitive actions.
+
+
## Validation
`createChallenge()` validates its inputs at runtime and throws if anything is invalid:
diff --git a/api/types.mdx b/api/types.mdx
index 6333e4a..d1e5eee 100644
--- a/api/types.mdx
+++ b/api/types.mdx
@@ -29,7 +29,7 @@ interface ChallengeSolution {
`PowAlgorithm` selects the proof-of-work algorithm for `createChallenge()` and `selectWorkload()`. The default is `'sha256'`; `'argon2id'` is a memory-hard opt-in — see [Argon2id opt-in](/api/create-challenge#argon2id-opt-in).
-`ArgonProfile` abstracts the raw Argon2id parameters so you never pass memory sizes yourself. It is only valid when the algorithm is `'argon2id'`.
+`ArgonProfile` abstracts the raw Argon2id parameters so you never pass memory sizes yourself. It is only valid when the algorithm is `'argon2id'`. Both profiles currently resolve to the same conservative parameters (`m: 8192`, `t: 1`, `p: 1`, `hashLen: 32`). Because each token embeds its own parameters and a construction version, choosing a tier now is safe: retuning in a future release cannot break in-flight tokens.
```ts
type PowAlgorithm = 'sha256' | 'argon2id';
diff --git a/api/verify-solution.mdx b/api/verify-solution.mdx
index 2d7f4e1..0c657e7 100644
--- a/api/verify-solution.mdx
+++ b/api/verify-solution.mdx
@@ -148,7 +148,7 @@ const result = await verifySolution(tokens, solutions, {
| Reason | Description |
| --- | --- |
-| `invalid-token` | JWT signature is invalid or malformed |
+| `invalid-token` | JWT signature is invalid, the payload is malformed, or the token carries an unknown construction version (`v`) |
| `expired-token` | Challenge TTL has passed |
| `invalid-solution` | The nonce does not produce a valid hash |
| `context-mismatch` | The supplied context does not match the token's bound context |
diff --git a/changelog/overview.mdx b/changelog/overview.mdx
index b04b95c..6614d35 100644
--- a/changelog/overview.mdx
+++ b/changelog/overview.mdx
@@ -4,6 +4,22 @@ description: "New features, updates, and fixes to Ribaunt."
mode: "center"
---
+
+ ## Improvements
+
+ **Challenge token construction version**
+
+ Every challenge token now embeds a construction version (`v: 1`) that pins how the solution is computed. [`verifySolution`](/api/verify-solution) rejects tokens with an unknown future version as `invalid-token`, and the solvers return `undefined` instead of solving under wrong assumptions. Tokens issued before this release (no `v` field) are treated as version 1 and still verify, so no action is needed.
+
+ **Argon2id profile tiers clarified**
+
+ The `'mobile'` and `'standard'` values of [`argonProfile`](/api/create-challenge#argon2id-opt-in) currently share the same conservative tuning. Pick the tier that matches your audience now; because each token embeds its own parameters and version, future retuning cannot break in-flight tokens.
+
+ ## Fixes
+
+ - **Argon2id salt entropy.** Challenge strings now carry 16 random bytes (128 bits) of entropy, and the Argon2id salt is derived from the full-entropy challenge instead of a shorter zero-padded value. Tokens issued before this fix still verify during their TTL window.
+
+
## New
diff --git a/concepts/how-it-works.mdx b/concepts/how-it-works.mdx
index af972e4..d594d5e 100644
--- a/concepts/how-it-works.mdx
+++ b/concepts/how-it-works.mdx
@@ -55,10 +55,13 @@ Each challenge token is a standard signed JWT. When decoded, its payload contain
| Field | Description |
| --- | --- |
-| `challenge` | A random base64 string generated fresh for each token |
+| `challenge` | A random 128-bit base64url string (16 random bytes) generated fresh for each token |
| `difficulty` | The number of leading zeros the SHA-256 hash must start with |
| `expires` | A Unix timestamp (seconds) after which the token is rejected |
| `jti` | A UUID that uniquely identifies this token, used for replay prevention |
+| `v` | The construction version, currently `1`. It pins how the solution is computed, so future changes cannot break in-flight tokens. |
+
+Tokens with an unknown future `v` value fail closed: `verifySolution()` rejects them as `invalid-token`, and the solvers return `undefined` instead of solving under wrong assumptions. Tokens issued before versioning (no `v` field) are treated as version 1 and still verify.
Ribaunt hashes with SHA-256 by default. If you opt in to the memory-hard [Argon2id algorithm](/api/create-challenge#argon2id-opt-in), each token additionally carries the algorithm identifier and its Argon2id parameters, and the solver and `verifySolution()` detect the algorithm per token automatically.