EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8291: SIWE-Gated NFT Media URI

Defines Sign-In with Ethereum authorization for private NFT media.

Authors
Created2026-05-27
Discussion Linkhttps://ethereum-magicians.org/t/erc-8291-siwe-gated-nft-media-uri/28708
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1801PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationDefinitionsPublic Metadata DiscoveryTypical Wallet FlowProtected Resource FlowVerification RulesAdditional Resources and DelegationUpdatesRationaleBackwards CompatibilityTest CasesReference ImplementationSecurity ConsiderationsCopyright

Abstract

This specification defines a private_media_uri metadata field and Sign-In with Ethereum (SIWE, ERC-4361) flow for private ERC-721 and ERC-1155 media. Access can be gated on a token holding, an ERC-20 balance threshold, or a server-defined policy. A wallet can detect private_media_uri, ask an authorized account to sign, and render the returned private image in place of the public fallback media.

Motivation

ERC-721 and ERC-1155 metadata is public by default. Some NFTs need a public preview and a private image, document, or data file that is only available to an authorized account. Today those flows are application-specific, so wallets and media clients cannot implement one reusable unlock path.

This proposal standardizes only the missing pieces: where the protected URI is advertised, how the SIWE challenge is obtained, what the signature is bound to, and how token ownership, balance, or a server policy is checked before private content is served.

Specification

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.

Definitions

  • account: the account whose entitlement, as named in the resource binding, authorizes access.
  • advertised token: a token whose public metadata exposes the requested private_media_uri. More than one token can advertise the same private media URI. Only ERC-721 and ERC-1155 tokens can be advertised tokens.
  • gating token: the asset identified by a token-form resource binding; the account's on-chain relationship to it authorizes the request. A gating token may be an advertised token but need not be.
  • private media URI: an absolute HTTPS URI without a fragment that identifies a protected token resource.
  • resource server: the HTTPS service that issues SIWE challenges and serves protected resources.

Public Metadata Discovery

Tokens that implement this specification MUST expose private_media_uri as a string member in existing public metadata JSON. ERC-721 tokens expose this JSON through tokenURI(tokenId). ERC-1155 tokens expose it through the metadata URI returned by uri(id).

{
  "name": "Example NFT",
  "description": "Public description safe for unauthenticated clients.",
  "image": "https://example.com/public-preview.png",
  "private_media_uri": "https://media.example.com/eip-private-nft-media/8453/0xabc0000000000000000000000000000000000000/42"
}

private_media_uri is only a discovery pointer. Its value MUST be an absolute HTTPS URI, MUST NOT include a URI fragment, and MUST NOT include embedded userinfo. It MUST NOT embed secrets, bearer tokens, personally identifying information, account-specific secrets, or confidential media payloads. Public metadata SHOULD include enough non-sensitive information for wallets and indexers to render a fallback. The public image MAY be a preview, placeholder, redacted asset, or other safe media.

Typical Wallet Flow

  1. The wallet reads public token metadata and renders the public image.
  2. The wallet sees private_media_uri and requests it.
  3. The resource server returns a SIWE challenge.
  4. The owner, holder, or another authorized account signs.
  5. The wallet retries the request and renders the returned private image as the unlocked NFT media.

Protected Resource Flow

A resource server that receives an unauthenticated request for a private media URI MUST return 401 Unauthorized with a WWW-Authenticate header using the SIWE scheme:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: SIWE realm="private-nft-media", challenge_uri="https://media.example.com/auth/challenge?resource=..."

The header MUST contain a challenge_uri parameter whose value is an absolute HTTPS URI. Its scheme and authority MUST match the requested private media URI. It MAY use a different path or query.

The challenge_uri endpoint MUST accept an address query parameter containing the Ethereum address that will sign. For the normal wallet flow, address is the account the client expects to be authorized, typically the owner or holder of an advertised token. Clients MAY also supply account when the signer is acting for a different account. If account is omitted, the resource server MUST use address as account. A challenge where address differs from account does not grant access by itself; the resource server MUST verify an approved operator, token approval, or explicit delegation before serving protected content. The resolved account is bound into the SIWE resources entry. address and account values MUST be 20-byte hexadecimal Ethereum addresses with a 0x prefix.

The challenge endpoint MUST return application/json with a message string containing the complete SIWE message to sign, and MAY return expires_at matching the SIWE expiration-time.

{
  "message": "example.com wants you to sign in with your Ethereum account...",
  "expires_at": "2026-06-03T12:10:00.000Z"
}

