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.
Motivation
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:
Wallets, marketplaces, and indexers cannot discover that a token has a pass representation, so the capability stays invisible outside the issuer’s own site.
Pass distributors have no shared signal that a token’s pass content is stale.
Each integration re-derives the same security decisions (how pass links relate to ownership, what happens on transfer), with uneven results.
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.
Specification
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.
Conformance targets
This document binds three targets. Each requirement names the target it binds.
Contract: an ERC-721 token contract that implements the interface under Contract interface. Its obligations are on chain only: the interface, the events, and the passURI view.
Pass Issuer: the off-chain server that serves the manifest reachable through 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.
Client: software that resolves 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.
Contract interface
A conforming Contract MUST implement the IERC721WalletPass interface and the ERC-165supportsInterface function, and MUST return true for the interface identifier 0xef5f1e71.
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.0;/// @title ERC-721 Wallet Pass Extension
/// @dev The ERC-165 identifier for this interface is 0xef5f1e71.
interfaceIERC721WalletPass{/// @notice Emitted when the wallet pass content bound to a token has
/// changed. Pass Issuers SHOULD regenerate the pass and, where the
/// platform supports it, push the update to installed passes.
eventPassUpdate(uint256indexedtokenId);/// @notice Emitted when the wallet pass content for a consecutive range
/// of tokens has changed.
eventBatchPassUpdate(uint256fromTokenId,uint256toTokenId);/// @notice Get the pass endpoint URI for a token.
/// @dev Throws if `tokenId` is not a valid token. The returned URI MUST
/// resolve to a pass manifest as defined in this standard.
/// @param tokenId The token whose pass endpoint is requested.
/// @return The pass endpoint URI.
functionpassURI(uint256tokenId)externalviewreturns(stringmemory);}
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.
Pass manifest
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.
This document defines two format keys: 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.
Additional format keys MAY be present. A Client MUST ignore format keys it does not recognize.
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.
Format keys
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.
Metadata mirror (OPTIONAL)
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.
Issuer identity commitment (OPTIONAL)
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.
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.0;/// @title ERC-721 Wallet Pass Extension, issuer identity commitment
/// @dev The ERC-165 identifier for this interface is 0x470be7e9.
interfaceIERC721WalletPassIssuer{/// @notice The platform signer identity the pass manifests of this
/// contract carry, as the JSON `issuer` object defined under
/// Pass manifest.
functionpassIssuer()externalviewreturns(stringmemory);}
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.
Acquisition URLs
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.
Public configuration. The manifest is served without access control. Because 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.
Gated configuration. Manifest resolution or pass acquisition is gated behind proof of control of the owning account. Only in this configuration may possession of an acquisition URL serve as a possession proof, and the Pass Issuer MUST rotate acquisition URLs upon observing a transfer or upon the new owner’s first claim, whichever comes first.
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.
Gated acquisition
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.
A request for the manifest that carries no control proof MUST, unless refused as 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.
The challenge endpoint MUST accept a GET request carrying the claimed account in the 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.
To resolve the manifest, the Client repeats the manifest request with two headers: 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.
The Pass Issuer MUST check the proof against the floor and MUST take the fresh entitlement read of check (2). An 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.
Acquisition URLs are issued per entitled account, and rotation is keyed by account. The Pass Issuer keeps, for each token, the set of accounts it has issued acquisition URLs to. Acquisition by a proven, entitled account that is not in that set is that account’s first claim: the Pass Issuer MUST issue acquisition URLs to that account before the manifest is returned, and MUST NOT retire the URLs of any other account that is still entitled. When the Pass Issuer observes that an account in the set has lost entitlement (a transfer, the end of a rental, the revocation of a delegation), it MUST retire that account’s URLs, per the rotation requirement above; a transfer retires the URLs of the previous owner and of every account whose entitlement derived from that owner. A rotation requested by an entitled account retires that account’s URLs, and the owner MAY also retire every account’s URLs. Because rotation is keyed by account, an owner and a delegate who claim in turn do not retire each other’s passes, and under the default ownerOf entitlement the set has at most one member, so this rule reduces to rotating when a different account claims.
Authorization of pass-reachable actions
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:
Proof of control of the owning account: a signature over a server-issued challenge. The challenge MUST name the token (chain id, contract address, and token id), the requested action, a single-use nonce issued by the verifier, an expiration time, and the identity of the verifier, and the verifier MUST check every one of these fields before acting, each against what the verifier itself holds (the token it serves, the nonce it issued, its own identity, and its own clock) and never against values supplied with the request. Where the signature alone does not identify the signing account, as with contract account signatures verified per ERC-1271, the challenge MUST also name the claimed account. A Pass Issuer MUST accept a signature produced by the claimed account’s key when the account is externally owned, MUST accept a signature that the claimed account validates per ERC-1271 when the account is a deployed contract, and SHOULD accept a signature wrapped per ERC-6492 when the claimed account is a counterfactual contract account that has not yet been deployed, since an email-onboarded holder’s account commonly exists only as a deterministic deployment until its first transaction. An ERC-1271 or ERC-6492 validation is a chain read and MUST be taken on the chain the challenge names, with the same freshness required of the entitlement read. Challenges SHOULD be serialized as Sign-In with Ethereum messages per ERC-4361: a conforming challenge already carries every field in which that format’s security lives, and wallets and signing libraries already parse the format, for externally owned accounts and contract accounts alike. Accounts created from an email address can satisfy this check without exposing key material to the user, through an embedded signer or a contract account.
A fresh on-chain read of ownership (for ERC-721, 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.
Extended entitlement
A Pass Issuer MAY extend entitlement to accounts other than the owner, for example a rental user designated per ERC-4907userOf, 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.
Example challenge
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.
issuer.example wants you to sign in with your Ethereum account:
0x2B7E9A4c1F0d8e63A5b2C4D6E8F0A1b3C5d7E9F2
Authorize the feed action for wallet pass token 412 on issuer.example.
URI: https://issuer.example/wallet-pass/actions
Version: 1
Chain ID: 1
Nonce: Xq3F9kP2mR7tW1Zb
Issued At: 2026-08-07T15:04:05Z
Expiration Time: 2026-08-07T15:09:05Z
Resources:
- eip155:1/erc721:0x5F9B5a1cdED9d6B3f5E8a2C47B0e13d6A8F4c2e1/412
- urn:wallet-pass:action:feed
Each floor field has a fixed place in the message:
The token is the first resource, a CAIP-19 asset identifier carrying chain id, contract address, and token id together.
The action is the second resource, urn:wallet-pass:action:feed, and is restated in plain language in the statement line.
The single-use nonce is the 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.
The verifier identity is the domain on the first line.
The claimed account is the 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.
The capability configuration
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:
the deployment operates the gated configuration defined under Acquisition URLs;
the URL is an unguessable, high-entropy capability;
the URL is bound to the specific token and action it reaches, and to the account it was issued to, which check (2) tests as it tests the claimed account of a signed challenge;
the URL rotates on transfer per Acquisition URLs (upon observation of the transfer or the new owner’s first claim, not synchronously), and MUST also rotate on the owner’s request;
the reachable action MUST NOT transfer, burn, or approve the token, MUST NOT change who is entitled to it, and MUST NOT grant an approval or other standing authority from an account the token controls;
the effect of the action under unlimited repetition MUST be bounded, including any value it moves from the token or from an account the token controls, and the Pass Issuer MUST document that bound;
the authority the link exercises SHOULD be limited on chain to the function the link invokes, for example through a session key scoped to that function; and
check (2), the fresh entitlement read, remains in force unconditionally.
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.
Refusal responses
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.
Pass Issuer requirements
A Contract SHOULD emit 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.
On transfer of a token, a Pass Issuer SHOULD update, invalidate, or visibly mark as superseded the passes issued to the previous owner, so that an installed pass does not continue to present itself as current. A pass marked as superseded SHOULD say why, per Acquisition URLs.
A wallet pass is a projection of the token, not the token. A Pass Issuer MUST NOT treat possession of a pass file, or the ability to add it to a device wallet, as proof of ownership of the underlying token, and MUST NOT authorize a state-changing action by pass possession alone. This holds in the capability configuration as well: there the capability URL carries the possession role under the stated conditions, and the fresh entitlement read of check (2) is still required.
When a state-changing action is refused, a Pass Issuer SHOULD tell the holder whether a retry can succeed, using the codes under Refusal responses. A chain read that could not be taken is retryable. A condition a retry cannot change, such as an account the token controls holding less than the action spends, SHOULD be reported as a refusal that names the condition, so that the holder is not left retrying an action that cannot succeed.
Where a token controls an account, a Pass Issuer SHOULD refuse standing approvals granted from that account, or revoke them on transfer, per Authority held by an account the token controls.
A Pass Issuer SHOULD serve pass endpoints from an origin consistent with the collection’s published web presence.
A Pass Issuer SHOULD serve every page reached through a capability link with 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.
A Pass Issuer SHOULD rate-limit challenge issuance, manifest resolution, and capability-link resolution, per requesting client and per token, and SHOULD answer excess requests with 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.
A read of the Pass Issuer’s store alone SHOULD yield no usable capability: the store holds only a one-way digest of each capability and is looked up by digest, or capabilities are derived from the stored value with a secret held outside the store, and that secret is in place before the first pass is issued. See Security Considerations.
Where devices register with the Pass Issuer to learn of a pass’s updates, the Pass Issuer SHOULD bound the number of device registrations kept per pass and SHOULD validate device identifiers before storing them. At the bound it SHOULD refuse the new registration rather than evict an existing one, and at rotation it SHOULD mark registrations superseded rather than delete them. See Security Considerations.
A Pass Issuer SHOULD bound the size of a presented challenge before parsing it.
Where a platform offers controls that refuse screenshots of a pass or require the device to be unlocked before the pass is shown, a Pass Issuer MAY use them; they exist on some platforms only and do not substitute for the bound and the rotation remedy.
A GET on a capability link MUST NOT perform the action. Mail scanners, chat previewers, and wallets prefetch links; a Pass Issuer performs the action only on a POST, and MAY answer the GET with a description of the link or a page that submits the POST.
A Pass Issuer SHOULD NOT embed holder personal information in pass identifiers or pass-visible fields. Platform object identifiers SHOULD be random rather than derived from email addresses or account identifiers.
Client requirements
A Client SHOULD fetch the manifest at the moment of the user’s add-to-wallet action, and MUST NOT durably cache acquisition URLs.
A Client MUST NOT navigate to or fetch an acquisition URL whose scheme is not 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.
A Client that signs a challenge on the holder’s behalf SHOULD first check that the verifier identity it names is the host to which the proof will be presented, and that its resources name exactly the requested token and action. A Client that signs whatever a challenge endpoint returns can be led to sign a proof that is valid at a different verifier gating the same token.
A Client that offers add-to-wallet actions for arbitrary conforming tokens SHOULD present the issuing contract address alongside the action.
A Client that renders a pass SHOULD NOT display capability URLs as text.
Rationale
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.
Backwards Compatibility
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.
Test Cases
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.
Reference Implementation
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.
Security Considerations
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.