Wallet Pass Extension for NFTs: surfacing tokens as Apple Wallet / Google Wallet passes

Hey everyone, first post here, so bear with me.

Quick intro so you know who’s talking: I’m a product designer and builder, and I’ve been participating in NFTs since 2021.

In chasing a better path for NFTs, because I think this is how they should have always worked: living on a surface people already use every day, not locked behind crypto wallets and seed phrases.

The surface I landed on is the wallet app that ships on every phone. Apple Wallet and Google Wallet are the biggest programmable surface in the world, and this ecosystem barely touches them.

So that’s where I’ve been building. An ERC-721 where the token IS the wallet pass: live state rendered right on the card, links on the pass that fire real on-chain transactions, push notifications when chain state changes. No app to download, no seed phrase, you onboard with an email address. It’s been live on a public testnet for a few days, and watching people who have never touched crypto play with an on-chain asset from their Apple Wallet without ever realizing there’s a blockchain underneath has honestly been the coolest thing I’ve ever shipped.

I think this is bigger than any one project. Tokenized real-world assets, a graded One Piece card sitting in a vault, tokenized equities, digital art, Government Identification, loyalty balances: anything with on-chain state wants a home on this surface. The chain should be invisible plumbing, and the pass should be the UI. For regular people, Apple and Google Wallet might be the best unexplored surface area on-chain assets will ever get, and right now our ecosystem treats it as an afterthought.

Here’s why I’m posting. Before writing anything I searched this forum and the ERCs repo for prior art and found nothing: no pkpass, no pass delivery, no mobile wallet passes at all. The closest thing was EthPass, a closed API service (Unlock Protocol used it for membership passes), and a service is not a standard. So right now every team that binds tokens to passes (tickets, memberships, loyalty, me) invents their own private bridge, and that has three real costs:

  1. Wallets, marketplaces, and indexers have no way to discover that a token even has a pass. The whole capability is invisible unless you’re on the issuer’s own site.
  2. There’s no shared signal for “this token’s pass content is stale, re-push it.”
  3. Everyone re-learns the same security lessons alone. I learned one the hard way: a pass is a bearer file. Someone can forward the .pkpass and add it to another phone, and in an early build the action links were derivable from the token’s serial, which meant anyone holding a shared pass could act as the owner. The fix was unguessable capability URLs that rotate on transfer. A standard should bake that scar tissue in so the next builder doesn’t ship the same hole.

Here’s the piece I believe is actually standardizable. It’s deliberately tiny, basically tokenURI’s sibling:

interface IERC721WalletPass {
    event PassUpdate(uint256 indexed tokenId);
    event BatchPassUpdate(uint256 fromTokenId, uint256 toTokenId);

    /// Returns a URI resolving to a JSON pass manifest for the token
    function passURI(uint256 tokenId) external view returns (string memory);
}

And the manifest it points to:

{
  "formats": {
    "apple": "https://issuer.example/passes/c3f1.../card.pkpass",
    "google": "https://pay.google.com/gp/v/save/eyJhbGciOi..."
  },
  "updatedAt": 1754500000
}

Plus the normative rules where the security actually lives: acquisition URLs SHOULD be unguessable capability URLs (MUST when the installed pass exposes state-changing action links), they SHOULD rotate on transfer, holding a pass MUST NOT count as owning the token, and pass-reachable actions MUST be authorized independently. Pass generation itself (Apple certs, APNs, the Google Wallet API, the art) stays out of scope on purpose: that’s platform plumbing, and the interoperable seam is discovery.