For each private media URI it protects, the resource server determines the entitlement or entitlements that gate it (gating tokens or policies) and constructs the resource binding accordingly. The entitlement selected for a challenge MAY depend on the requesting account. For an account that owns or holds an advertised token, the server MUST issue either a token-form binding naming that advertised token or another binding the account satisfies.

The SIWE message MUST comply with ERC-4361 and MUST include:

  • domain equal to the authority of the requested private media URI, including the port when present;
  • uri equal to the requested private media URI;
  • chain-id equal to the gating token's chain for token-form bindings, or a server-selected chain-id (typically the chain of an advertised token) for policy-form bindings;
  • a server-generated nonce;
  • issued-at;
  • an expiration-time;
  • a resources entry binding the entitlement (gating token or policy), account, and private media URI.

The SIWE message SHOULD include a statement that identifies, in human-readable form, the resource being unlocked and the entitlement that gates it (the gating token or policy).

Before requesting a signature, wallets SHOULD present the entitlement named in the resource binding to the user. When the gating token differs from the advertised token the wallet is rendering, or when the binding is policy form, wallets SHOULD identify that entitlement distinctly from the rendered token. When the binding carries a minAmount, wallets SHOULD present that threshold scaled by the token's decimals where the token exposes them, so the signer reads the amount the binding claims rather than its base-unit encoding.

The SIWE message MUST contain exactly one resource binding entry. The entry MUST use either the token form or the policy form.

Token form, one template per standard value. For erc721 and erc1155, the contract and token id identify the gating token; for erc20, the contract alone identifies the gating token and there is no token id:

eip155:{chainId}/erc721:{contractAddress}/{tokenId}?account={account}&resource={privateMediaUri}
eip155:{chainId}/erc1155:{contractAddress}/{tokenId}?account={account}&minAmount={minAmount}&resource={privateMediaUri}
eip155:{chainId}/erc20:{contractAddress}?account={account}&minAmount={minAmount}&resource={privateMediaUri}

The contractAddress and account values MUST be 20-byte hexadecimal Ethereum addresses with a 0x prefix and MUST be compared by address value, not string casing. standard MUST be erc721, erc1155, or erc20. chainId, and tokenId when present, MUST be unsigned base-10 integers written without leading zeros, except that the value zero is written as 0. The /{tokenId} segment MUST be present for erc721 and erc1155 bindings and MUST NOT be present for erc20 bindings; resource servers MUST reject an erc20 binding that carries a token id segment and an erc721 or erc1155 binding that omits one.

minAmount is the minimum amount of the gating asset the bound account MUST hold, expressed in the asset's base units: the raw units balanceOf returns, with no decimals scaling applied. minAmount MUST be an unsigned base-10 integer without leading zeros and greater than or equal to 1. minAmount is REQUIRED for erc1155 and erc20 bindings, including when the threshold is 1; a binding of either standard without it is invalid. minAmount MUST NOT be present for erc721 bindings, because ERC-721 ownership is exclusive and ownerOf is the check. Each threshold therefore has exactly one encoding, because it is always explicit.

privateMediaUri MUST be the requested private media URI percent-encoded such that all RFC 3986 reserved characters are encoded, and the decoded value MUST match that URI exactly.

Policy form:

policy:{policyId}?account={account}&resource={privateMediaUri}

policyId MUST be a nonempty server-defined identifier composed of URI unreserved characters or percent-encoded octets, and MUST be compared by exact string match without percent-decoding. The policy form makes no on-chain claim; it exists so grants that a verifier cannot recheck through a standardized on-chain verification procedure are labeled as policy instead of being expressed as token claims. account and privateMediaUri follow the same constraints as the token form.

A binding MUST carry exactly the parameters defined for its form: every required parameter, no parameter more than once, and no parameter beyond those this specification defines for that form or those defined by the ERC that introduced the binding's standard value. Those parameters MUST appear in the order shown in the binding's template: the template for the binding's standard value for token-form bindings, or the policy-form template. Resource servers MUST reject a binding that violates these constraints rather than ignoring the offending parameter, because the signer signed the binding as written.

Resource servers MUST treat a resource binding that matches neither binding form as invalid, and MUST reject a token-form binding whose standard value they do not implement.

Future ERCs MAY define additional standard values. Such an ERC MUST specify, for each value it defines: a deterministic verification procedure over on-chain state, naming the read or reads it performs and the comparison that decides the result, bounded and executable by any verifier without server-defined input; the query parameters the value adds and where they appear in the binding; how signer authorization works when the SIWE address differs from the bound account; and whether tokens of that standard can be advertised tokens or gating entitlements only. A standard value MUST NOT define an open-ended or server-defined evaluation program; that is the policy form's role. A standard value MUST NOT reduce the set of accounts the advertised-token requirement in Verification Rules authorizes.

