ERC-8426: Wallet Pass Extension for NFTs

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.

2 Likes

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

Alright, I decided to keep the experience on test net.

it’s live now and anyone can use it. I have included smart wallets, account abstraction, and alot of other supports in order to allow you to claim a pk pass tied to your nft, and feed/water/play with your nft on chain directly form the ui inside ur apple/google wallet. This new surface area should make it so normal people can hold/interact with nfts, in a surface area they know and trust, next to their amex, boarding pass, loyatly cards, and other real world tangible use case digital products.

If a normie is expected to hold their drivers license, deed, vehicle ownership, personal accounts etc as nfts in the future, this is the most likely path to adoption imo.

More specifically

PR is up: Add ERC: Wallet Pass Extension for NFTs by huntclubhero · Pull Request #2036 · ethereum/ERCs · GitHub
Douglas, every point you made is in the text, and the PR description maps each one to the sentence that answers it:

- two checks, each named for the hole it closes: signed challenge for forwarding, fresh ownerOf at request time for the transfer window

  • public manifest means the URL is hygiene, gated means it’s the proof and rotation is a MUST
  • email onboarding stays seedless: embedded signer or 1271, with the claimed account in the challenge
  • the full floor: chain, contract, id, action, single-use nonce, expiry, verifier, and the server checks every one
  • 4361 as the SHOULD, with the round trip admitted instead of pretending the floor saves it
  • the binding shape from 8291: CAIP-19 token and an action URN inside the signed message
  • 721 only, with balanceOf(account, id) against a minimum as the written 1155 path

Since my last post I ran Douglas’s method against my own production system, and it failed my own floor. My users onboard with email and never sign, the key sits with an embedded signer, the pass links are static. Check (1) as written did not exist on my own pass path.

So the draft now has a capability configuration for exactly that deployment: gated issuance, unguessable, token-and-action bound, rotating, fresh read still mandatory. It’s named as weaker, not dressed up as equivalent. Writing it honestly surfaced the cost, and that’s in the text too: every condition can hold and a forwarded URL still acts while ownership is unchanged. No signature, no forwarding protection. In my deployment that residual is bounded on chain: one owner signature arms a session key that can only call care for that one pet, so a forwarded link can repeat a care and can’t touch the token.

Also in: a full 4361 example challenge with every floor field mapped (token as a CAIP-19 resource, action as a URN), rotation timing (an issuer doesn’t see transfers synchronously, so the fresh read is the boundary in that gap), and a precedence rule for 4907 rentals so owner and renter can’t both act.

The reference implementation ships in the PR’s assets folder, CC0. A minimal 721 with 0xef5f1e71 proven in tests, and the server half with one test per floor requirement, each rejecting its own attack. Next to it are deployment notes from a live testnet deployment whose contract implements the interface, running on real Apple Wallet and Google Wallet passes: the platform gotchas, and the pass download bug I caught in review before it cost anyone anything.

Demo on testnet if you want it on your phone: https://www.playwalletchi.com

Tear this version apart too.

I read the PR, the reference implementation, and the deployment notes. The two checks, the floor with the server verifying each field against what it holds, and the public/gated split all read right. Naming the capability configuration as weaker is the most honest move in the document, and running the floor against your own production system and publishing that it failed is what makes the rest credible.

Since you asked for another round, both of my points are about that configuration.

First, its safety rests on a judgment the spec leaves to the implementer. The substitution is sound “only for actions whose worst-case impact under forwarding the implementation can tolerate,” and actions above that “require the signed challenge,” with no MUST. The entry condition does not bound it either: a pass link can open a page that asks for a signature, as your own fallback does, so the line between the two paths is really whether the product prompts per action. Your notes describe the boundary that actually holds in your deployment: a session key limited on chain to the single function pass links invoke. I would lift that into the conditions, and add the dimension it leaves open. A capability link is reusable, so the worst case under forwarding is the bound action repeated until something rotates the link, and with no transfer the only thing that can is the owner. Your notes also say actions spend from the token’s own account, so “cannot touch the token” holds for ownership, not for what the token holds. Something like: in the capability configuration, the reachable action MUST NOT transfer, burn, or approve the token or change who is entitled to it; its effect under unlimited repetition MUST be bounded, including any value it moves from the token or its account; its authority SHOULD be limited on chain to the function the link invokes; and rotation on the owner’s request becomes a MUST, since it is the only remedy for a leak under an unchanged owner. Then “tolerable” has a floor, and your deployment is the worked example.

