Skip to main content

PRC-3 - Paima Inverse Projection Interface (ERC-721)

Use case​

List your in-game ERC-721 assets on any L1 marketplace, with live metadata served straight from your game.

Summary​

Games that need significant compute typically run on sidechains, L2s, or appchains. That keeps execution affordable but fragments liquidity - buyers and sellers congregate on popular L1 NFT marketplaces, which don't know about the game's home chain.

PRC-3 lets the game inverse-project its state outward: a smart contract on an L1 mints ERC-721 tokens whose tokenURI resolves back to the game node over HTTP. When the L1 NFT is sold, the buyer's claim on the in-game state transfers with it. When it is burned, the asset is returned (or transferred) in-game. No traditional bridge - the L1 is just a tradeable surface on top of the game's own state.

Specification​

Every PRC-3 compliant contract MUST implement IInverseProjectedNft, which extends ERC-721 and ERC-4906 (metadata-update events):

  • A public burn() callable by the token owner.
  • A configurable baseURI and baseExtension.
  • Multiple tokenURI(...) variants - the default uses the game's RPC, but callers can pass either a custom RPC base URL or a contract implementing IUri for fully on-chain metadata.

Base URI shape​

https://{rpcBase}/inverseProjection/{standard}/{purpose}/{chainIdentifier}/{identifier}
  • rpcBase - the URL of an RPC for the game (defaults to the project's hosted node).
  • standard - the PRC version, e.g. prc3.
  • purpose - app-specific (e.g. cards, weapons).
  • chainIdentifier - CAIP-2 chain id (e.g. eip155:1 for Ethereum mainnet).
  • identifier - depends on initialisation mode (see below).

A concrete URI looks like https://rpc.mygame.com/inverseProjection/prc3/cards/eip155:1/0xabc.../42.

Initialisation modes​

PRC-3 supports two ways of starting a projection, each with its own trade-offs.

1. App-Layer Initiated​

The game first issues a userTokenId for <chainId, address, ...>. The user then mints the corresponding NFT on the L1 referencing that id.

  • Pros: no finality wait on the L1, the game state is verifiable, supports batch minting.
  • Cons: requires coordinated signing on two chains; gas ≈ 130–175 k.

userTokenId is <minter,counter>-unique. The Solidity contract enforces uniqueness so two mints with the same pair are impossible.

2. Base-Layer Initiated​

The user mints directly on the L1, passing initialData to describe what they're projecting. The contract's own monotonically-increasing tokenId is the identifier.

  • Pros: no app-layer transaction, lower gas (≈ 75–100 k).
  • Cons: must wait for L1 finality before the game can react; the game may need to reject the mint if initialData is no longer valid in current game state.

A typical resulting URI for app-initiated mode:

https://rpc.mygame.com/inverseProjection/prc3/cards/eip155:1/0x1946a1DD…/1

Mint validity​

Marketplace features like collection offers and "floor sweeping" need a way to distinguish well-formed mints from broken ones. PRC-3 requires every token's metadata to carry a validity attribute:

{ "attributes": [{ "trait_type": "validity", "value": "valid" }] }
  • valid - game-state matches the mint claim.
  • invalid - mint exists but is not honoured by this version of the game.

Critically, invalid mints still increment the counter. A mint that's invalid in one game variant may be valid in another (forks, mods, parallel deployments). Failing to increment would cause the same valid mint to map to a different userTokenId across variants and break interoperability.

Endpoint error cases​

  • A userTokenId (or tokenId) the game hasn't seen yet → return HTTP 404 (don't return dummy data - NFT marketplaces aggressively cache).
  • A token marked invalid for this game variant → return HTTP 404 (same reason).

Rationale​

Instead of pinning metadata to IPFS or other immutable storage, the tokenURI is the RPC call. The marketplace fetches live state. The contract MUST enforce chainIdentifier on-chain (not just in the base URI) so a deployment on chain A can't pretend to be tokens from chain B.

tokenURI overloads exist so neither the user nor the marketplace is locked into trusting the original game's RPC:

  1. tokenURI(id, customBaseUri) - pass any RPC you trust.
  2. tokenURI(id, IUri customSource) - use any on-chain source implementing IUri.

The contract emits ERC-4906 metadata-update events so marketplaces invalidate their caches when the game advances. ERC-4906 in PRC-3 is callable by anyone, not just the admin - if the game updates and marketplaces are stale, any user can trigger a refresh.

Security model​

PRC-3 relies on honest RPC operation, which is the standard assumption for nearly every existing dApp (e.g. trusting OpenSea to render the right metadata). A user who wants stronger guarantees can run their own full node and serve their own RPC.

Marketplaces' caches are kept honest by ERC-4906 metadata-update events; the game forces refreshes when state advances or when a token's validity flips.