This specification deliberately does not define:

  • composed entitlements, meaning two or more entitlements that must all hold, or a set of any-of alternatives, because they would require relaxing the exactly-one-resource-binding rule and defining evaluation order and failure semantics;
  • attestation-based entitlements, for example those read from an on-chain attestation registry, because there is no sufficiently common convention for registry, schema, issuer trust, expiry, and revocation.

An attestation binding can arrive as an additional standard value under the extension clause above once those conventions exist. A composed entitlement is not a standard value: it changes the structure of the binding rather than the meaning of one field, so it needs a future ERC that updates this specification. Until either exists, such grants use the policy form.

Token form example, where the gating token is the advertised token:

eip155:8453/erc721:0xabc0000000000000000000000000000000000000/42?account=0x1230000000000000000000000000000000000000&resource=https%3A%2F%2Fmedia.example.com%2Feip-private-nft-media%2F8453%2F0xabc0000000000000000000000000000000000000%2F42

Token form example, where holding at least three units of an ERC-1155 membership pass on chain 1 gates the same media URI:

eip155:1/erc1155:0xdef0000000000000000000000000000000000000/7?account=0x1230000000000000000000000000000000000000&minAmount=3&resource=https%3A%2F%2Fmedia.example.com%2Feip-private-nft-media%2F8453%2F0xabc0000000000000000000000000000000000000%2F42

Token form example, where an ERC-20 balance on chain 1 gates the same media URI:

eip155:1/erc20:0xfed0000000000000000000000000000000000000?account=0x1230000000000000000000000000000000000000&minAmount=1000000000000000000&resource=https%3A%2F%2Fmedia.example.com%2Feip-private-nft-media%2F8453%2F0xabc0000000000000000000000000000000000000%2F42

Policy form example:

policy:press-preview?account=0x1230000000000000000000000000000000000000&resource=https%3A%2F%2Fmedia.example.com%2Feip-private-nft-media%2F8453%2F0xabc0000000000000000000000000000000000000%2F42

The client then sends the signed authorization proof to the resource server:

GET /eip-private-nft-media/8453/0xabc0000000000000000000000000000000000000/42 HTTP/1.1
Host: media.example.com
Authorization: SIWE eyJtZXNzYWdlIjoiLi4uIiwic2lnbmF0dXJlIjoiMHguLi4ifQ

The Authorization value after SIWE MUST be an unpadded base64url-encoded JSON object with a message string and signature hex string. Unknown JSON members MUST be ignored.

{
  "message": "example.com wants you to sign in with your Ethereum account...",
  "signature": "0x..."
}

Malformed, expired, replayed, or otherwise invalid proofs MUST NOT return private content.

For the primary wallet rendering flow, a private media URI SHOULD return application/json NFT metadata containing an image string. Clients SHOULD treat that private image as the unlocked replacement for the public metadata image. Additional protected resources MAY use the same SIWE flow.

Verification Rules

Before serving protected content, the resource server MUST verify:

  • the SIWE message, signature, nonce, and expiration;
  • domain, uri, and chain-id;
  • the resource binding, in any defined form, against the expected binding for the requested private media URI and the resolved account;
  • entitlement authorization (token ownership, balance, or policy evaluation) for the bound account at the time of the request;
  • signer authorization when the SIWE address differs from the bound account.

For externally owned SIWE addresses, the recovered signer address MUST match the SIWE address. For contract-account SIWE addresses, the signature MUST be valid under ERC-1271 for that address on the SIWE chain-id chain. domain MUST match the authority of the requested private media URI, including the port when present. uri MUST match the requested private media URI. The SIWE chain-id MUST equal the chainId of the token-form resource binding. For policy-form bindings, the SIWE chain-id MUST equal the chain-id the resource server selected when issuing the challenge.

The resource server MUST verify the presented resource binding against the expected binding it determined for the requested private media URI and the resolved account. The decoded resource value MUST equal the requested private media URI exactly.

For token-form bindings, the bound contract, together with the bound token id when the binding has one, identifies the gating token. The gating token MAY differ from an advertised token; the ownership and balance checks in this section MUST be evaluated against the gating token.