Second, the reference server exercises only the signed path for actions. POST /action redeems a signed challenge, and the capability bindings in the pass store cover the download URLs, whose rotation is tested, but never an action link. A capability action route with a test per listed condition (a link bound to one action refused for another, a rotated link refused, the fresh read refusing a sold token behind a live link, and a forwarded link succeeding under an unchanged owner, asserted as the disclosed residual) would put the weaker configuration on the same footing as the floor.

Neither changes the shape. Nice work.

Douglas

2 Likes

Douglas, thank you. Three rounds now, and every one of them made the document better than I could have made it alone. I owe you one atleast.

I hope both our efforts serve the greater good and appreciate you more than you know.

Both points are in the PR.
The floor is in the conditions, not left to me. In the capability configuration the reachable action MUST NOT transfer, burn, or approve the token or change who is entitled to it; its effect under unlimited repetition MUST be bounded, including anything it moves from the token or an account the token controls, and that bound has to be documented; the link’s authority SHOULD be limited on chain to the one function it invokes; and rotation on the owner’s request is a MUST, because you are right that under an unchanged owner it is the only remedy there is. The residual paragraph now says the worst case out loud: a reusable link, the bound action repeated until the owner rotates it, and nothing above that reachable by a link at all.
The entry condition was the softer error. “Cannot carry a signature” was never the line, since my own fallback opens a page that asks for one. The text now says what actually splits the paths: whether the product prompts per action.

“Cannot touch the token” was true for ownership and I let it read as more than that. The notes now state the bound: each care spends from the pet’s own account through the issuer’s tap-to-pay spender, and that spender path is capped at 20 cares per 24 hour window, so a forwarded link can at most spend that allowance on the one thing the account exists to spend on. The owner’s own signed execution is not capped and a link cannot reach it. Ownership and entitlement stay out of reach; what the account holds is exposed up to the cap, and the notes say so.

Reference: the weaker path now has its own route and its own tests, one per condition. A link bound to one action is refused for another, a rotated link is refused, a live link behind a sold token is refused by the fresh read, and a forwarded link under an unchanged owner succeeds, asserted on purpose as the residual the text discloses. Rotation on the owner’s request is a route too, guarded by its own signed proof (not the acquire proof, since an acquire proof authorizes nothing else).
And the deployment: it did not have owner-requested rotation before tonight. It has a “Reset my pass links” action now, because the MUST I just wrote applied to me first.

Seriously, thank you.

2 Likes

Very cool! Would be nice to see a open sourced full example app for easier testing and building upon the protocol, maybe an SDK is a good idea too!

2 Likes

o shit an sdk would be dope, ok i think i can do that.

Thank you!!

@SanLeo461 took you up on it, it’s live: GitHub - huntclubhero/erc8426-sdk: SDK for ERC-8426: put any NFT in Apple Wallet and Google Wallet. Contracts, issuer, Apple and Google delivery, client, React, conformance suite. · GitHub (MIT)

  • contracts: the interface, a base you can inherit, 4907 rentals, and BoundedAction, which enforces the capability config’s bounded repetition on chain
  • issuer: one fetch handler for the whole server side (gated manifests, challenges, the two checks, capability links, rotation). drops into next.js, hono, workers, node
  • apple + google: signed pkpass, the PassKit web service, APNs push, google wallet objects and save links, all from one pass description
  • client + react: detect support, add to wallet, signed actions, and an add to wallet button that shows the issuing contract
  • conformance: npx @erc8426/conformance against any contract + pass server checks every MUST

and the example app you asked for: a next.js app that mints on a local chain and previews the pass right in the browser, no apple or google certs needed. plus runnable demos for a pet game, a stablecoin spending card, nft staking, event tickets, memberships, an ID credential, rentals, and a partner app.

packages are on npm under @erc8426. it’s 0.x and tracks the draft, so it moves with the spec. would love for you to try it and break it - i’ll be updating my erc submission pr with this sdk, example and decisions.

not sure if thats the right thing to do, but you exposed it needs to be more developer friendly if i want people to use it…soooooo hope it helps

Thank you ser

1 Like

small spec update pushed to the PR, all checks green.

building the SDK meant running an independent review against it, and three things came out of it that belong in the spec, not just the code:

  • rotation now has to replace installed passes, not update them. every copy of a pass gets whatever you push to it (and on google wallet everyone who saved the object shares it), so pushing fresh links to the old pass hands them to a leaked copy, or to the seller after a sale. new links go in a new pass, the old one gets marked superseded.
  • acquisition URLs must be https, and clients must not open anything else from a manifest.
  • clients signing on the holder’s behalf should check the challenge names the host the proof is going to, scoped to exactly that token and action.

