| Internet-Draft | WTX-1 | August 2026 |
| Surampudi | Expires 4 March 2027 | [Page] |
This document defines WTX-1, a protocol for preserving pseudonymous user context across web domains that are operated by, or on behalf of, the same organization and that have mutually opted into the exchange. The protocol operates only after explicit user consent and does not use third-party cookies, browser fingerprinting, or collection of direct identifiers by default. Identifiers are pseudonymous, not anonymous: they can be linked to application-level identities by the deploying organization and may constitute personal data under applicable law.¶
WTX-1 transfers context using an encrypted, authenticated, destination-bound, single-use token carried in the URL fragment. Tokens are issued and verified server side, with atomic replay consumption, DNS-based domain authorization, and short-lived server-signed write grants that authorize browser write operations without trusting caller-supplied tenant claims.¶
This revision (draft-02) replaces the signed-cleartext token format of draft-01 with a sign-then-encrypt construction, specifies write grants and replay-consumption ordering, defines the consent lifecycle including withdrawal and asynchronous cancellation, adds storage retention limits and user inspection, reset, and revocation controls, and rescopes the document's security, privacy, and performance claims to match the reviewed reference implementation. A complete change log appears in Appendix A.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 4 March 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
Third-party cookies have been the primary mechanism for cross-domain user identification since the 1990s. With Safari's Intelligent Tracking Prevention ([ITP]) deployed in 2017, Firefox Enhanced Tracking Protection in 2019, and Chrome's Privacy Sandbox initiative ([PrivacySandbox]), the mechanism is being removed or restricted across major browsers.¶
Organizations that legitimately operate multiple registrable domains (for example, a healthcare system with separate provider and patient-portal domains, a bank with separate brand and servicing domains, or a government with separate agency domains) lose the ability to understand that the same visitor moved between their own properties, even when the visitor has consented to that continuity.¶
WTX-1 addresses this narrow case: consented, pseudonymous context continuity between domains that are commonly operated and mutually authorized. It is deliberately not a general-purpose cross-site tracking mechanism; the authorization model (Section 8) makes the set of participating domains explicit and auditable.¶
Carrying identity through navigation URLs ("link
decoration") is established practice. Google Analytics
cross-domain measurement appends a _gl parameter to
outbound links ([GA-LINKER]), and the Adobe
Experience Cloud Identity Service appends an
adobe_mc parameter ([ADOBE-MC]).
WTX-1 belongs to this transport family and does not claim to
introduce a new capability class.¶
What WTX-1 specifies, and what distinguishes it from deployed linker parameters, is the security and privacy contract of the handoff:¶
This document is therefore best read as a hardening and consent layer for an existing, widely deployed pattern, and as a candidate baseline for evaluating that pattern in privacy research venues such as the IRTF Privacy Enhancements and Assessments Research Group ([PEARG]).¶
In scope: the pseudonymous identifier format (Section 4); the token envelope, cryptographic construction, issuance, and verification rules (Section 5 and Section 6); fragment transport and URL cleanup (Section 7); domain authorization, tenant resolution, and write grants (Section 8); the consent lifecycle (Section 9); first-party storage integrity and retention (Section 10); and user transparency and control interfaces (Section 11).¶
Out of scope: analytics event semantics beyond the boundary described in Section 11.1; identity resolution against application accounts (only its privacy consequences are discussed, in Section 14.4); and any assessment of legal or regulatory compliance, which depends on jurisdiction, purpose, and deployment context and is not a property a protocol document can confer.¶
Anonymity. WTX-1 identifiers are pseudonymous. The deploying organization can link them to application-level identities (Section 14.4), and they may be personal data under applicable law (Section 14.1).¶
Protection against a fully compromised page. A script with arbitrary execution on a participating page (for example, via cross-site scripting) can observe what that page can observe (Section 13.3).¶
Guaranteed compatibility with future browser anti-tracking heuristics (Section 14.6).¶
Legal or regulatory compliance. This document uses jurisdiction-neutral language throughout and makes no compliance claims.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
To keep interoperability requirements distinguishable from advice, this document separates statements into four classes:¶
Statements about the reference implementation (Section 17) are descriptive, not normative.¶
Draft-02 is a substantial revision: the token format, authorization model, consent lifecycle, storage retention, and claims language all changed. Implementers of draft-01 should treat this document as a replacement rather than a delta. Appendix A lists the changes and their rationale.¶
unknown, granted,
denied, or withdrawn
(Section 9). Identity creation, storage,
token processing, and event transmission occur only in the
granted state.¶
head element that removes the
token from the URL before other scripts execute
(Section 7.2).¶
Reviews of earlier revisions found that the words "encrypted" and "signed" were applied to mechanisms that were neither. This document uses the following terms precisely, and implementations' user-facing documentation SHOULD preserve these distinctions:¶
Browser-stored identity data is encoded and tamper-evident; it is not encrypted. Cross-domain tokens are encrypted and signed. Consent state is stored as plain values.¶
Source page (source.example) Verification server
---------------------------- -------------------
1. Consent granted; WaiTag
exists in first-party
storage.
2. POST /api/tracking/grant ------> Resolve tenant from
{ domain } domain config; sign
<------ write grant
3. Register WaiTag with
grant (X-Nylo-Grant) ------> Validate grant; store
pseudonymous record
4. User activates link to
destination.example.
Client asks its backend
for a token; backend
calls issuance API ------> Issue encrypted,
(server credential) signed, destination-
<------ bound token
5. Navigate to
https://destination.example/page#nylo_token=<token>
Destination page (destination.example)
---------------------------------------
6. Early-cleanup script strips the fragment and stashes
the token before other scripts run.
7. Client requests write grant for destination.example.
8. POST /api/tracking/verify-cross-domain-token
with token + grant ------> Authorize grant;
validate envelope;
decrypt; verify
signature; check
expiry and binding;
atomically consume
<------ { identity } or error
9. On success, the client adopts the WaiTag in first-party
storage and dispatches a transparency event
(nyloContextPreserved). On any error, the destination
starts a fresh, unlinked session.
Steps 2-3 and 7 fail closed: a page whose domain has no tenant mapping receives no grant and therefore cannot register identifiers, ingest events, or verify tokens. Step 8's internal ordering is normative and is specified in Section 6.¶
The token envelope carries an integer format version
v. This document defines version 2. Verifiers
MUST reject any token whose envelope version
is absent or not equal to 2 with
UNSUPPORTED_VERSION, before attempting decryption.
In particular, the signed-cleartext format of draft-01
(version 1 or unversioned) MUST be rejected;
there is no downgrade path. The version is also bound inside
the authenticated additional data and the signed inner
payload (Section 5.2), so an attacker
cannot re-label a token's version without detection.¶
Future versions are expected to be introduced by issuing tokens with a new version number and deploying verifiers that accept the new version explicitly. A verifier MUST NOT apply version-2 processing to a token labeled with another version.¶
A WaiTag is a lowercase ASCII string of the form:¶
waitag = "wai_" 19HEXDIG-L "_" 8HEXDIG-L HEXDIG-L = DIGIT / "a" / "b" / "c" / "d" / "e" / "f"¶
that is, the literal prefix wai_, 19 lowercase
hexadecimal characters, an underscore, and 8 lowercase
hexadecimal characters. Consumers MUST
validate this shape and reject values that do not match
rather than normalizing them.¶
The 27 hexadecimal characters are consecutive substrings of a single SHA-256 digest [FIPS-180-4] computed over the concatenation of:¶
crypto.getRandomValues
[WebCrypto]);¶
The timestamp and salt act as diversifiers only; all unpredictability comes from the 128-bit random input. The identifier exposes 108 bits (27 hexadecimal characters) of the digest.¶
Generation MUST fail closed: if no
cryptographically secure random source or digest primitive
is available, the client MUST NOT generate an
identifier, and cross-domain features remain disabled.
Clients MUST NOT fall back to
non-cryptographic randomness such as
Math.random(). (Draft-01 permitted such a fallback;
draft-02 removes it, matching the reviewed
implementation.)¶
A WTX-1 token is a sign-then-encrypt object: the inner payload is signed with HMAC-SHA256 [RFC2104], the signed payload is encrypted with AES-256-GCM [SP800-38D], and the result is wrapped in a cleartext envelope carrying only the routing metadata needed to select decryption keys. Confidentiality is not optional in version 2: a conforming verifier never processes cleartext context fields. (Draft-01 specified a signed but cleartext payload; see Appendix A.)¶
The token string is the Base64 encoding ([RFC4648], Section 4) of a UTF-8 JSON [RFC8259] object with exactly these members:¶
v:tid:dst:iv:ct:tag:The complete token string MUST NOT exceed 16384 characters; verifiers MUST enforce this and the per-field limits above before any cryptographic processing, to bound resource consumption (Section 13.5).¶
tid and dst are the only cleartext fields.
They carry no user context; they exist so the verifier can
derive the correct keys, and both are additionally bound
into the AEAD associated data and repeated inside the signed
inner payload, so tampering with either is detected twice
(Section 5.2).¶
When a token is placed in a URL fragment or query component, the parameter value MUST be percent-encoded as required for that component ([RFC3986]); consumers decode the parameter before Base64 decoding.¶
All keys are derived from a single server-side token secret using HKDF-SHA256 [RFC5869]. The secret MUST NOT be used directly as an encryption or MAC key, and it is never available to browsers.¶
key(purpose, tenant, destination) =
HKDF-SHA256(
IKM = token secret (UTF-8),
salt = "nylo-wtx1-v2" (UTF-8),
info = purpose || "|" || tenant || "|"
|| lowercase(destination) (UTF-8),
L = 32 octets)
encKey = key("enc", tid, dst) ; AES-256-GCM key
macKey = key("mac", tid, dst) ; HMAC-SHA256 key
AAD = "WTX1|v2|" || tid || "|" || lowercase(dst) (UTF-8)
sig = lowercase-hex(HMAC-SHA256(macKey,
JSON(canonical inner payload)))
plaintext = JSON(canonical inner payload + {"sig": sig})
(ct, tag) = AES-256-GCM-Encrypt(encKey, iv, plaintext, AAD)
token = base64(JSON(envelope))
¶
Consequences that verifiers rely on:¶
tid or dst in
the envelope causes AEAD authentication failure even before
the inner signature is examined, and the AAD's fixed
WTX1|v2| prefix binds the protocol version.¶
The issuer MUST generate a fresh random
12-octet IV for every token from a cryptographically secure
source; an AES-GCM key/IV pair MUST NOT be
reused. Issuers MUST use a cryptographically
secure source for the jti claim (the reference
implementation uses 16 random octets, hex-encoded).¶
The inner payload is a JSON object whose members are serialized in exactly this order:¶
{
"v": 2,
"jti": "<128-bit random, lowercase hex>",
"iat": <issue time, integer milliseconds since epoch>,
"exp": <expiry time, integer milliseconds since epoch>,
"tenantId": "<tenant identifier>",
"sourceDomain": "<lowercase source registrable domain>",
"destinationDomain": "<lowercase destination domain>",
"waiTag": "<WaiTag being transferred>",
"sessionId": "<opaque session identifier>",
"userId": <application user id string, or null>
}
¶
Canonicalization rule: the signature is computed over the
UTF-8 JSON serialization of the object with the ten members
above in exactly that order, with no insignificant
whitespace, and with userId normalized to JSON
null when absent. A verifier MUST
reconstruct this canonical form from the decrypted fields
and compare signatures in constant time. The sig
member (lowercase hexadecimal HMAC-SHA256 output) is
appended to the payload object after signing and is not
itself signed.¶
All of v, jti, iat, exp,
tenantId, sourceDomain,
destinationDomain, waiTag, and
sessionId are required claims; userId is
nullable. Issuers MUST refuse to issue tokens
with missing required claims, and verifiers
MUST reject payloads with absent, null, or
empty required claims (MISSING_CLAIMS).¶
Times are integer milliseconds. The default token lifetime
(exp - iat) is 300 seconds and is
configurable per deployment; issuers SHOULD NOT exceed it materially, since token lifetime
bounds both the replay-store retention window and the
exposure window of a leaked token.¶
Token issuance is an authenticated, server-side operation:¶
X-API-Key header); requests without a
valid credential are rejected with UNAUTHORIZED
(HTTP 401). Browsers never hold issuance credentials;
deployments front issuance with their own first-party
backend.¶
destinationDomain MUST be
supplied at issuance; tokens without a destination binding
MUST NOT be issued.¶
sourceDomain and
destinationDomain MUST be validated
as registrable domains ([PSL]), and both
MUST be authorized for the tenant
(Section 8.1) at issuance time;
issuance for an unauthorized domain is refused with
DOMAIN_NOT_VERIFIED. When domain-authorization
state cannot be evaluated, production issuers
MUST refuse issuance rather than skip the
check.¶
The destination client submits the token to the tenant's
verification endpoint together with the destination domain
and a valid write grant (Section 8.3) in
the X-Nylo-Grant header:¶
Both body members are REQUIRED; requests missing either, or naming a syntactically invalid domain, are rejected with HTTP 400 before the algorithm runs, without a machine-readable code (Section 6.4).¶
The verifier MUST authorize before it
consumes: validation of a grant bearing the
ingest scope (Section 8.3) --
signature, expiry, scope, and domain binding -- and the
domain-authorization checks of
Section 6.2 complete before any
replay-protection state changes. Without this ordering, any
unauthenticated observer of a token could "verify" it once,
burning it and denying the legitimate destination its
handoff (Section 13.5).¶
The tenant used for token binding comes exclusively from the
authenticated grant. A customerId member in the
request body is legacy-optional; when present it
MUST agree with the grant tenant and the
request MUST be rejected on conflict
(TENANT_MISMATCH) rather than silently
reassigned.¶
After grant authorization, the verifier MUST perform the following checks in order, returning the indicated error code at the first failure and revealing no further detail:¶
MALFORMED_TOKEN.¶
MALFORMED_TOKEN.¶
v equal to 2; otherwise
UNSUPPORTED_VERSION. Legacy cleartext tokens fail
here before any further processing.¶
tag present and a string;
otherwise MISSING_SIGNATURE.¶
tid,
dst, iv, ct nonempty strings
within the limits of Section 5.1;
decoded IV exactly 12 octets, decoded tag exactly 16
octets, decoded ciphertext between 1 and 8192 octets;
otherwise MALFORMED_TOKEN.¶
dst
equals the expected destination
(DOMAIN_MISMATCH) and the envelope tid
equals the grant tenant (TENANT_MISMATCH). These
exist to give precise errors cheaply; the cryptographic
binding below re-enforces both.¶
tid, dst), decrypt with the envelope IV,
tag, and the AAD of Section 5.2. Any
failure (wrong secret, wrong tenant or destination,
tampered ciphertext, IV, tag, or AAD) yields
INVALID_SIGNATURE.¶
MALFORMED_TOKEN.¶
sig present;
otherwise MISSING_SIGNATURE.¶
MISSING_CLAIMS.¶
v equal to 2; otherwise
UNSUPPORTED_VERSION.¶
tid, dst) MAC key;
compare in constant time; otherwise
INVALID_SIGNATURE.¶
payload.tenantId
equals tid and
payload.destinationDomain equals dst;
otherwise INVALID_SIGNATURE. A spoofed envelope
cannot smuggle a foreign payload.¶
iat is a number no further
than 60 seconds in the future (clock-skew allowance);
otherwise INVALID_IAT.¶
exp;
otherwise TOKEN_EXPIRED.¶
destinationDomain equals the requested domain
(DOMAIN_MISMATCH) and the signed tenantId
equals the grant tenant (TENANT_MISMATCH).¶
sourceDomain and the signed
destinationDomain MUST be currently
authorized for the token's tenant
(Section 8.1); otherwise
DOMAIN_NOT_VERIFIED. When domain-authorization
state cannot be evaluated, production verifiers
MUST reject rather than skip this step
(Section 16.2).¶
TOKEN_REPLAYED.¶
Each token is acceptable at most once. The consumption
operation MUST be an atomic check-and-set on
a collision-resistant token identifier (the reference
implementation uses the SHA-256 hash of the token string;
the jti claim is an equivalent key): of N
concurrent verifications of the same token, exactly one
succeeds and the rest receive TOKEN_REPLAYED.
Consumed-token records MUST be retained at
least until the token's exp has passed (plus the
clock-skew allowance); after that, expiry itself rejects the
token.¶
Replay state MUST NOT change for requests that fail authorization or any verification step; only a fully valid, authorized verification consumes the token. In production the replay store must be durable and shared across server processes; this is a deployment requirement with fail-closed semantics (Section 16.1).¶
Verifiers MUST report every failure of
grant authorization (Section 8.3) and of
the verification algorithm
(Section 6.2) in a JSON body of
the form
{"success": false, "error": "<CODE>"} using
the codes below. The error code, not the HTTP status, is
the interoperable signal; as a guideline, the reference
implementation maps MALFORMED_TOKEN to 400, an
absent grant to 401, an unconfigured grant subsystem to
503, and every other coded failure -- including unsupported
versions, grant validation, binding, authorization, and
replay -- to 403.¶
Two response classes intentionally fall outside this coded
contract: structural request errors (a missing
token or domain member, or a
syntactically invalid domain) are rejected with HTTP 400,
and internal server errors with HTTP 500; both carry only a
human-readable message alongside
{"success": false}, with no error member.
Clients MUST treat a failure response
without an error member exactly like a coded
failure: fresh session, no retry of the same token.¶
| Code | Meaning |
|---|---|
UNSUPPORTED_VERSION
|
Envelope or payload version is not 2 (includes all legacy cleartext tokens). |
MALFORMED_TOKEN
|
Token is not a decodable envelope within size limits, or the decrypted payload is not a JSON object. |
MISSING_SIGNATURE
|
Envelope lacks the AES-GCM authentication tag, or the inner payload lacks its signature. |
MISSING_CLAIMS
|
A required claim is absent, null, or empty. |
INVALID_SIGNATURE
|
AEAD decryption or inner HMAC verification failed, or envelope and payload routing disagree (covers ciphertext, IV, tag, AAD, and payload tampering, and wrong-key attempts). |
INVALID_IAT
|
Token is future-dated beyond the clock-skew allowance. |
TOKEN_EXPIRED
|
Current time is after exp. |
TOKEN_REPLAYED
|
Token was already consumed. |
DOMAIN_MISMATCH
|
Token destination does not match the requesting destination. |
DOMAIN_NOT_VERIFIED
|
The signed source or destination domain is not currently authorized for the token's tenant (also returned at issuance; Section 8.1). |
ORIGIN_MISMATCH
|
A grant request's Origin header disagrees
with the requested domain (grant issuance,
Section 8.3). |
GRANT_REQUIRED
|
No write grant accompanied a write request (reported with HTTP 401). |
GRANT_DOMAIN_MISMATCH
|
The write grant was issued for a different domain. |
TENANT_MISMATCH
|
Tenant routing (envelope, payload, or legacy body field) disagrees with the authenticated grant tenant. |
Failures of the presented grant itself are reported with
GRANT_-prefixed codes: GRANT_REQUIRED
when no grant accompanies the request, and
GRANT_MALFORMED, GRANT_BAD_SIGNATURE,
GRANT_INVALID_CLAIMS, GRANT_NOT_YET_VALID,
GRANT_EXPIRED, or GRANT_SCOPE_MISSING
from grant validation (Section 8.3);
GRANTS_UNAVAILABLE (HTTP 503) signals an
unconfigured grant subsystem
(Section 16.2).¶
On any verification failure the destination client MUST proceed as if no token had been presented: it starts a fresh, unlinked session and MUST NOT retry the same token (Section 12.1).¶
A successful response carries the transferred identity, the destination domain, and the verification time:¶
{
"success": true,
"identity": {
"waiTag": "<WaiTag>",
"sessionId": "<session identifier>",
"userId": null
},
"domain": "<destination domain>",
"verifiedAt": "<ISO 8601 timestamp>"
}
¶
Tokens travel in the URL fragment under the parameter name
nylo_token (consumers also accept the legacy alias
wai_token), serialized in
application/x-www-form-urlencoded form:¶
https://destination.example/landing#nylo_token=<token>¶
Per [RFC3986], Section 3.5, the fragment
component is not sent in HTTP requests. With fragment
transport the token therefore does not appear in request
lines, Referer headers, server access logs, proxy
or CDN logs, or WAF logs of the destination or of third
parties; this is a structural property of HTTP, not a
runtime measurement (Section 15). The
fragment does remain visible to the user agent itself:
address bar, browser history until cleanup, and any script
or extension that can read the page's location
(Section 13.2).¶
Destination pages SHOULD place a small
synchronous script at the top of the head element
that, before any other script executes: (1) reads the
fragment; (2) if a token parameter is present, moves the
token into an implementation-specific in-memory variable;
and (3) rewrites the URL without the token via
history.replaceState(), so the address bar,
history entry, and any URL the user copies exclude the
token. The WTX-1 client later consumes the stashed token
and deletes the variable.¶
The reference early-cleanup script (informative):¶
<script>
(function () {
try {
var h = window.location.hash;
if (h && (h.indexOf("nylo_token=") > -1 ||
h.indexOf("wai_token=") > -1)) {
var p = new URLSearchParams(h.slice(1));
var t = p.get("nylo_token") || p.get("wai_token");
if (t) {
window.__nylo_early_token = t;
p.delete("nylo_token");
p.delete("wai_token");
var n = p.toString();
history.replaceState(null, "",
window.location.pathname + window.location.search +
(n ? "#" + n : ""));
}
}
} catch (e) { /* fail open: SDK falls back to hash */ }
})();
</script>
¶
Without early cleanup the protocol still functions -- the client removes the token when it initializes -- but the token remains in the URL for the whole interval between navigation and client initialization, which enlarges the exposure analyzed in Section 13.2 and Section 14.2.¶
Implementations MAY support carrying the token in the query component for environments that destroy fragments (some server-side redirect chains, some app webviews). Query transport MUST be disabled by default and enabled only by explicit configuration. When enabled, the token is transmitted to the destination server and appears in the six logging surfaces that fragment transport avoids; deployments enabling it SHOULD scrub token parameters from logs and MUST still enforce single use and expiry, which bound (but do not eliminate) the exposure.¶
Single-page applications using hash-based routing may conflict with fragment transport; such sites can run the token extraction before their router initializes or opt into query transport with the caveats above.¶
All grant, issuance, verification, registration, and event
requests MUST use HTTPS. The protocol
MUST NOT be deployed over cleartext HTTP;
without TLS, request-carried material (grants, verification
bodies, query-transported tokens) is visible to network
intermediaries. Destination pages SHOULD
send Referrer-Policy: no-referrer or an equally
strict policy so that post-cleanup URLs do not leak to
third-party resources, even though the fragment itself is
never included in Referer headers.¶
The client maintains one of four persistent consent states:¶
+---------+
+------- | unknown | -------+
| +---------+ |
grant| |decline
v v
+---------+ withdraw +----------+
| granted | --------------> | denied / |
| | <-------------- | withdrawn|
+---------+ re-grant +----------+
unknown: no decision recorded (initial state)
granted: identity and transfer features active
denied: user declined from unknown
withdrawn: user revoked a previous grant
¶
The initial state MUST be unknown
absent a persisted decision. In every state other than
granted, the client MUST NOT
generate or store identifiers, MUST NOT
create, transmit, or accept cross-domain tokens,
MUST NOT queue or send events, and
MUST NOT initiate protocol network traffic.
The distinction between denied and
withdrawn is informational (it records whether an
identity previously existed); their behavior is identical.¶
How and when a deployment asks for consent (banner, preference center, account setting) is deployment policy and out of scope; the protocol contributes a fail-closed default and the state machine above. Deployments in jurisdictions that require prior consent for identifiers can rely on the fact that nothing is generated, stored, or transmitted before an affirmative grant.¶
On transition to granted, the client generates a
WaiTag (Section 4.2), persists it in
first-party storage (Section 10), registers
it under a write grant, and begins any configured event
reporting (Section 11.1). The consent
decision itself is persisted so subsequent page loads
resume without re-prompting.¶
On transition out of granted, the client
MUST, before returning control:¶
Withdrawal is a client-side guarantee about the browser; deletion of previously transmitted server-side data is a separate obligation of the deploying organization outside this protocol's scope, supported by the pseudonymous identifier as a deletion key.¶
Identity startup is asynchronous (key generation, storage writes, grant requests, registration calls), so consent can change while work is in flight. Implementations MUST ensure that work belonging to a superseded consent decision neither persists nor transmits anything after the decision changes, and neither deletes state belonging to a newer decision:¶
The reference implementation satisfies this with a monotonically increasing epoch counter: every asynchronous continuation validates, immediately before persisting or transmitting, that the epoch and consent state under which it started are still current. The mechanism is an implementation choice; the observable requirements above are normative. Section 13.9 discusses why this matters for privacy.¶
In the granted state, identity is persisted
redundantly in first-party storage: a cookie
[RFC6265] (24-hour maximum age, consistent
with JavaScript cookie capping under Intelligent Tracking
Prevention [ITP]), localStorage
(long-lived
layer, governed by the retention rules below), and
sessionStorage (tab-scoped). All layers are
first-party; the protocol uses no third-party storage.¶
Stored identity records are encoded (a reversible, salted transformation) and carry an HMAC-SHA256 integrity value computed with a key that is present in client-side code. Terminology matters (Section 2.1): this storage is encoded and tamper-evident, not encrypted, and implementations and their documentation MUST NOT describe it as encrypted. (Draft-01's storage section used "obfuscation" and "encryption" interchangeably; draft-02 corrects this.)¶
On every read, the integrity value MUST be validated; a record that fails validation is rejected and that layer cleared. If no layer holds a valid record, a fresh identifier is generated. This defends against casual tampering (developer tools, cookie editors) and cross-site cookie injection. It is explicitly not a defense against a script running in the page context, which can read the key and forge valid records (Section 13.8).¶
Stored identifiers MUST NOT persist indefinitely. Two windows apply, evaluated on every read of the long-lived layer:¶
| Rule | Default | Behavior |
|---|---|---|
| Absolute lifetime | 180 days from creation | Expires regardless of activity |
| Unused expiry | 30 days from last use | Sliding window; "use" means restoration for active tracking. Passive inspection via the context-view API does not extend retention. |
These bounds limit how long any single pseudonymous identifier can correlate visits (Section 14.1). Implementations MAY rotate identifiers more aggressively.¶
Implementations MUST provide user-facing capabilities equivalent to the following, which the reference implementation exposes as public client APIs:¶
getStoredContext()):resetContext()):revokeContext()):nyloContextPreserved) that pages can use to show a
user-visible continuity indicator. Implementations
SHOULD surface restoration rather than making
it silent.¶
WTX-1 proper ends at pseudonymous context transfer. Two adjacent capabilities are deliberately separated from it:¶
Application identity linkage. An
identify()-style API that associates a WaiTag with
an application-level user identifier creates exactly the
linkage that pseudonymity otherwise avoids; the
userId claim exists so consented deployments can
carry that linkage. The privacy consequences are analyzed
in Section 14.4; implementations
MUST document the consequence and
MUST NOT enable such linkage by
default.¶
Event reporting. Reporting page views, clicks, or custom events is optional and separable. Every event category MUST be individually opt-in (default off), gated by the same consent state, and authorized by write grants. Event payloads are minimized: URLs are reduced to origin plus path (query strings and fragments discarded unless individually allowlisted), and fingerprint-capable attributes (user agent, language, timezone, screen and viewport geometry, pointer coordinates, and similar) are neither collected by the reference client nor accepted by the reference server, which strips them as defense in depth. Identity generation and matching never use browser or device attributes.¶
This section walks one full handoff. All identifiers, keys, and encoded blobs are illustrative placeholders, not a cryptographic test vector; Appendix B shows the corresponding structures in detail.¶
On source.example, the user has granted
consent. The client holds WaiTag
wai_9f86d081884c7d659a2fe_aa32b8b4 and a write
grant for source.example.¶
The user activates a link to
destination.example. The client asks its
first-party backend for a token; the backend calls the
issuance API with its server credential:¶
The issuer signs and encrypts the payload under keys
derived for (tenant t_1001,
destination.example) and returns the token.¶
The client decorates the navigation:¶
https://destination.example/landing#nylo_token=eyJ2IjoyLC...¶
On arrival, the early-cleanup script removes the
fragment before other scripts run and stashes the token;
the address bar shows
https://destination.example/landing.¶
The destination client obtains a write grant for
destination.example (scopes ingest and
register, Section 8.3), then
verifies under the ingest scope:¶
The verifier authorizes the grant, runs the algorithm of Section 6.2, atomically consumes the token, and answers:¶
{
"success": true,
"identity": {
"waiTag": "wai_9f86d081884c7d659a2fe_aa32b8b4",
"sessionId": "sess_c1a2b3",
"userId": null
},
"domain": "destination.example",
"verifiedAt": "2026-08-26T12:00:05.000Z"
}
¶
The destination client adopts the WaiTag into first-party
storage and dispatches nyloContextPreserved.¶
Error case: any later submission of the same token -- from a script that raced and lost, an extension that captured it, or a shared link -- fails:¶
{ "success": false, "error": "TOKEN_REPLAYED" }
¶
and the submitting page simply starts a fresh, unlinked session.¶
On any verification failure or network error, the destination client MUST behave as if no token had been presented: it starts a fresh session, MUST NOT retry the same token, and MUST NOT attempt to extract context from the token itself (the payload is ciphertext). No user-visible error is required; degraded continuity is the designed failure mode. A token lost to a network failure before consumption simply expires.¶
The assets are: the cross-domain token in transit through the user agent; the WaiTag and its stored records; the server-side token secret and grant-signing key; the replay store; and the domain-authorization state. The adversaries considered are: third-party scripts on participating pages; browser extensions; an attacker with script execution on a participating page (XSS); network observers (bounded by the HTTPS requirement); parties who can influence DNS answers; unauthenticated internet clients (denial of service); and authorized participants behaving beyond their authorization (collusion, Section 13.10).¶
Residual-risk language in this section is scoped: "none identified" means none identified within the stated model and assumptions, not an absolute guarantee. Draft-01's categorical "Residual risk: None" entries are withdrawn.¶
The token spends an interval in
window.location.hash. Three observer classes
matter:¶
Third-party page scripts. The early-cleanup script (Section 7.2) removes the token before subsequent scripts execute; afterward the token exists only in an implementation-specific variable until the client consumes and deletes it. A script that knows the variable name and runs in that window can read it; single use and short expiry bound the value of doing so, and the payload is ciphertext, so a captured token reveals only routing metadata unless the interceptor also wins the verification race -- in which case the legitimate page's verification fails and the user simply gets an unlinked session. There is no silent double-restore.¶
Browser extensions. A content script with
"run_at": "document_start" executes before any
page script, including early cleanup, and can read the
fragment. No application-layer mechanism can prevent this;
it is inherent to the extension model, and comparable
exposure exists for OAuth redirect parameters, tokens in
web storage, and cookies. The mitigations are the same as
above (single use, short expiry, ciphertext payload), plus
the observation that an extension with that capability
already reads all page content. This residual risk is
accepted and disclosed; a browser-mediated transfer API
would be required to eliminate it.¶
URL observers outside the page: browser history before cleanup, copied links, shoulder surfing. The fragment never reaches servers structurally (Section 7.1); locally it is bounded by early cleanup and by single use -- a copied pre-cleanup URL stops working after the original verification consumes the token, and after expiry in any case.¶
An attacker with script execution on a participating page operates with the page's full authority. Within this protocol's scope such an attacker can: read the stashed token before consumption or submit it first (winning the race, after which the legitimate flow fails visibly); read stored identity records from any storage layer; extract the client-visible integrity key and forge tamper-evident records (Section 13.8); obtain a write grant for the page's domain and submit writes as that page (Section 8.3); and observe the WaiTag. The attacker cannot decrypt tokens, forge tokens, mint grants for other domains or tenants, read server-side data, or extend a token's single use.¶
No cookie flag or client-side hardening changes this materially: identity records exist in storage layers that page script can read by design (an HttpOnly cookie would protect one of three copies while breaking the no-server-dependency deployment model). The defense is preventing XSS. Deployments SHOULD apply standard XSS prevention [OWASP-XSS] and a Content Security Policy (Section 16.6); clients MUST treat token and identity values as data, never inserting them into markup unescaped.¶
Single use is enforced by atomic consumption after full authorization (Section 6.3). The design points worth restating: consumption keyed by a collision-resistant token identifier; consumption strictly last, so failed or unauthorized attempts cannot burn tokens; records retained at least through expiry plus skew; and durability across server processes required in production (Section 16.1) -- an in-memory store behind a load balancer would accept the same token once per process.¶
jti values; rate limiting
(Section 16.5) bounds the
unauthenticated request stream.¶
Domain authorization is exactly as trustworthy as the DNS answers it consumes. An attacker who can spoof answers to the verification server, or who controls a participating zone, can pass authorization for a domain they do not legitimately control, and DNS compromise of a participating domain typically implies broader compromise (traffic redirection, certificate issuance). Mitigations: resolvers SHOULD validate DNSSEC where the zone is signed; verification servers SHOULD resolve over authenticated channels (DNS over HTTPS or DNS over TLS) to a trusted resolver; authorization results MUST be re-verified periodically rather than cached indefinitely (Section 16.4), so record removal revokes within the revalidation interval. Residual risk: an attacker with sustained control of DNS answers within the revalidation window; deployment-dependent.¶
All token keys derive from one token secret (Section 5.2). HKDF separation confines derived keys to their (purpose, tenant, destination) context -- a leaked derived key for one destination does not expose another's -- but it does not reduce the blast radius of master-secret compromise: an attacker holding the token secret can decrypt and forge tokens for every tenant and destination served by that secret until it rotates. The grant-signing key is similarly critical: its holder can mint write grants for any mapped domain.¶
Accordingly: secrets MUST be stored in server-side secret management (never in client-delivered code or version control); deployments SHOULD rotate the token secret periodically (90 days is the reference recommendation) and SHOULD support overlapping acceptance windows during rotation so in-flight tokens survive; keys MUST NOT appear in logs or error output; hardware-backed key storage is worth using where available. Compromise recovery is rotation plus invalidation of outstanding tokens (they expire within minutes by design).¶
The storage integrity mechanism (Section 10.2) is tamper evidence against actors without page-script execution: manual editing via developer tools, generic cookie-editor extensions, and cross-site cookie injection (a sibling domain setting cookies for the registrable domain cannot produce a record that validates). Against page-context script it provides nothing -- the key is client-visible by construction -- and this document deliberately does not call it authentication. Verifiers never trust browser-stored records for anything security-relevant; registration and verification are server-authorized (Section 8.3).¶
Without the cancellation requirements of Section 9.4, a withdrawal that arrives while identity startup is in flight can lose the race: the startup completes afterward and re-persists an identifier the user just deleted -- a consent violation that looks like a bug. The normative requirements (no persistence or transmission from superseded work; no cross-epoch purging; reset invalidates pending uploads) close this class. Implementers are urged to test the interleavings explicitly; the reference implementation's suite includes gated-crypto and gated-grant race tests.¶
WTX-1 constrains who can participate (tenant configuration, DNS authorization) and what crosses domains (a consented, pseudonymous, destination-bound token). It cannot technically prevent authorized participants from misusing what they legitimately receive: domains inside a tenant's authorized set could pool identity beyond what was disclosed to the user, and a tenant could link WaiTags to application identities server side (Section 14.4). These are policy violations, not protocol breaks. The protocol's contribution is auditability: the participating set is visible in public DNS records, the client exposes what is stored (Section 11), and write grants never authorize reads, so bulk export requires separate authentication outside this protocol.¶
Servers MUST NOT log token strings, grant strings, or any derived key material; log the token identifier hash if correlation is needed. With fragment transport the token never reaches server logs structurally; deployments enabling query transport MUST scrub the token parameter from access logs (Section 7.3). Verification failures are safe and useful to log by error code.¶
This section follows the questions of [RFC6973] where they apply. It states what the protocol does and does not protect, without jurisdictional claims; operators are responsible for their own legal assessment.¶
A WaiTag contains no direct identifiers and is generated from randomness, but it exists precisely to link visits by the same user across authorized domains over time. It is therefore a pseudonymous identifier, and this document does not describe any part of the system as anonymous. Because the deploying organization can hold additional information that attributes a WaiTag to a person -- most directly via application-identity linkage (Section 14.4), but also via account data, support records, or server logs -- WaiTags and the records keyed by them may constitute personal data under applicable data protection law, and deployers should assume they do when planning notice, lawful basis, access, and deletion processes. (Draft-01 stated the design "falls outside" personal-data definitions; that claim was wrong and is withdrawn.)¶
What bounds the pseudonym's reach: linkage extends only across the tenant's authorized domain set, only after affirmative consent, only while consent stands, and only within the retention windows (Section 10.3); reset and revocation break linkage at the user's initiative (Section 11); and a fresh identifier has no derivable relationship to its predecessor.¶
WTX-1 is a link-decoration protocol and inherits that shape's local observability: until cleanup runs, the decorated URL -- including the token -- exists in the address bar, in the history entry for the navigation, in anything that snapshots the URL (sync services, "recently closed" features), and in any link preview the source page shows. What distinguishes WTX-1's exposure: the token is ciphertext except tenant and destination routing fields, so an observer of the URL learns that a handoff occurred between which properties, but not the identifier or any context; the token stops working after one use or five minutes; and early cleanup removes it from the URL and the history entry promptly on arrival. The fact that a handoff occurred is itself a (deliberately minimal) disclosure; deployments for which even that is sensitive should not use URL-carried transfer at all.¶
By design, consented cross-domain correlation is the feature. The privacy-relevant boundaries are: the participating set is finite, tenant-configured, and DNS-auditable; correlation requires the token path (no passive matching -- identity generation and matching never use browser or device attributes); and nothing in the protocol supports correlation across tenants. Collusion beyond the disclosed set -- authorized domains pooling data contrary to what users were told -- is a policy violation the protocol cannot prevent, only make auditable (Section 13.10). Users can bound their exposure unilaterally: reset mints an unlinked identifier; withdrawal deletes local state entirely.¶
If a deployment calls an identify()-style API, or
issues tokens with a non-null userId, the
pseudonymous identifier becomes linked to an
application-level identity, and every record keyed by that
WaiTag -- past and future, on every domain where it is
restored -- becomes attributable to that account. This is
the single most consequential privacy action in the
system. It MUST be disabled by default,
SHOULD be taken only with notice to the
user and a considered basis, and MUST be
disclosed in implementation documentation. The protocol
deliberately keeps userId nullable so deployments
can run purely pseudonymous.¶
The token carries exactly the ten claims of Section 5.3: version, token id, two timestamps, tenant, two domains, WaiTag, session id, and nullable user id. No behavioral data, page content, interaction history, or device attributes ride the token. Event reporting, where enabled, is separately minimized (Section 11.1): URLs reduced to origin plus path, fingerprint-capable attributes stripped at both ends, invalid values rejected rather than normalized. The reference server stores no IP addresses with protocol records; infrastructure access logs are a deployment matter deployers should align with their retention policy.¶
WTX-1 uses no third-party cookies, no third-party storage, no redirect or bounce patterns, and no fingerprinting; its cookie layer respects script-written-cookie capping ([ITP]). The working hypothesis is that current tracking-prevention heuristics in major browsers do not disrupt it. This has not been verified with browser vendors, and heuristics evolve -- in particular, link-decoration countermeasures could strip or truncate unknown fragment parameters, which would break handoff (safely: the destination starts an unlinked session). This document records the interaction as a testable hypothesis, not a guarantee, and the honest position is that a consent-gated protocol should be legible to browser policy rather than engineered around it; [PEARG] review is sought partly for this question.¶
userId is possible by
deployment choice (Section 14.4).¶
Earlier revisions presented performance figures as protocol claims. This revision separates three kinds of statement:¶
Referer to the
destination, Referer to third parties, proxy/CDN
logs, WAF logs, pre-TLS packet capture of requests). This
follows from [RFC3986], Section 3.5, and is
verifiable by packet capture: request a page as
/landing#nylo_token=CANARY and confirm the canary
is absent from all captured requests and logs, then repeat
with query transport and confirm presence.¶
The following were observed on the reference implementation
and are reported as scoped measurements with methodology,
not as guarantees; independent reproduction is invited via
the client's timing API (getTimingMetrics()),
which timestamps each token lifecycle phase with
performance.now():¶
Hypotheses a deployment should test rather than assume: that its pages load the cleanup script first in practice; that its replay store sustains its verification rate; and that current browser heuristics leave its fragments intact (Section 14.6).¶
Items here are operational requirements and policy for running WTX-1 in production; they complement, and where marked extend, the protocol requirements.¶
In production, the replay store MUST be durable and shared across all server processes (a database or distributed cache); a production deployment configured without one MUST refuse to start rather than fall back to per-process memory. In-memory stores are for development only: they forget consumed tokens on restart and are per-process behind load balancers, both of which re-open replay.¶
Origin header
(Section 8.3).¶
Store the token secret and grant-signing key in a secrets manager; rotate the token secret on a schedule (the reference recommendation is 90 days) with an overlap window during which tokens issued under the outgoing secret are still verifiable; monitor for verification-failure spikes during rotation; never emit key material to logs (Section 13.7).¶
Re-verify domain authorization on a schedule appropriate to the deployment's risk tolerance (hours to days, not months); treat revalidation failure as revocation; prefer DNSSEC-validating or authenticated (DoH/DoT) resolution for authorization lookups (Section 13.6).¶
Rate-limit grant issuance, verification, registration, and ingestion endpoints. The reference guideline is on the order of 100 verification requests per client address per minute with backoff on failures; the specific numbers are deployment policy and should be set from observed legitimate traffic (Section 13.5).¶
Deploy CSP on participating pages as an XSS mitigation
(Section 13.3). The early-cleanup script is
inline by necessity (it must run before external scripts
load), so allow it via a nonce or a script hash rather
than a blanket 'unsafe-inline' where the
deployment can manage nonces; include the verification
origin in connect-src; consider
frame-ancestors 'none' where framing is not
needed.¶
Apply the logging rules of Section 13.11;
align infrastructure access-log retention with the
protocol's data-minimization posture
(Section 14.5); monitor replay
rejections (TOKEN_REPLAYED), grant failures
(GRANT_REQUIRED, ORIGIN_MISMATCH,
UNKNOWN_DOMAIN), and authorization failures as
abuse signals; alert on issuance-endpoint authentication
failures.¶
This section records the status of known implementations at the time of posting, per the spirit of [RFC7942], and is to be removed before any publication as an RFC.¶
One implementation is known: Nylo (https://github.com/tejasgit/nylo), the reference implementation by this document's author, comprising a browser client and a Node.js verification server. Maturity: pre-alpha; not production-deployed. Licensing: dual (open-source core, commercial terms for cross-domain components); see the repository. Coverage: the implementation and its automated test suite exercise the v2 token construction and every verification error path of Section 6.2, concurrent single-winner replay consumption, write-grant authorization including tenant-conflict rejection, the consent state machine including the asynchronous-cancellation races of Section 9.4, storage integrity and retention enforcement, registrable-domain parsing, and the user-control APIs of Section 11.¶
There is no second, independent implementation yet, and therefore no interoperability report; the wire contract in this revision (canonical serialization, key derivation, error ordering) was specified to make an independent implementation and such a report possible.¶
A condensed review aid; section references are normative where they conflict.¶
jti (Section 5.3).¶
ingest scope before any token processing; derives
tenant only from the grant
(Section 6.1).¶
DOMAIN_NOT_VERIFIED)
before consumption; rejects when authorization state
cannot be evaluated in production
(Section 6.2).¶
{"success": false, "error": "<CODE>"};
message-only responses are limited to structural HTTP 400
rejections and internal HTTP 500 errors
(Section 6.4).¶
granted
state, fail-closed CSPRNG-and-digest only
(Section 4.2,
Section 9.1).¶
granted
(Section 9.1).¶
This document has no IANA actions. The error codes of Section 6.4 and the grant scopes of Section 8.3 are defined only within this protocol; no registry is requested at this maturity.¶
This revision aligns the specification with the reviewed reference implementation and with an adversarial security and privacy review of draft-01. Grouped by theme:¶
UNSUPPORTED_VERSION; removed all acceptance of
unsigned tokens and any fallback behavior
(Section 3.4).¶
jti, tenantId,
sourceDomain as first-class signed claims),
constant-time comparison, clock-skew allowance, and size
limits (Section 5.3).¶
Math.random()
fallback; generation now fails closed when
cryptographic primitives are unavailable.¶
customerId body
field, which allowed cross-tenant data poisoning.¶
ingest scope for the destination domain and
re-checks that both signed domains remain authorized
(DOMAIN_NOT_VERIFIED); the customerId
body field is legacy-optional and checked for agreement
rather than trusted
(Section 6.1).¶
error member
(Section 6.4).¶
withdrawn distinct from denied, a
fail-closed initial state, and full purge-on-withdrawal
semantics (Section 9). Removed draft-01's
"anonymous mode": the current protocol performs no event
activity of any kind outside the granted state,
rather than tracking with per-session random
identifiers.¶
_gl, Adobe
adobe_mc), not a new capability class
(Section 1.2).¶
identify()-style linkage
(Section 14.1,
Section 14.4).¶
The values below show the structures at each stage. They are
placeholders for readability -- the hexadecimal and Base64
strings are not outputs of real keys, and this appendix is
not a cryptographic test vector. A verifiable vector can be
produced with the reference implementation by fixing the
token secret, jti, IV, and timestamps.¶
Inner payload before signing (canonical member order):¶
{
"v": 2,
"jti": "8c9f1e2ab34cd56e78f90a1b2c3d4e5f",
"iat": 1787740800000,
"exp": 1787741100000,
"tenantId": "t_1001",
"sourceDomain": "source.example",
"destinationDomain": "destination.example",
"waiTag": "wai_9f86d081884c7d659a2fe_aa32b8b4",
"sessionId": "sess_c1a2b3",
"userId": null
}
¶
Signature input is exactly the JSON serialization above (no
whitespace); the resulting sig (64 lowercase hex
characters) is appended as an eleventh member, and the
plaintext is encrypted with:¶
encKey = HKDF-SHA256(secret, "nylo-wtx1-v2",
"enc|t_1001|destination.example", 32)
macKey = HKDF-SHA256(secret, "nylo-wtx1-v2",
"mac|t_1001|destination.example", 32)
AAD = "WTX1|v2|t_1001|destination.example"
¶
Envelope before Base64 wrapping:¶
{
"v": 2,
"tid": "t_1001",
"dst": "destination.example",
"iv": "u4hnhe0N3RB1lZnw",
"ct": "H1n1...opaque Base64 ciphertext...X8Q=",
"tag": "0T2Yebw2WQFA0mLxu+f0AA=="
}
¶
The token string is the Base64 encoding of that JSON object
and rides the fragment as
#nylo_token=<token>. Note which fields an
observer of the URL can read: v, tid, and
dst only; the WaiTag, session, domains-as-signed,
timestamps, and user id are ciphertext.¶
The honest comparison baseline is the link-decoration family WTX-1 belongs to, plus the browser-native alternatives.¶
| Property | 3rd-party cookies | GA _gl / adobe_mc | Related Website Sets | WTX-1 |
|---|---|---|---|---|
| Cross-domain carrier | Cookie header | URL query parameter | Browser-mediated storage access | URL fragment (not sent in requests) |
| Payload confidentiality in transit URL | n/a | Encoded, readable | n/a | Encrypted; routing metadata only |
| Destination binding / single use | No | No (time-window checks only) | Browser-enforced set membership | Cryptographic binding; atomic single use |
| Consent-gated by design | No (policy overlay) | No (policy overlay) | No (set-level, not user-level) | Yes; fail-closed default off |
| Participant set auditable | No | No | Yes (published sets) | Yes (DNS records + server config) |
| Browser-enforced guarantees | Being removed | None | Yes | None (application layer) |
The last row is the important caveat: WTX-1's properties are application-layer contracts between the participating deployment's components, verifiable by inspection and test, but not enforced by the user agent. Browser-native mechanisms (Related Website Sets [RWS], storage access APIs) provide weaker linkage with stronger enforcement; WTX-1 provides stronger controls with no enforcement beyond the deployment itself. Which is preferable is a policy question this document does not decide.¶
This revision owes its shape to an extensive adversarial security and privacy review of draft-01 and the reference implementation, which identified the cleartext-token exposure, the tenant-spoofing and token-burning issues, the overstated anonymity and compliance claims, and the storage-terminology inaccuracies corrected here. The author thanks the reviewers and the implementers of the test suite that now guards these behaviors.¶