Things I’d genuinely love to be challenged on:

  1. One fetched JSON manifest vs format query params. I want a marketplace to learn everything about a token’s pass options in one request. Am I missing a reason to prefer negotiation?
  2. A dedicated PassUpdate event vs just reusing ERC-4906’s MetadataUpdate. Pass content changes way more often than metadata (countdowns, balances), and the consumers are different (pass distributors, not NFT indexers). Does the separate event pull its weight?
  3. I kept ERC-1155 and fungibles out of scope because multi-holder semantics change how a pass binds to a person. But if this surface is ever going to carry RWAs and tokenized equities, a future extension has to cover them. Right call to start 721-only, or lazy call?
  4. Are the capability URL rules pitched at the right strength?
  5. To anyone building wallets or marketplaces: if this existed, would you actually surface an “Add to Apple Wallet / Save to Google Wallet” button on compliant tokens? Because that’s the whole prize.

I have the full draft in EIP-1 format ready to PR to ethereum/ERCs, with a working implementation behind it (closed source today; an open reference implementation would ship before this advances past Draft). I’m not the guy who usually writes interfaces, so I want this community to pressure-test the shape before I make it official. Tear it apart.

My hope for this is simple: give the next wave of people blockchain rails without blockchain complications, starting from the wallet already in their pocket.

Thanks for reading.

1 Like

Welcome, and thanks for bringing the scar tissue with you. The rules where you say the security lives are the right ones, and the bearer-file lesson is the one most teams ship before they learn it. On your question 4, the capability URL rules are pitched close to right, but the section would get sturdier by naming what “authorized independently” actually consists of, because it is two checks, and they close different holes.

A pass is presentation. Every artifact in this design (the .pkpass, the save link, the capability URL) is a bearer artifact. Two facts, together, authorize an action, and neither lives in the artifact. First, the actor proves control of the owning account: a signature over a server challenge that names the token and the action, not possession of a file. Second, the server takes a fresh on-chain read of that account’s standing (ownerOf for 721) at the time of the request, not at pass issuance and not at URL minting.

Each closes a hole the other cannot reach. The fresh read closes the transfer window: a sold token stops acting immediately, however many valid-looking passes are still installed. The control proof closes forwarding: your shared-pass incident stays closed even when the URL leaks and the owner has not changed, which is the case rotation cannot reach. Naming the token and the action in the challenge closes replay, so a proof taken for a cheap action cannot be presented for an expensive one.

There is also a tension between the discovery seam and the unguessability rule that is worth calling out in the spec. passURI is a public view function, and per your first question the manifest is meant to be fetched by any marketplace in one request, so the acquisition URL inside it is one public chain read away from anyone who can enumerate token ids. Unguessability does not survive publication. That is not an argument for dropping the rule, it is the reason the rule cannot be the boundary on its own. Where the pass UX genuinely cannot carry a signature the URL is standing in as the possession proof, your rotation rule and your escalation to MUST when action links exist are both right, and it would help implementers to say which of the two jobs the URL is doing in each configuration, so they know when it is the boundary and when it is hygiene.

One note on your onboarding story, since this could read as putting the seed phrase back in front of the user. It does not have to. An account created from an email address can produce the control proof invisibly, either from an embedded signer or as a contract account whose signature is checked under ERC-1271.

There is fresh precedent one thread over: ERC-8291, SIWE-gated NFT media, at ERC-8291: SIWE-Gated NFT Media URI. It is still an open PR against ethereum/ERCs, which is why your prior-art search would not have surfaced it. It worked through the same trust shape for web-served private content and landed on the same pair: a SIWE proof of account control, bound to the exact resource, plus entitlement authorization evaluated at the time of the request, with servers told to re-check ownership, balance, approval, delegation, or policy state before every protected response. Passes are a different delivery surface with the same shape, and aligning the wording would let the two specs reinforce each other.

On question 3, starting 721-only is the right call, and 8291’s 1155 handling is a usable model for when you extend. The binding names an account alongside the token id, and the holder test is balanceOf(account, id) against a minimum that defaults to 1, rather than the exclusive ownerOf equality that 721 gets. That keeps multi-holder semantics out of the pass file entirely and in the authorization check where they belong.

The discovery seam itself, passURI plus one fetched manifest, looks right to me, and your instinct to keep the platform plumbing out of scope is what makes it standardizable.