also fixed a slip in the reference server: the rotate route was returning the manifest, which the spec itself says a non-acquire proof must never do.

the live implementation is updated too, and the conformance tool passes against it. same review, same tests, all in the open: GitHub - huntclubhero/erc8426-sdk: SDK for ERC-8426: put any NFT in Apple Wallet and Google Wallet. Contracts, issuer, Apple and Google delivery, client, React, conformance suite. · GitHub

If a pass needs replacing, I’d want the old one to clearly explain why and how to get the new one. Otherwise, someone might think their NFT or ticket has disappeared. Would the replacement flow guide them through adding the new pass and removing the outdated one?

1 Like

good question, and yes that’s exactly the failure mode to avoid. replacement only happens when links rotate: a sale, a leaked copy, or the owner hitting reset (lost phone etc). normal state changes still update the same pass.

in walletchi the old pass doesn’t just go dead. it turns into a keepsake that says why: “TRANSFERRED, this pet moved to a new wallet” for the seller, or “LINKS RESET, sign in and add the fresh card” when you reset it yourself. buttons stop working, on google the old object is marked expired, and the new pass comes from your account page.

what we can’t do is delete it for you. apple and google don’t let issuers remove a pass from someone’s wallet, so the old one stays until the holder removes it. that’s why the spec says to mark it superseded instead of leaving it looking current.

right now the spec only says SHOULD present as superseded. i think you’re right it should also say the old pass explains why and where to get the new one. i’ll add that.

THANK YOU!!

done, pushed to the PR (all green): a superseded pass SHOULD say why, and if the holder might still be the owner, where to get the new one, and SHOULD NOT show action buttons. also notes that wallets don’t let issuers delete a pass, so the old pass’s text is what tells you your token isn’t gone.

the sdk does it by default now too (0.1.1 on npm): the old pass reads “Transferred” after a sale or “Links reset” after a reset, with where to get the current one. live on the demo at erc8426-demo.vercel.app if you want to see it

1 Like

yooooo

been a minute, lots shipped since the last update, so here’s the roundup.

walletchi went public.

It was so much fkn funnnnnn

The amount of outreach on twitter was crazy, devs, founders, testers, collectors, protocols, etc etc etc.

But it wasn’t cheap, hahaha i ran out of what privy provided as far as free accounts, and before even that i hit my server limits and had to up the memory, all this while only being live for about 15% of the actual test net supply meaning going the whole way woulda been way too expensive hahaha.

But it worked, and EVERYONE LOVES IT!!!

**
the reference implementation opened to everyone on oct 13. real people, real phones, still testnet:

  • 158 pets minted, 157 hatched, 137 still alive (all read from the vault contract, not a database)
  • first ever google wallet adds, alongside apple
  • every contract verified on the robinhood chain testnet explorer
  • minting is closed for now. we hit the user cap on our embedded wallet provider, which is its own lesson for anyone onboarding with email

the most useful thing real users taught me: a big chunk of people tap the link inside the twitter app, and in-app browsers can’t add a pass to the wallet. desktop browsers can’t either. nothing in the spec should change for that, but anyone building on this should plan the hand off to the phone’s real browser from day one.

sdk is at 0.2.2 on npm (@erc8426erc8426, MIT):

  • 0.2.0: apple’s new ios 27 poster face, with a fallback so older iphones still get the classic layout. signing now refuses a certificate that doesn’t match the pass, which before would sign fine and install nowhere
  • 0.2.1 and 0.2.2: an outside security review by Nexory, who went through both the sdk and walletchi line by line. the client now checks every redirect hop, not just the first url. the conformance tool refuses to sign a challenge that isn’t scoped to the host, token and action under test. challenge messages are size-capped before parsing (the parser underneath is quadratic on hostile input, reported upstream). apple device registrations are bounded per pass

huge thank you to Nexory. this is exactly why it’s all in the open.

spec changes in the PR, all coming out of that review:

  • message size: verifiers SHOULD bound a presented message before parsing it, since they parse before they know who sent it
  • redirects: the https rule for acquisition urls now applies to every redirect hop a client follows
  • push fan-out: anyone holding a forwarded pass can register devices for its updates, and every update costs a push per device. implementations SHOULD bound registrations per pass and validate device ids

PR is here: Add ERC: Wallet Pass Extension for NFTs by huntclubhero · Pull Request #2036 · ethereum/ERCs · GitHub (needs one more editor review). if anyone wants to try to break it: npx @erc8426/conformance runs against any contract and pass server, and the demo is live at erc8426-demo.vercel.app

still tracking the draft, still moving with it. thanks again to everyone in this thread, the spec is way better than what i walked in with.

1 Like