If the SIWE address differs from the bound account, the resource server MUST verify that the SIWE address is authorized by the bound account for the requested private media URI. That authorization MUST come from an approved operator relationship, token approval, or explicit delegation policy. Ownership or balance of the bound account alone is not sufficient. Servers that do not verify such a relationship MUST require the SIWE address to equal the bound account. For erc20 bindings, that authorization MUST come from an explicit delegation policy: ERC-20 defines no operator relationship, and an ERC-20 allowance is a spending approval rather than an access grant, so a nonzero allowance(account, address) MUST NOT be treated as authorizing the signer.

For erc721 bindings, the bound account MUST equal ownerOf(tokenId). For erc1155 bindings, the account bound to the resource MUST have balanceOf(account, id) >= minAmount. For erc20 bindings, the bound account MUST have balanceOf(account) >= minAmount on the bound contract. A binding of erc1155 or erc20 that omits minAmount is invalid and MUST be rejected.

Resource servers MUST NOT treat an ERC-20 contract as an advertised token. Fungible tokens expose no per-token metadata, so an erc20 binding is always a gating entitlement, and erc20 bindings do not affect the advertised-token requirement below.

For every ERC-721 or ERC-1155 token that advertises a private media URI, the resource server MUST accept the token's current owner (ERC-721) or any account with a positive balance of that token (ERC-1155) as an authorized account for that URI. No minAmount reduces that set: the server MUST issue every such account a binding that account satisfies, whatever threshold it applies to other accounts. It follows that a threshold cannot withhold a private media URI from holders of a token that advertises it; minAmount restricts access only through gating tokens that do not advertise the requested URI, so a deployment that wants a holdings threshold to withhold content serves it from a private media URI that the excluded accounts' tokens do not advertise. This requirement determines who is authorized; whether a response is served remains subject to the server's content-level refusal policy, such as content removed for legal reasons, which MUST NOT discriminate among authorized accounts for the same URI.

For policy-form bindings, the resource server MUST evaluate the identified policy for the bound account at the time of the request. Policy semantics are server-defined and out of scope. A policy-form binding makes no on-chain claim.

Approval and delegation policies are server-defined, but they MUST be explicit and checked before protected content is served. An ERC-721 token-level approval (getApproved) MUST be treated as authorizing a signer only when the bound account is the current owner of the gating token.

Nonces MUST be unpredictable, single-use, bound to the resource server domain, and short-lived. Nonces SHOULD additionally be bound to the challenge parameters (account, binding, and resource) for which they were issued. Resource servers SHOULD re-check token ownership, balance, approval, delegation, or policy state before every protected response.

Additional Resources and Delegation

Resource servers MAY protect additional exact URIs with the same SIWE binding mechanism. For example, access to third-party-view.json MUST NOT authorize private-image.png or sibling resources unless those exact URIs are also covered by the server policy. This specification does not define the delegation token or consent flow; it only requires exact resource scope.

Updates

If private_media_uri changes, the token's public metadata changes. When the private content itself changes, the contract MAY emit an ERC-4906 metadata update event to signal that new content is available; content can change frequently, so no on-chain signal is required. Protected content behind a stable URI SHOULD use standard HTTP caching headers and fresh authorization checks.

Rationale

SIWE is wallet-native, human-readable, and already binds signatures to a domain, URI, chain id, nonce, and expiration. This specification adds a token- or policy-scoped resource binding instead of a new wallet signing primitive.

Discovery uses public metadata because ERC-721 and ERC-1155 clients already discover token media there. Authorization stays off-chain because private media is served off-chain. Token contracts remain responsible for ownership, balance, and approval signals.

Token-form bindings are two-sided by design: the bound account holds the gating token, and owners and holders of advertised tokens are always authorized. A token binding is therefore a claim the signer can read and any verifier can check from the message, signature, and chain state. Grants a verifier cannot recheck that way belong to resource servers; the policy form exists so those grants are labeled as policy rather than expressed as token claims. Keeping address separate from account lets servers support operators, delegates, or application policies for the signer, but those signers must be authorized for the exact resource before protected content is served.

What distinguishes a token form from a policy form is a standardized, deterministic verification procedure that any verifier can execute from the message and chain state alone. The three forms defined here are each one chain read and one comparison: ownerOf against the bound account for erc721, balanceOf(account, id) against minAmount for erc1155, and balanceOf(account) against minAmount for erc20. That shared shape is why one minAmount parameter covers every standard with a quantity instead of a per-standard parameter. Requiring minAmount wherever a balance is the check avoids a special case and makes the publisher decide explicitly whether a dust-level balance qualifies; a threshold of 1 reproduces the plain holder rule. Entitlements without such a standardized procedure are not token claims; they use the policy form or a future standard value defined under the extension clause.

