Selling subpages

The SubpageRegistrar is a separate contract that sells subpages of a page you hold, on terms you set. Buyers pay your price and get their subpage in the same transaction. The protocol takes nothing.

You keep the page in your wallet the whole time. The registrar never holds, moves or approves a page: you make it your page’s subpage minter, and it mints each subpage it sells.

Setting up a sale

From the page’s manage page, open Sell subpages and set:

  • Price per subpage, in ETH. Zero makes the sale free.
  • Payout address: who gets paid. It defaults to you.
  • Token gate (optional): buyers must hold a minimum balance of a token you name, for community-only drops. See Token gating.
  • Commit and reveal (optional): buyers commit first and buy at least 20 seconds later, so nobody can see which label they want and take it first. See Commit and reveal.
  • Enabled: switch sales on and off at any time.

The registrar can also price a sale in any ERC-20 token, but this site only handles ETH sales. Setting an ERC-20 price, and buying from a sale priced that way, are done by calling the contract directly.

Setting up takes two calls on two contracts:

  1. SubpageRegistrar.configure(parentId, payout, feeToken, price, enabled, gateToken, minGateBalance, commitReveal) sets the terms. Only the page’s holder can call it, and the page must be top level.
  2. PageNFT.setSubpageMinter(parentId, registrar) makes the registrar the page’s subpage minter.

disable(parentId) stops sales. Clearing the subpage minter stops them too.

Rules for the terms:

  • feeToken zero means ETH.
  • payout zero means you. It cannot be the registrar or PageNFT.
  • price and minGateBalance must fit in 96 bits.
  • gateToken and minGateBalance are both set or both zero.

When the terms sell

The terms sell only while all of this holds:

  • the sale is enabled;
  • the account that set the terms still holds the page;
  • the page has not been registered again since (its epoch is the same);
  • the registrar is still the page’s subpage minter;
  • the page is live and paid, and the label is free.

So a sale pauses when the page moves, and sells again if the page comes back to you. When the page lapses and is registered again, the terms stop for good, and the new holder sets their own.

Buying a subpage

A buyer calls one of these on the registrar, with maxPrice and expectedFeeToken: the highest price and the fee token the buyer accepts, so the seller cannot change the terms under a purchase in flight.

  • registerSubpage(parentId, label, maxPrice, expectedFeeToken) buys for the caller.
  • registerSubpageFor(parentId, label, to, maxPrice, expectedFeeToken) buys for another address.

Both revert with CommitRequired when the terms ask for commit and reveal.

For an ETH price, send at least the price; anything above it is sent back. For a token price, send no ETH, and first approve the registrar for exactly the price: the registrar moves it from the buyer to the payout.

config(parentId) returns the terms: (controller, enabled, commitReveal, feeToken, price, gateToken, minGateBalance, payout, parentEpoch).

PageNFT mints the subpage with the registrar as the caller. So if the receiver is a contract, it must accept ERC-721 tokens, even when it is the buyer, unless it is an account with no code or an EIP-7702 delegation.

Commit and reveal

When the terms ask for it, a buyer buys in two steps, like a page registration:

  1. Pick a random 32-byte secret, compute makeSubpageCommitment(parentId, label, to, secret), and send commitSubpage(commitment).
  2. Wait at least 20 seconds and at most 24 hours, then send revealSubpage(parentId, label, secret, maxPrice, expectedFeeToken) to buy for yourself, or revealSubpageFor(parentId, label, to, secret, maxPrice, expectedFeeToken) for another receiver.

The commitment binds the page, the label, the receiver and the secret, but not who reveals. A reveal works for any sale, also one that does not ask for commit and reveal.

Token gating

A gate turns a sale into a community-only drop: buyers must hold a minimum balance of a token you name, or their purchase reverts. Point it at your DAO’s ERC-20 and require 100 of them, or at your membership NFT and require 1.

Both standards work through the same field, because the registrar only ever calls balanceOf(address), which ERC-20 and ERC-721 both have. It never asks which standard it is. The minimum you set follows from that:

  • ERC-20: an amount in base units. 100 tokens at 18 decimals is 100000000000000000000.
  • ERC-721: a count of NFTs held. At least one from the collection is 1.

The gate token must be on OP Mainnet, where the registrar reads it.

The gate is a balance check and nothing more. Nothing is transferred, nothing is approved, and the buyer’s gate token is never touched: they still pay the sale price. It also applies to a whole contract rather than a token id, so you can require “holds at least one from this collection”, never “holds #42”.

The contract enforces it, not the website. The check runs inside every purchase, so it applies to every buyer on every route: this site, the explorer, a script, another contract. There is no path to a subpage that skips it.

A gate proves a balance, not a membership. The check reads the balance of the account that pays, at the moment of purchase. Nothing requires them to have held the token before or to keep it after, so they can buy it, register, and sell it back in one transaction, or borrow it with a flash loan. The gate also applies to whoever pays: a holder can have a subpage minted to someone who holds nothing, and any subpage can be transferred once it exists. If you need a lasting claim about who ends up holding your subpages, a gate is the wrong tool.

ERC-1155 does not work. Its balanceOf takes a token id as well, so the call does not match and the gate rejects everyone.

A bad address fails closed. If the gate address is not a token, its balance reads as zero and nobody can buy. The sale stays enabled and simply stops selling, with nothing to tell you why, so check the address before you save.

Claiming proceeds

  • ETH sales are credited to the payout in the registrar, and proceedsOf(payout) shows the amount. The payout claims them from the same tab, or with claimProceeds(to), where zero sends them to itself. Anyone can also push them to the payout with claimProceedsFor(payout).
  • ERC-20 sales go straight from the buyer to the payout at each sale, so there is nothing to claim.

Safety properties

  • The terms are bound to the holder who set them and the parent’s epoch. A sale pauses when the page moves, and stops for good when the page is registered again.
  • The subpage is minted in the same transaction as the payment, so there is no moment where the seller holds both the money and the subpage.
  • The registrar never holds a page. Never send a page to the registrar: a safeTransferFrom to it reverts, but a plain transferFrom would leave the page there, and nothing can send it back.

One caveat buyers should understand: the parent’s holder can later replace any subpage by minting it again (see Subpages). Buying a subpage is trust in the parent’s holder, priced accordingly.