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 requestedprivate_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
- The wallet reads public token metadata and renders the public
image. - The wallet sees
private_media_uriand requests it. - The resource server returns a SIWE challenge.
- The owner, holder, or another authorized account signs.
- The wallet retries the request and renders the returned private
imageas 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:
domainequal to the authority of the requested private media URI, including the port when present;uriequal to the requested private media URI;chain-idequal to the gating token's chain for token-form bindings, or a server-selectedchain-id(typically the chain of an advertised token) for policy-form bindings;- a server-generated nonce;
issued-at;- an
expiration-time; - a
resourcesentry 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%2F42Token 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%2F42Token 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%2F42Policy form example:
policy:press-preview?account=0x1230000000000000000000000000000000000000&resource=https%3A%2F%2Fmedia.example.com%2Feip-private-nft-media%2F8453%2F0xabc0000000000000000000000000000000000000%2F42The 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 eyJtZXNzYWdlIjoiLi4uIiwic2lnbmF0dXJlIjoiMHguLi4ifQThe 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, andchain-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 Unauthorizedwith a SIWE challenge; - ERC-721 owner and ERC-1155 holder can access the private media URI;
- wallet can render unlocked
imagefrom 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
erc20binding succeeds whenbalanceOf(account)is at leastminAmountand fails when the balance is below it; - an
erc1155binding withminAmountgreater than1fails whenbalanceOf(account, id)is belowminAmount; - an
erc1155orerc20binding withoutminAmountfails; - an
erc721binding withminAmountpresent fails; - an
erc20binding carrying a token id segment fails, and anerc721orerc1155binding 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
standardvalue the resource server does not implement fails; - a resource binding matching neither defined form fails;
- expired or reused nonce fails;
- signer different from
accountfails unless approval or delegation authorizes it; - delegate scoped to one
.jsonresource cannot access the unlocked image; - public
tokenURIand ERC-1155uriremain 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
accountmust not be accepted solely becauseaccountowns 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
minAmountcan 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 evaluateminAmountat request time rather than treating a past balance as continuing entitlement, and servers that need durable holdings must not treat a singlebalanceOfread 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
ownerOfbinds 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
Copyright and related rights waived via CC0.