This specification does not add an on-chain discovery interface, global delegation registry, encrypted payload format, or zero-knowledge ownership proof. Those mechanisms can be layered onto the same exact-resource SIWE binding if a deployment needs them.

Backwards Compatibility

This specification is backwards compatible with ERC-721 and ERC-1155. Existing wallets, explorers, and indexers can ignore private_media_uri. Gating on an ERC-20 balance requires no change to existing ERC-20 contracts.

Public tokenURI and ERC-1155 uri responses can continue to return redacted or preview metadata. Wallets that implement this specification can unlock and render private media without changing token contracts. Unauthorized clients receive 401 Unauthorized for the private media URI instead of private content.

Test Cases

Implementations should cover at least these cases:

  • unauthenticated request returns 401 Unauthorized with a SIWE challenge;
  • ERC-721 owner and ERC-1155 holder can access the private media URI;
  • wallet can render unlocked image from private JSON metadata;
  • a token-form binding naming a gating token different from the advertised token succeeds when it matches the server's expected binding;
  • an advertised token's owner or holder without the gating token can access the URI through an advertised-token binding;
  • a policy-form binding succeeds only when the server's policy evaluation passes;
  • wrong domain, uri, chain-id, contract, token id, minAmount, policy id, account, or private media URI fails;
  • an ERC-721 binding whose account is not ownerOf(tokenId) fails;
  • an erc20 binding succeeds when balanceOf(account) is at least minAmount and fails when the balance is below it;
  • an erc1155 binding with minAmount greater than 1 fails when balanceOf(account, id) is below minAmount;
  • an erc1155 or erc20 binding without minAmount fails;
  • an erc721 binding with minAmount present fails;
  • an erc20 binding carrying a token id segment fails, and an erc721 or erc1155 binding that omits its token id segment fails;
  • a binding with minAmount=0, a repeated parameter, an undefined parameter, or the defined parameters out of order fails;
  • an account holding a positive but below-threshold balance of an advertised ERC-1155 token is still issued a binding it satisfies, such as one carrying minAmount=1, and is served the URI through it;
  • a binding whose standard value the resource server does not implement fails;
  • a resource binding matching neither defined form fails;
  • expired or reused nonce fails;
  • signer different from account fails unless approval or delegation authorizes it;
  • delegate scoped to one .json resource cannot access the unlocked image;
  • public tokenURI and ERC-1155 uri remain readable by clients that do not implement this specification.

Reference Implementation

A non-normative reference implementation can be added in the relevant ERC assets directory.

Security Considerations

Implementations should treat this as access control, not address anonymity or encrypted storage. The resource server can learn the requester, token, account, and resource relationship. A signed binding reveals the account's relationship to the gating token or policy.

  • Private data must not appear in public metadata, token URIs, on-chain logs, or private media URIs. The URI is a locator, not an authorization secret.
  • Resource servers must validate SIWE fields exactly. A proof authorizes only the exact private media URI named in its binding; a proof for one domain, chain, contract, token, minimum amount, policy, account, or resource must not authorize another. The binding's token or policy identifies the entitlement source, which may differ from an advertised token.
  • When the gating token differs from the token the client is rendering, or when a policy-form binding is used, wallets should present the entitlement to the user before signing.
  • A proof signed by a different address than account must not be accepted solely because account owns or holds the gating token or satisfies the policy.
  • Servers must prevent nonce replay and should use short challenge expirations.
  • Signed URLs, bearer tokens, or delegations should be short-lived, revocable, and scoped to exact resources.
  • Balance thresholds are rentable. A minAmount can be met with borrowed or momentarily transferred funds, and evaluating the balance at request time narrows that window without closing it, because a borrowed position can be held across a request. Servers should evaluate minAmount at request time rather than treating a past balance as continuing entitlement, and servers that need durable holdings must not treat a single balanceOf read as evidence of them.
  • A fungible gate is only as strong as the cost of acquiring minAmount. A low threshold on a widely distributed, faucet-funded, or freely mintable token is close to no gate at all, and anyone can push tokens into an address to satisfy one.
  • Fungible balances are not exclusive. One funded account can back an unlimited number of signers through a permissive delegation policy, where ownerOf binds to a single owner, so servers should bound how many signers a delegation policy authorizes for one account.
  • Cached ownership, balance, approval, delegation, or policy state can leak data after sale, burn, transfer, or revocation.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth