This proposal extends ERC-721 so that a token contract can advertise that its tokens are available as native mobile wallet passes, such as Apple Wallet passes and Google Wallet objects. It defines a passURI view function, a pass manifest that tells clients how to acquire the pass on each platform, events that signal when pass content has changed, and the authorization required before a pass can trigger a state-changing action. passURI is a public view: anyone who can read the chain can read the URI it returns. Where access to a pass is restricted, the restriction is applied only by the off-chain endpoint that URI names, never by the contract. How an issuer generates, signs, and pushes passes is out of scope, beyond the few obligations on that pipeline that the authorization model depends on.
Native wallet passes are a programmable surface that ships by default on nearly every smartphone. A pass can display live state, link into web experiences, and receive platform push updates, all without the holder installing an application. Production systems already bind NFTs to wallet passes (memberships, tickets, loyalty balances, and interactive experiences whose state lives on chain), but each one builds a proprietary bridge. The same seam serves any token whose state a holder would want on a card they already carry, from tickets and memberships today to tokenized real-world assets and credentials. As a result:
A small shared discovery interface lets any client (a marketplace, a wallet app, an email service, a point of sale) offer "Add to Apple Wallet" or "Save to Google Wallet" for any conforming token, the way tokenURI made metadata portable.
This specification has three parts: a discovery interface and pass manifest, a freshness signal, and the authorization a pass-reachable action requires. Only the first two touch the chain; the third exists because a pass is a bearer artifact and any action it can trigger must be authorized independently of it.
The chain carries discovery only. passURI is a public view function with no access control, and nothing a Contract stores or returns is secret. All gating defined in this document, whether of the manifest, of pass acquisition, or of a pass-reachable action, happens at the off-chain endpoint that passURI names and is enforced by the Pass Issuer.
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 RFC 2119 and RFC 8174.
This document binds three targets. Each requirement names the target it binds.
passURI view.passURI, issues passes and acquisition URLs, and verifies the control proof and the entitlement of any pass-reachable action. The verifier named in a challenge is the Pass Issuer acting in that role.passURI, fetches a manifest, offers an add-to-wallet action, or signs a challenge on a holder's behalf, such as a wallet application, a marketplace, an indexer, or a point of sale.The targets conform independently. A Contract conforms by implementing Contract interface. A Pass Issuer conforms by serving the manifest under Pass manifest and operating one of the configurations under Acquisition URLs, including the authorization and refusal rules that configuration carries. A Client conforms by following Client requirements. A Pass Issuer may serve the tokens of any conforming Contract, and a conforming Contract does not depend on any particular Pass Issuer or Client.
A conforming Contract MUST implement the IERC721WalletPass interface and the ERC-165 supportsInterface function, and MUST return true for the interface identifier 0xef5f1e71.
The range in BatchPassUpdate is inclusive of both fromTokenId and toTokenId.
passURI parallels tokenURI: a level of indirection to an off-chain document, because wallet passes are signed platform artifacts that cannot be produced on chain.
The scheme of the returned URI depends on the configuration the Pass Issuer operates (see Acquisition URLs). In the public configuration the Contract MAY return any URI a Client can resolve to the manifest document, and an absolute https URL is RECOMMENDED. In the gated configuration the Contract MUST return an absolute URL with the https scheme, because Gated acquisition is defined in terms of HTTP status codes and request headers, and a Client MUST NOT present a control proof over any other scheme.
The Pass Issuer MUST serve, at the URI returned by passURI, a JSON document (the pass manifest) with the following shape. In the gated configuration defined under Acquisition URLs, the manifest is returned only to a request that carries a control proof; the response to any other request is defined under Gated acquisition.
version is REQUIRED and is the integer 1 for a manifest with the shape defined in this document. Only a revision that changes the meaning or the shape of an existing member uses a different value; adding an optional member or a new format key does not. A Client MUST NOT act on a manifest whose version is present and is not a value it understands, and SHOULD treat a manifest without version as version 1.formats is REQUIRED and MUST contain at least one entry. Each key is a format key naming a pass platform; each value is an acquisition URL for that platform, a URL through which the token's pass on that platform is obtained, either as the pass itself or through the platform's own flow for saving it. An acquisition URL can stop yielding the pass, as described under Acquisition URLs. Every acquisition URL MUST be an absolute URL with the https scheme.apple, whose acquisition URL obtains an Apple Wallet pass, and google, whose acquisition URL obtains a Google Wallet pass. How each platform delivers a pass is described under Format keys, which is informative.updatedAt is OPTIONAL. When present it MUST be a Unix timestamp (integer seconds) of the last content change, and SHOULD change whenever PassUpdate is emitted for the token. It reflects content freshness only and says nothing about whether the acquisition URLs in formats are still valid.issuer is OPTIONAL. When present it is an object whose keys are format keys and whose values are objects of string members that identify the platform signer of that format's passes. For apple the members are teamIdentifier and passTypeIdentifier, the values of the fields of those names that the pass declares; for google the member is issuerId, the issuer id with which the pass's object identifiers begin. Members for other format keys are defined with those keys. A Client MAY compare these with the signer identifiers of a pass it acquires, and SHOULD NOT install a pass whose signer identifiers differ from the manifest's. How a Client obtains these values from a pass is platform-specific.The Pass Issuer MUST serve the manifest with a Content-Type header whose media type is application/json; a charset parameter MAY be present. A Client MUST NOT act on a manifest served with another media type.
A Client running in a web browser, such as a marketplace page, can read the manifest only if the Pass Issuer's responses pass the browser's cross-origin resource sharing (CORS) checks. A Pass Issuer that intends such Clients to resolve the manifest SHOULD send Access-Control-Allow-Origin on manifest, challenge, and refusal responses alike (a refusal the browser cannot read is indistinguishable from an outage), and in the gated configuration SHOULD answer the preflight OPTIONS request with Access-Control-Allow-Methods naming GET and Access-Control-Allow-Headers naming the two proof headers defined under Gated acquisition, and SHOULD send Access-Control-Expose-Headers naming Retry-After, which a browser cannot read across origins otherwise. The value * is appropriate in the public configuration, where the manifest is public data, and is acceptable in the gated configuration because authorization travels in the proof headers rather than in cookies; a Pass Issuer SHOULD NOT send Access-Control-Allow-Credentials: true on these endpoints, so that no ambient credential can be attached to a cross-origin manifest request.
This subsection is informative. It describes what the two format keys defined above mean at their platforms at the time of writing; platform behavior can change, and no requirement of this document depends on the delivery behavior it describes.
apple names an Apple Wallet pass. The URL serves a signed pass file with the media type application/vnd.apple.pkpass, which Apple defines for pass files and which Apple devices rely on to hand the file to Wallet. The file declares teamIdentifier and passTypeIdentifier, and Apple Wallet checks both against the certificate that signed it.google names a Google Wallet pass. The URL is a Save to Google Wallet link, which opens the platform's flow for saving a pass object to the user's account. issuerId is the numeric issuer id that prefixes every object id the issuer account can create.The JSON returned by tokenURI MAY include a top-level wallet_pass property whose value is the pass manifest object defined above. This lets metadata-only consumers surface an add-to-wallet action without an extra fetch. When both are present, the manifest reachable through passURI is authoritative. Because tokenURI is public, the mirror is appropriate only in the public configuration defined under Acquisition URLs; a Pass Issuer in the gated configuration MUST NOT mirror acquisition URLs into metadata.
A Contract MAY commit on chain to the identity of the platform signer its passes come from, so that a Client can check the signer of a pass against something the Contract, not the endpoint, asserts. A Contract that does so MUST implement the following interface and MUST return true from supportsInterface for the interface identifier 0x470be7e9.
passIssuer MUST return a JSON object with the shape of the manifest's issuer member. When a Contract implements this interface, the Pass Issuer MUST include issuer in every manifest it serves for that Contract, equal member for member to the object the Contract returns. A Client MAY read passIssuer, compare it with the manifest's issuer and with the signer identifiers of the acquired pass, and SHOULD NOT install a pass that does not match. Because changing the committed identity is a transaction the Contract's owner sends, a Client that sees a mismatch is looking at either an endpoint serving passes signed by someone the Contract does not name, or an issuer migration the Contract has not yet recorded; both are reasons to stop.
A Pass Issuer SHOULD issue acquisition URLs that are capability URLs: unguessable, high-entropy, and not derivable from public data such as tokenId, pass serial numbers, or metadata fields. When an installed pass exposes links that can trigger state-changing actions, the Pass Issuer MUST issue acquisition URLs that are capability URLs. Action links embedded in an installed pass are capability URLs in this sense and follow the same rules.
A Pass Issuer operates in one of two configurations, and a Client learns which from the manifest response: a manifest returned to a request without a control proof is the public configuration, and the 401 response defined under Gated acquisition is the gated configuration.
passURI is a public view function, its acquisition URLs are public data, one chain read away from anyone able to enumerate token ids: unguessability does not survive publication. A capability URL here is hygiene (it limits scraping and accidental sharing), and a Pass Issuer MUST NOT treat it as a proof of possession or ownership.Where passes expose state-changing links, a Pass Issuer in the public configuration SHOULD also rotate acquisition URLs on transfer, so that a pass held by a previous owner stops granting the new owner's links. A Pass Issuer SHOULD rotate acquisition URLs on explicit owner request, and in the capability configuration defined under Authorization of pass-reachable actions it MUST: under an unchanged owner, rotation on request is the only remedy for a leaked link.
Rotation retires links only if it also replaces the installed passes that carry them. Every copy of an installed pass receives the content the Pass Issuer pushes to it, and on platforms where a saved pass object is shared, every account that saved it does too. Pushing the new links to a pass issued before the rotation therefore delivers them to each copy a leak produced, and after a transfer to the previous owner. When rotation retires links that installed passes carry, the Pass Issuer MUST NOT deliver the new links to those passes: it issues the new links in a new pass, and SHOULD update the passes that carried the old links so that they present as superseded. That content can only arrive over the pass's platform update path, so rotating the secrets that derive links and cutting off that path are separate operations: a Pass Issuer SHOULD keep an installed pass's update path working until the superseded content has been delivered. Where an installed pass fetches updates with a credential the Pass Issuer issued, the Pass Issuer SHOULD keep that credential valid, and where devices register with the Pass Issuer to learn of updates, SHOULD keep those registrations, until the superseded content has been delivered. A pass whose update path is cut first freezes on its last content and presents as current for as long as it stays installed, which is the condition the superseded requirement exists to prevent. A pass presented as superseded SHOULD state why (a transfer, or a rotation the owner requested) and, where its holder may still be the owner, how to obtain the replacement, and SHOULD NOT render action links. Device wallets generally do not let an issuer remove an installed pass, so the superseded pass remains until its holder removes it; its content is what tells the holder the token has not been lost.
Rotation is not synchronous with the on-chain transfer. A Pass Issuer observes a transfer through indexing or at the new owner's first claim, so between the transfer and rotation the previous owner's acquisition URLs remain cryptographically valid. The fresh entitlement read required under Authorization of pass-reachable actions is the control that refuses, in that interval, the actions those URLs reach; a Pass Issuer in the gated configuration SHOULD take the same read when resolving an acquisition URL to a pass file, so that a sold token's pass stops downloading as well. Rotation closes the residual window rather than providing the boundary.
Acquisition URLs MAY be short-lived. A Save to Google Wallet link, for example, is a signed JWT that issuers commonly mint on demand and allow to expire. The durable handle for a token's pass is the URI returned by passURI; the acquisition URLs inside the manifest are not durable.
So that a Client can acquire a pass from a gated manifest without out-of-band knowledge, a Pass Issuer in the gated configuration MUST behave as follows.
rate_limited, be answered with HTTP status 401 and a JSON body whose error member is "proof_required" and whose challenge member is the URI of the challenge endpoint for that token. The challenge member MUST be either an absolute URL with the https scheme, or a relative reference (RFC 3986) that resolves against the manifest URL to a URL on the manifest URL's origin. A Client resolves a relative reference against the manifest URL, and MUST NOT request a challenge from a URL whose scheme is not https. The body MUST NOT contain acquisition URLs. Any other 401 answer to the manifest request SHOULD carry the same challenge member, since the Client's next step is the same fresh challenge.address query parameter, and MUST return a JSON body whose message member is a challenge for that account that meets the floor defined under Authorization of pass-reachable actions, with the action identified as urn:wallet-pass:action:acquire. Because the nonce is single-use, every challenge the endpoint issues MUST carry a fresh nonce; a request refused as rate_limited issues none. A request whose address is missing or is not a valid account address MUST be refused with HTTP status 400.Wallet-Pass-Proof, the UTF-8 bytes of the signed message encoded as base64url without padding (RFC 4648, Section 5), and Wallet-Pass-Signature, the signature as a 0x-prefixed hex string. A Client MUST omit padding, and a Pass Issuer SHOULD also accept a Wallet-Pass-Proof value that carries padding. Encoding the message keeps the multi-line challenge inside a header and keeps manifest resolution a GET, although not a safe or idempotent one: the request spends a nonce and, on a first claim, issues URLs, so a user agent that retries it on its own meets nonce_invalid, and a Client that receives nonce_invalid fetches a fresh challenge. The header names carry no X- prefix, per RFC 6648. A Pass Issuer MAY also accept the names X-Wallet-Pass-Proof and X-Wallet-Pass-Signature. Each name form is a pair: a request that mixes the forms is refused as invalid_request, and when a request carries both complete pairs the Pass Issuer uses the unprefixed one.acquire proof MUST NOT authorize any other action, and a proof for any other action MUST NOT resolve the manifest. A proof that fails any floor check MUST be refused. A proof that verifies but comes from an account that is not entitled MUST be refused with HTTP status 403, and that status MUST NOT be used for any other refusal, so that a Client can tell an unusable proof from a refused account. A verified request MUST be answered with the manifest and the header Cache-Control: no-store.ownerOf entitlement the set has at most one member, so this rule reduces to rotating when a different account claims.Every artifact this standard describes (the pass file, the acquisition URL, the manifest) is a bearer artifact, and authorization never lives in the artifact. An action that mutates chain state and is reachable from a pass is authorized by two checks, each closing a hole the other cannot reach:
ownerOf) at the time of the request, not at pass issuance and not at URL minting. A Pass Issuer SHOULD take this read at the newest block its node has, the latest block tag of an eth_call, and SHOULD NOT take it at a safe or finalized block. The trade-off is explained under Rationale.A Pass Issuer MUST perform the fresh read for every state-changing action: it closes the transfer window, so a sold token stops acting immediately regardless of how many valid-looking passes remain installed. A Pass Issuer MUST require the control proof wherever the product prompts for a per-action signature when a pass link is followed: it closes forwarding, the case rotation cannot reach, in which a pass or URL leaks while ownership is unchanged. Where the product does not prompt for a per-action signature when a pass link is followed, check (1) is instead met by the capability configuration described below, sound only under the conditions stated there.
Each challenge field closes its own hole. The token and action scope the proof, so a proof obtained for one action cannot be presented for another. The single-use nonce makes a captured proof worthless a second time. The expiration makes a leaked proof go stale. The verifier identity keeps a challenge signed for one issuer from being presented to another that gates the same token. Because the nonce is single-use and issued by the verifier, every conforming flow includes a server round trip; a flow that does not adopt the RECOMMENDED serialization avoids parsing a message format, not the round trip.
A repeat of an identical state-changing request is not a replay. Device wallets and in-app browsers retry a request the device abandoned, so a verifier can receive the same presentation twice for one tap: the same proof, or the same capability URL, with the same body. A Pass Issuer SHOULD answer a repeat that is identical to a request it is still processing, or that it completed within a short window, with that request's outcome, and SHOULD NOT execute the action a second time or refuse the repeat as nonce_invalid; a holder whose action landed would otherwise be told it failed. This rule covers state-changing actions only: a repeated manifest request is never answered from a record of an earlier outcome, because its proof is single-use and, in the gated configuration, the acquisition URLs it would return are possession proofs, so a captured proof MUST NOT yield them a second time. The fresh entitlement read is part of what makes two requests identical: a Pass Issuer MUST take it before consulting any record of earlier outcomes, and MUST treat a repeat as a new request once that read no longer names the account the first request was authorized for, so that the window never answers a transfer with a recorded success. On the signed path, a presentation that differs from the first in any field, or that arrives after the window, meets the nonce store and is refused. A capability link followed after the window is a new request: capability links are reusable by design, and the fresh read judges each presentation.
A Pass Issuer MAY extend entitlement to accounts other than the owner, for example a rental user designated per ERC-4907 userOf, or a delegate authorized through an on-chain delegation registry, provided the entitlement policy is documented and every extended entitlement is read with the same request-time freshness required of ownerOf, never cached from pass issuance or URL minting. The claimed account named in the challenge is then tested against the documented entitlement rather than bare ownerOf equality, and each entitled account receives its own acquisition URLs, retired when that account's entitlement ends, per Gated acquisition.
Extension raises a question the single-owner model never had to answer: during an active rental both the owner and the rental user are live accounts, so a policy that merely adds accounts lets both act at once. A Pass Issuer's documented policy MUST therefore define precedence. A rental entitlement (an active ERC-4907 userOf) SHOULD be exclusive of the owner for the actions it covers, while a delegation is additive by intent. Absent a documented extension, ownerOf is the entitlement and the fresh read of it is the default requirement. ERC-4907 is cited here as an example only: nothing in this document requires a Contract to implement it or a Pass Issuer to read it, and a Pass Issuer that extends entitlement through some other documented mechanism conforms equally.
The following is a conforming Sign-In with Ethereum challenge for the feed action on token id 412 of an example ERC-721 contract on chain id 1, issued by issuer.example.
Each floor field has a fixed place in the message:
urn:wallet-pass:action:feed, and is restated in plain language in the statement line.Nonce field and the expiration is the Expiration Time field. A challenge MAY also carry a Not Before field; a Pass Issuer that issues one MUST refuse a proof presented before that time, as not_yet_valid under Refusal responses.domain on the first line.address line. A contract account verified per ERC-1271 uses the same message, with the claimed account in that same line.Both resource entries are valid RFC 3986 URIs, as ERC-4361 requires of the resources list. The example timestamps omit fractional seconds; RFC 3339 permits them, and a message whose timestamps carry them (for example 2026-08-07T15:04:05.000Z, the form common signing libraries emit) conforms identically.
An installed pass is a static artifact. Its fields are fixed at issuance and change only through the platform's update mechanism, so a pass cannot carry a single-use nonce or a short expiration. In an email-onboarded custodial or relayer product the signing key is typically held by the server or an embedded signer rather than prompted per action, so no per-action user signature exists when a pass link is followed. A challenge, when one is used, is therefore fetched from the verifier at action time; the link embedded in the pass only reaches the flow and never carries the proof. The boundary between the two paths is therefore not what a pass can carry but whether the product prompts: a pass link can always open a page that asks for a signature, and a product that does so is on the signed path. The capability configuration is not a third configuration beside public and gated: it is the gated configuration operated so that the capability URL carries the possession role.
In such a deployment a Pass Issuer MAY let the capability URL stand in for check (1). The substitution is explicitly weaker than a per-action signature and is permitted only when all of the following hold:
Check (2) is never substitutable. No configuration, capability or signed, may omit or defer the fresh entitlement read.
What the substitution gives up: with no per-action signature, forwarding is not closed. Any party holding the capability URL can trigger the bound action while ownership is unchanged. Unguessability does not help once the URL has leaked, rotation does not fire absent a transfer, and the fresh entitlement read refuses a sold token, not a forwarded URL under an unchanged owner. The configuration accepts this residual and bounds it. Because a capability link is reusable, the worst case under forwarding is the bound action repeated until something rotates the link, and under an unchanged owner only the owner can cause that rotation; the conditions above fix what the repetition can do (it cannot change ownership, entitlement, or approvals, and its total effect is bounded and documented) and guarantee the owner the remedy. An action whose repeated effect cannot be bounded that way MUST NOT be reachable through a capability link and requires the signed challenge.
The verifier-identity field of a signed challenge and the issuer binding of a capability URL are different properties. The verifier-identity field stops a captured signature from being presented to a different verifier that gates the same token. A server-held capability URL has no user signature to lift; its binding to the issuer is structural (the URL resolves only at the issuer's endpoint), not signed.
The identifier a capability URL binds MAY be a server-resolved identifier, such as a pass serial, provided it maps deterministically and uniquely to a single on-chain token and cannot be re-bound to another token. The explicit chain id, contract address, and token id triple is RECOMMENDED for capability bindings, and remains REQUIRED in a signed challenge.
So that a Client can tell a holder what went wrong and whether to try again, a Pass Issuer's JSON response that refuses a manifest request, a challenge request, or a state-changing action SHOULD carry an error member from the following list with the listed HTTP status. The proof_required response of Gated acquisition is REQUIRED as specified there.
error | Status | Meaning | Retry |
|---|---|---|---|
invalid_request | 400 | The request is malformed (for example a missing or invalid address) | After correcting the request |
invalid_message | 400 | The presented message is not a parseable challenge, or exceeds the size bound | With a fresh challenge |
domain_mismatch | 400 | The challenge names a different verifier | With a fresh challenge |
binding_mismatch | 400 | The proof or link does not bind this exact token and action | With a fresh challenge |
proof_required | 401 | No control proof was presented | With a fresh challenge |
nonce_invalid | 401 | The nonce is unknown or already spent | With a fresh challenge |
challenge_expired | 401 | The challenge's expiration time has passed | With a fresh challenge |
not_yet_valid | 401 | The challenge carries a not-before time that has not been reached | After that time, or with a fresh challenge |
signature_invalid | 401 | The signature is not valid for the claimed account | With a fresh challenge |
not_owner | 403 | The proof is valid but the account is not entitled (fresh read) | No |
unknown_capability | 404 | The capability URL does not resolve, for example after rotation | No |
insufficient_balance | 409 | An account the token controls holds less than the action spends | After the account is funded |
rate_limited | 429 | Too many attempts for this token or action | After Retry-After |
read_failed | 503 | The fresh entitlement read could not be taken | With a fresh challenge, after Retry-After |
Responses with status 429 or 503 SHOULD carry a Retry-After header. A Pass Issuer SHOULD NOT use a different code for a condition this list covers, and MAY use additional codes for conditions it does not; a Client MUST handle an unrecognized code by its HTTP status. A response rendered for a person, such as a page opened from a pass link, SHOULD state the same condition in words, and SHOULD NOT present a condition marked "No" or "After the account is funded" as one that a retry will fix.
PassUpdate whenever on-chain state rendered on the pass changes, and a Pass Issuer SHOULD push updated content to installed passes through the platform update mechanism where available. A Pass Issuer SHOULD NOT push to a pass whose content is unchanged, and, where devices fetch updated content from the Pass Issuer, SHOULD evaluate If-None-Match and If-Modified-Since on a fetch per RFC 9110, Section 13, and answer with status 304 (Not Modified) (RFC 9110, Section 15.4.5) when the content is unchanged: platforms can throttle, and can disable, automatic updates for an issuer whose passes update too often or without change, and a pass that has stopped updating presents stale state as current.Referrer-Policy: no-referrer, a noindex robots directive (the X-Robots-Tag header or the equivalent meta element), and Cache-Control: no-store, so that the link does not leak through the Referer header of anything the page loads or links to, is not indexed if it leaks, and is not retained by a shared cache.rate_limited and a Retry-After header. Each of these endpoints does work for a requester it has not yet authenticated: a nonce issued and stored, a signature verified, a chain read taken. The limit SHOULD be keyed by the requesting client and the token together, not by network address alone: many honest holders share one address behind a campus, office, or venue network or a carrier's address translation, each presenting a different token, while the request pattern a limit exists to bound is many requests for one token from one place. A limit keyed by address alone refuses the crowd and admits the attacker who spreads across addresses.https. This applies to every redirect a Client follows: each hop is checked as the first URL was. The manifest is issuer-asserted, so a Client that follows any URL it carries runs issuer-chosen content in its own context.URI indirection instead of on-chain pass data. Apple Wallet passes are signed bundles produced with issuer certificates; Google Wallet passes are objects registered through an issuer account. Neither can be constructed on chain, and both change far more often than token metadata (countdowns, balances, standings). A tokenURI-style endpoint is the only shape that fits every platform.
A manifest instead of format query parameters. One JSON manifest tells the client everything available for the token in a single request and extends to future platforms by adding keys. Query-parameter negotiation was rejected because it makes capabilities undiscoverable without probing.
A dedicated PassUpdate event instead of reusing ERC-4906. MetadataUpdate signals that tokenURI content changed and is consumed by NFT indexers. Pass content routinely changes when metadata does not (for example a countdown rendered on the pass), and its consumers are pass distributors rather than indexers. A Pass Issuer that mirrors the manifest into metadata can have the Contract emit both events.
Two checks, not one. Rotation feels like leak coverage but is only transfer coverage: a URL forwarded under an unchanged owner never triggers it. A fresh read alone refuses a sold token but not a forwarded one. Requiring both, and naming what each closes, keeps Pass Issuers from mistaking either for the whole boundary.
The fresh read is taken at latest. The read exists to shorten the window in which a sold token keeps acting for its previous owner. A read at a safe or finalized block is immune to reorganization but lags the chain head by a fixed delay (two epochs, about thirteen minutes, for finalized on Ethereum mainnet; typically longer on rollups, whose safe head follows the parent chain), and that delay is added to the window after every transfer. A read at latest shortens the window to one block, at the price that a reorganization can discard the block the read saw: a transfer observed at latest and then reorganized out would let the new owner's action through on a chain where the sale never happened. That failure needs a reorganization deeper than the action's own confirmation, is rare on chains with fast finality, and affects one action; the lag of a safe read affects every transfer, every time. The proposal therefore recommends latest and discloses the reorganization residual under Security Considerations.
A mandated challenge floor with ERC-4361 as the recommended serialization. The security of the control proof lives in the challenge fields, so this proposal mandates the fields rather than a message format. Sign-In with Ethereum is the serialization of exactly those fields that wallets and libraries already emit and parse, so recommending it buys interoperability at near zero cost, while a flow that cannot parse the format still meets the same floor.
A disclosed capability configuration instead of silence. Consumer pass products commonly onboard by email and hold keys server-side, so a per-action user signature does not exist on the pass path. Without a defined substitute, every such product would be non-conforming by construction, or would claim a signature-equivalent guarantee it does not have. The capability configuration permits the substitution under stated conditions and states its cost, and the conditions give "tolerable" a floor (no change of ownership, entitlement, or approvals; a bounded, documented effect under repetition; rotation on the owner's request) rather than leaving the judgment to each implementer.
A shared refusal vocabulary. A pass link opens wherever the holder taps it, often in a client that did not build the pass, so the client can only explain a refusal it can name. The codes are the ones the reference implementation already returns, plus two a deployment met in practice (an empty token account and a throttled link) and one a signing library needs (a challenge presented before its not-before time), and each states whether a retry can help, because a holder told to "try again" about a condition no retry can change keeps retrying while the token's state runs down.
Delivery pipeline out of scope. How device registration, platform push (APNs and the Google Wallet API), action links, and pass art are built is platform- and vendor-specific. Standardizing the construction would freeze implementation detail without improving interoperability; discovery and authorization are the interoperable seams. What this document does state are the observable obligations of that pipeline that the security of the seams depends on: a superseded pass stays reachable, registrations, where devices register with the Pass Issuer, are bounded and superseded rather than deleted, unchanged content is not pushed, and a GET on a link performs nothing.
This proposal is a pure extension of ERC-721 and changes no existing behavior. Contracts that do not implement it are unaffected, and the metadata mirror is ignored by clients that do not understand it. The interface can be implemented alongside other extensions (enumerable, royalty, token-bound accounts) without interaction. Manifests and requests shaped by earlier revisions of this document remain readable: a manifest without version is treated as version 1, and a Pass Issuer may accept the X- prefixed proof header names those revisions used, and is expected to accept a padded Wallet-Pass-Proof value.
Application to ERC-1155 is out of scope: fungible balances and multi-holder semantics require a different binding between holder and pass. A future extension would bind a specific account alongside the token id and test balanceOf(account, id) against a minimum holding, which defaults to 1, in the authorization check, keeping multi-holder semantics out of the pass artifact.
The reference implementation carries executable tests. The contract suite asserts that type(IERC721WalletPass).interfaceId equals 0xef5f1e71 and covers passURI and both events. The action suite and the manifest suite exercise the authorization floor one property at a time: a replayed nonce, an expired challenge, a different verifier, a proof for another action or token, a proof scoped to a chain or contract the verifier does not serve, a malformed signature, a signature from the wrong account, and an ownership change between challenge and action are each rejected, and an ownership read that cannot be taken is refused as retryable rather than as a refusal of the account; a gated manifest is refused without a control proof and its response points at the challenge endpoint, whose acquire challenge then resolves the manifest; acquisition URLs rotate on a new owner's first claim and not on a repeat claim; and a rotated capability URL stops resolving. The capability suite puts the capability configuration on the same footing, one test per condition: a link bound to one action is refused for another, a link rotated on transfer or on the owner's request is refused, a live link behind a sold token is refused by the fresh read, a failed read is answered as retryable, no action link exists in the public configuration, a GET on a link is side-effect free, and a forwarded link under an unchanged owner succeeds, asserted deliberately as the residual the specification discloses. The repeat suite covers the rule that a repeat of an identical request is not a replay: two identical signed requests arriving together execute once and receive one outcome, an identical repeat inside the window receives the first outcome rather than nonce_invalid, a repeat after the window meets the nonce store, a spent nonce in a request that differs is refused, a repeated refusal is repeated, a capability link followed twice on one tap acts once while a link that has rotated is refused regardless of the window, a repeat after the token changed hands is judged afresh and refused, a repeated manifest request is never answered from a record, a read_failed retry needs a fresh challenge, and a rotation request that landed is reported as landed when the device resends it. The manifest suite also covers a contract account under ERC-1271 through an injected verifier, a mixed pair of proof header names refused as invalid_request, and the first claim re-issuing links minted before any claim.
A reference implementation is included in the assets directory under CC0. It consists of a minimal ERC-721 implementing the interface (ERC721WalletPass.sol) and an off-chain server that issues and verifies ERC-4361 challenges against the floor (authorize.ts), serves the manifest in the public and gated configurations, rotates capability URLs on transfer and on the owner's signed request (passStore.ts), and authorizes actions reached through capability links under the capability configuration's conditions (capability.ts).
A non-normative appendix of platform notes records the Apple Wallet and Google Wallet behavior that shaped the requirements above (push, notifications, shared objects, superseded passes, field limits), as met by one production deployment, and the lessons that deployment learned. It names no deployment and carries no deployment-specific detail.
Passes are bearer artifacts. A pass file can be forwarded and installed on any number of devices, and platforms generally do not enforce uniqueness. Where a platform offers a single-holder setting that lets one account save a pass object and refuses every other, it bounds who can save, not what a screenshot or a forwarded pass already saved reveals, and the analysis here still applies. Treating possession as ownership creates a confused deputy in which anyone holding a shared pass exercises the owner's privileges. The two checks under Authorization of pass-reachable actions are not redundant: the fresh read closes the transfer window, the control proof closes forwarding, and the challenge fields close replay and cross-verifier reuse. A Pass Issuer that skips either check reopens the hole only that check closes. The capability configuration gives up forwarding protection by design, as the specification states, and in exchange bounds what a forwarded link can do to a repeatable, documented effect that cannot change ownership or entitlement, with rotation on the owner's request as the remedy.
Freshness is bounded, not absolute. The fresh read narrows the transfer window to the interval between the read and the action, and a read is only as fresh as the chain head it reflects: a lagging node can serve a stale head, and because the read is taken at latest a reorganization can discard the block it saw. This residual window is accepted risk of the design; the Rationale explains why a lagging safe read would widen it for every transfer rather than narrow it.
Derivable links. A pass link or download URL keyed only by public data, such as a token id or pass serial, lets anyone who can read the chain obtain the pass or trigger its actions. The requirement that such URLs be capability URLs exists to rule this out, and it applies to pass download URLs as much as to action links.
Capabilities readable from the store. A capability that is stored as issued, or derived from stored values alone, is recoverable by anyone who can read the Pass Issuer's store, so a leak of the store yields every link and every pass download without a single request being made. The Pass Issuer requirements therefore call for a store that yields no usable capability on its own, by digest or by a derivation secret held outside it. A derivation secret adopted after passes are in circulation invalidates every capability issued without it and so requires every installed pass to be replaced, which is why the requirement puts it in place before the first pass is issued.
Capability URL leakage. Acquisition URLs and action links appear in emails, chat messages, and device backups. Entropy limits guessing and rotation limits the damage window of a leak, but a URL published in a public-configuration manifest is public data regardless of entropy. Durable client caching would widen exposure and surface URLs that have expired or rotated. Rotation closes a leak only when the new links reach the holder in a new pass rather than as an update to the leaked one, which is why rotation replaces installed passes.
Capability links are visible on screen. A link on a pass, on its back or as an action card beneath its face, is shown to whoever sees the pass: in a screenshot the holder shares, in a screen recording, and on the lock screen when the platform surfaces the pass at its relevant time. A link seen that way is leaked, and no transport protection helps. The capability configuration bounds what a leaked link can do and gives the owner rotation as the remedy; a Pass Issuer can further limit the spread by keeping link text short and unrevealing, by serving the pages such links open with Referrer-Policy: no-referrer and a noindex directive so that one leak does not propagate through referrers and search indexes, and by rate-limiting link resolution so that a leaked link cannot be hammered. The Pass Issuer requirements admit the platform controls that refuse screenshots or require unlock where they exist, and the Client requirements ask that capability URLs never be displayed as text.
Push fan-out. On a platform where devices register with the Pass Issuer for a pass's updates, anyone holding a forwarded pass can register a device, and each update costs the issuer one platform push per registered device. Unbounded registrations let a single pass holder make every update for that pass arbitrarily expensive. The Pass Issuer requirements therefore bound registrations and validate device identifiers, refuse at the bound rather than evict (eviction lets a forwarded copy push the holder's own devices out), and supersede registrations at rotation rather than delete them, so that the superseded content still reaches the devices that hold the old pass; deleting a registration races the device's next fetch, and a pass whose registration is gone never learns it was superseded.
Message size. A verifier parses a presented challenge before it knows who sent it, and message parsers can be superlinear on adversarial input. The Pass Issuer requirements bound the size of a presented message before it is parsed. A conforming challenge is small: a domain, an account, a nonce, a few timestamps and two resource URIs.
Authority held by an account the token controls. A token may control an account, such as a token-bound account, that holds the balance its pass actions spend. The account moves with the token, but authority granted from it does not reset: a token allowance, an operator approval, or a session key granted from the account under one owner stays usable after the token is sold, so a previous owner who granted it keeps a claim on what the new owner's account holds. The fresh entitlement read does not reach this, because the authority is exercised against the account directly and not through the issuer. This is why the capability configuration forbids actions that grant such authority, and why issuers are asked to refuse standing approvals from these accounts or revoke them on transfer.
Stale passes after transfer. Without invalidation or push, a previous owner's installed pass keeps displaying state and may still render action links. Until the Pass Issuer observes the transfer, that owner's capability URLs also remain valid; the fresh read, not rotation, refuses the actions they reach in that interval, and the pass download too where the Pass Issuer takes the read on acquisition URL resolution. The same staleness arises with no transfer at all when a Pass Issuer cuts off a pass's update path before the superseded content reaches it, for example by rotating the pass's update credential together with the link secrets on an owner's reset: the installed pass then shows its last state as current, with nothing on it to say so, for as long as it stays installed. Acquisition URLs keeps rotating the link secrets separate from cutting off the update path for this reason.
Phishing surface. Pass fields carry arbitrary links and text under issuer branding. Presenting the issuing contract address in clients and serving pass endpoints from an origin consistent with the collection are the mitigations this proposal specifies.
The manifest is issuer-asserted. The only chain-asserted facts are that the token contract designated the endpoint through passURI and, where the Contract implements the issuer identity commitment, which platform signer its passes come from. Apple pass files are signed under the issuer's Apple certificate, and Google Wallet saves only objects under an issuer id the signing account is authorized for, but nothing else in the manifest JSON is bound to chain state, so a Client can verify that the manifest was reached through the contract's passURI and, with the commitment, that the pass it installs carries the signer identifiers the Contract names. Without the commitment, an endpoint that has been taken over, or a look-alike endpoint a Client was misdirected to, can serve a pass signed under anyone's credentials and the Client has nothing to check it against.
Privacy. Pass delivery involves device push registrations and often email addresses. Identifiers derived from them would leak through forwarded passes and backups, which is why the issuer requirements call for random identifiers free of personal information.
Endpoint availability. The manifest endpoint is off-chain infrastructure. Its failure affects pass delivery and freshness only; ownership and transferability of the token never depend on it.
Copyright and related rights waived via CC0.