Douglas

1 Like

damn, i landed a really big fish.

Hey douglas, thank you so much for taking the time ser,

Taking all of it. The two checks go into the draft close to how you wrote them: signed challenge naming the token and the action, fresh ownerOf at request time. When I wrote “authorized independently” I was gesturing at something I do in production but had never taken apart, and the piece I’d never cleanly separated is the leak that doesn’t end in a transfer. Rotation felt like leak coverage. It’s only transfer coverage. That one stung a little, in a good way.

The manifest one stings more. I put the word “unguessable” inside a document any client can fetch off a public view function and didn’t stare at that sentence long enough. Draft now splits the two configurations: public manifest, URL is hygiene and never a possession proof; gated issuance for flows that can’t carry a signature, where the URL is the proof and rotation goes to MUST.

ERC-1271 line goes in too. That’s what keeps “signature” from reading as “seed phrase” to anyone skimming, and I want it in the spec text, not just in my head.

8291 is a great pointer, open PRs were exactly the hole in my prior-art search. I’ll line up my authorization wording with theirs, and their balanceOf(account, id) binding is a better 1155 answer than the one I was carrying around. 721-only stands, with that as the written extension path.

One thing I’m still chewing on: does the challenge format belong in the spec at all? 8291 uses SIWE. I’m leaning SIWE as SHOULD with “names the token and the action” as the MUST, so a pass flow that can’t do a full SIWE round trip still has a floor. Would that split have survived what you’ve built?

1 Like

That split survives, with one amendment: the floor needs more than scope. Naming the token and the action is the scope half. The other half is what makes a captured proof worthless: a single-use server nonce, an expiration, and the verifier’s own identity inside the signed bytes. Without the nonce the same approval replays against the same action until it expires. Without the expiration a leaked proof never goes stale. Without the domain a challenge signed for one issuer can be presented to another that gates the same token. So I would write the MUST as: the signed challenge names the token (chain, contract, and id), the action, a single-use nonce, an expiration, and the verifier (plus the claimed account where the signature alone does not identify it, the 1271 case), and the server verifies each of them before acting.

Put that way, SIWE as SHOULD costs almost nothing and buys interop: a conforming floor challenge already carries the fields where SIWE’s security lives (the domain, chain, nonce, and expiration slots), and the rest is boilerplate a wallet library emits for free, so the SHOULD is less “also support this format” and more “serialize the floor the way wallets already parse.” Worth noting the floor does not save the round trip either, since a single-use server nonce has to come from the server, so what a constrained flow is really avoiding is parsing a SIWE message, not the trip. SIWE also keeps the 1271 path clean, since contract accounts sign SIWE messages the same way they sign anything else. 8291’s binding is the worked example: domain, chain, contract, token id, account, nonce, expiration, and the exact resource inside the signed message, which is exactly the make-the-proof-useless-anywhere-else property, and it is the part of that spec I would copy before any other.

Your two-configuration split reads right, and moving rotation to MUST where the URL is the possession proof is the honest version of the rule. Looking forward to the draft.

Douglas

1 Like

Floor rewritten field by field, exactly that list: chain, contract, id, action, single-use nonce, expiration, verifier, plus the claimed account on the 1271 path, and the server verifies each before acting. 4361 went into requires as the SHOULD, because you are right that once the floor carries those fields, refusing the serialization wallets already parse is just stubbornness dressed up as minimalism.

The round trip point saved me from shipping a wrong sentence. I was one editing pass from justifying the floor with “for flows that can’t afford the trip”, and there is no such flow, the nonce has to come from the server either way. The draft now says what the floor actually spares you: parsing a format, not a network hop.

That was my last open question. I have a few more things to do for my launch, then PR to ethereum/ERCs goes up next, and I’ll drop the link here when it’s live. This section is now better than anything I would have written alone, thank you Douglas.

1 Like