Reveal Protocol v1 is a minimal on-chain receipt and settlement layer for digital sellers.
Buyer pays.
Seller gets paid.
Your backend receives a verifiable on-chain purchase receipt.
Receipt Mode lets sellers create fixed-price listings or accept seller-authorized dynamic quotes. The contract settles funds immediately, emits ReceiptPurchased, and records the seller net payment with SellerPaid so seller bots, APIs, dashboards, or indexers can fulfill orders off-chain.
v1 is intentionally limited to Receipt Mode.
-
v1 records payment, settlement, and receipt creation on-chain
-
v1 settles funds immediately
-
v1 supports fixed-price listings and seller-authorized signed quotes
-
v1 supports protocol fees and optional signed integrator fees
-
v1 leaves fulfillment, delivery, refunds, disputes, and customer support to off-chain seller systems
-
v1 does not act as a marketplace, escrow system, refund system, or delivery-verification protocol
The current Receipt Mode contracts do not implement zero-knowledge proofs.
The contracts focus on the first production primitive of zkReveal:
payment → settlement → on-chain receipt
The "zk" name refers to the project’s longer-term direction, where future modules may introduce privacy-preserving verification, selective disclosure, encrypted delivery, or reveal mechanisms.
In the current contracts, privacy is limited to design choices such as:
- storing metadata commitments instead of full metadata
- using opaque purchase references
- keeping fulfillment and delivery logic off-chain
- emitting indexable payment and receipt events
This README describes the current smart contract system only. It should not be read as a claim that the current contracts use ZK proofs.
Reveal Protocol v1 receipt mode has two immutable on-chain components:
PurchaseRefRegistryis the canonical replay-protection primitive. It consumes each protocol-scopedpurchaseRefonce and stores which authorized settlement contract or module consumed it. Random wallets cannot consume refs directly. The registry owner may authorize or deauthorize settlement modules for future use, but cannot delete or unconsume refs that have already been consumed.RevealReceiptStoreis the seller-facing receipt settlement contract. It manages listings, signatures, payment settlement, and receipt records.
Current and future Reveal Protocol settlement contracts only share replay protection when they point to
the same PurchaseRefRegistry. Adding a future settlement module requires authorizing that module
in the shared registry.
RevealReceiptStore must be authorized as a PurchaseRefRegistry consumer before purchases can
settle. The deployment script handles this by deploying the registry with the deployer as temporary
owner, authorizing the receipt store, and then transferring registry ownership to
PROTOCOL_OWNER.
Every listing is created in one of two explicit modes, chosen at createListing time
via the ListingMode argument and immutable for the lifetime of the listing. The mode
is a deliberate protocol design choice — it decides up front how a listing may be
purchased, rather than leaving that to depend on which function a buyer happens to call.
| Mode | unitPrice |
Direct purchaseReceipt |
Signed purchaseSignedReceipt |
|---|---|---|---|
PublicFixedPrice |
required, within bounds | allowed | allowed |
SignedQuoteOnly |
must be 0 |
reverts ListingRequiresSignedQuote |
required |
PublicFixedPrice— public direct checkout at the listing's on-chainunitPrice. It also accepts seller-authorized signed quotes, which is what enables buyer-bound payment links, metadata-bound checkout, dynamic pricing, discounts, integrator fees, and bot, merchant-backend, or AI-agent checkout flows on top of a public listing.SignedQuoteOnly— carries no on-chain price (unitPriceis0) and must be purchased through a seller-authorized EIP-712 quote whoseamountis validated at purchase time. DirectpurchaseReceiptreverts withListingRequiresSignedQuote.
The protocol only enforces whether a signed quote is required. Who issues that quote — seller wallet, backend, bot, or dashboard — is an application-layer concern.
The mode cannot be changed after creation; v1 has no setListingMode. To move a product
to a different mode, deactivate the listing with setListingActive(listingId, false) and
create a new one.
For production checkout/payment-link flows, prefer signed quotes.
- Direct purchase path for
PublicFixedPricelistings only; aSignedQuoteOnlylisting reverts withListingRequiresSignedQuote. - The buyer passes the exact
amountthey expect to pay; the contract reverts withPriceMismatchif the listing'sunitPricediffers in either direction. - Listing prices are immutable after
createListing, so the price the buyer asserts is the price the buyer pays. - Does not bind the buyer before submission.
- Anyone who submits a valid unconsumed
purchaseRefand pays first receives the receipt. - Suitable for simple public listings where any buyer may purchase.
- Not recommended for seller-issued private payment links, Telegram checkout links, order-specific checkout, buyer-specific checkout, dynamic pricing, or integrator-fee flows.
- Recommended default for production checkout/payment-link flows.
- Works for both listing modes: required for
SignedQuoteOnly, and also supported onPublicFixedPricelistings. - Uses a seller-authorized EIP-712 quote.
- Binds buyer, listingId, seller, amount, purchaseRef, metadataHash, settlementToken, purchaseRefRegistry, expiry, chain, and contract.
buyeris optional: a non-zerobuyermust matchmsg.sender, so another wallet cannot redeem the same quote; a zerobuyerleaves the quote unbound so any wallet may submit and pay (single-usepurchaseRefstill prevents double-redemption).- Supports dynamic pricing and optional integrator fees.
- Use this for Telegram bot flows, seller-issued order links, private links, custom pricing, and partner or integrator checkouts.
Buyer Proof Mode fits inside the fixed-price path.
- Create a fixed-price listing with
createListing(listingHash, unitPrice, ListingMode.PublicFixedPrice). TheunitPriceis immutable for the lifetime of the listing; to change a product's price, create a new listing. - Optionally pause or resume the listing with
setListingActive(listingId, active). - Agree on a
rawPurchaseRefoff-chain, generate a secretpurchaseRefNonce, and derive the canonical protocol-scopedpurchaseRefwithhashPurchaseRef(seller, listingId, rawPurchaseRef, purchaseRefNonce), or compute the same hash off-chain. - Buyer approves the settlement token and calls
purchaseReceipt(listingId, purchaseRef, amount), whereamountmust equal the listing'sunitPriceexactly.
purchaseReceipt is the direct fixed-price purchase path. The buyer-passed amount must match listing.unitPrice; listing prices are immutable so the price the buyer sees on chain is the price they will pay. The call is public and is not buyer-bound before submission. Anyone who submits a valid unconsumed purchaseRef and pays first receives the receipt. It does not support integrator fees. The raw reference stays off-chain; only the derived bytes32 hash is submitted. For production checkout/payment-link flows, prefer signed quotes.
listingHash is an opaque seller-defined metadata commitment. Human-readable product data lives off-chain, for example inside a seller-signed payment link or checkout payload.
Seller Payment Link Mode fits inside the signed quote path and is the recommended default for production checkout flows. The target listing may be either mode: a SignedQuoteOnly listing (created with createListing(listingHash, 0, ListingMode.SignedQuoteOnly)) restricts the product to this path, while a PublicFixedPrice listing also accepts signed quotes for payment-link, dynamic-price, or integrator-fee orders.
- Seller backend creates an order, generates a short off-chain
rawPurchaseRef, and derives the protocol-scopedpurchaseRefhash. - Seller optionally authorizes a backend or service key for the listing with
setListingQuoteSigner(listingId, signer, true). - The seller wallet or an authorized quote signer signs a
SignedReceiptQuoteoverlistingId,buyer,purchaseRef,amount,metadataHash, optionalintegratorFeeRecipient, optionalintegratorFeeAmount, seller-declaredissuedAt, andexpiresAt; the EIP-712 digest also binds the listingseller, the v1settlementToken, and the immutablepurchaseRefRegistry. - Buyer approves the settlement token and calls
purchaseSignedReceipt(quote, sellerSignature). - The contract verifies the EIP-712 signature and accepts it when the recovered signer is the seller or a quote signer authorized for
quote.listingIdat purchase time. ReceiptPurchasedconfirms payment, and the seller fulfills the order off-chain.
Signed quotes are the v1 mechanism for dynamic pricing. They do not introduce escrow, delayed settlement, or on-chain price discovery. issuedAt is the seller-declared quote issuance timestamp and part of the signed EIP-712 payload. A signed quote is valid only between issuedAt and expiresAt, and expiresAt - issuedAt must not exceed MAX_QUOTE_TTL.
metadataHash must be non-zero and should commit to the readable off-chain payment-link or checkout metadata the seller intends to authorize.
Use validateSignedReceiptPurchase(quote, sellerSignature, expectedBuyer) when a frontend, bot, or backend wants the same validation path as purchaseSignedReceipt without moving funds or creating a receipt.
Use previewSignedReceiptPurchase(quote) only for fee math. It does not verify the seller signature, buyer match, expiry, listing active status, or replay state.
Dynamic signed quotes may be signed either by the seller wallet directly or by an authorized quote signer. This lets a seller keep the settlement wallet separate from a backend hot key. The seller authorizes a signer per listing with setListingQuoteSigner(listingId, signer, true), and that signer can create dynamic quotes only for that listing.
Authorized quote signers can issue signed receipt quotes only for listings where they were authorized. Revoke compromised signers immediately with setListingQuoteSigner(listingId, signer, false).
Integrator fees are supported only through seller-authorized signed quotes.
This lets marketplaces, bots, checkout frontends, dashboards, and other seller tools monetize without changing seller settlement semantics. The seller or authorized quote signer includes integratorFeeRecipient and integratorFeeAmount in the signed quote.
A listing-authorized quote signer is trusted to set the full signed quote intent for that listing, including amount, metadataHash, buyer, purchaseRef, and optional integrator fee fields. The contract enforces protocol fee caps and quote validity, but it does not know whether a delegated signer chose the seller's intended price, metadata, buyer binding, purchase reference, or integrator fee recipient.
On purchase, the Reveal Protocol pays:
- protocol fee
- integrator fee, if present
- seller net amount
receipt.amount remains the gross amount paid. Fee breakdowns should be indexed from ProtocolFeePaid and IntegratorFeePaid.
TypeScript signing shape:
const domain = {
name: "RevealReceiptStore",
version: "1",
chainId,
verifyingContract: receiptStoreAddress,
};
const types = {
SignedReceiptQuote: [
{ name: "listingId", type: "uint256" },
{ name: "seller", type: "address" },
{ name: "buyer", type: "address" },
{ name: "purchaseRef", type: "bytes32" },
{ name: "amount", type: "uint256" },
{ name: "metadataHash", type: "bytes32" },
{ name: "settlementToken", type: "address" },
{ name: "purchaseRefRegistry", type: "address" },
{ name: "integratorFeeRecipient", type: "address" },
{ name: "integratorFeeAmount", type: "uint256" },
{ name: "issuedAt", type: "uint64" },
{ name: "expiresAt", type: "uint64" },
],
};
const message = {
listingId,
seller, // always the listing seller address
buyer,
purchaseRef,
amount,
metadataHash, // hash of seller-defined readable checkout metadata
settlementToken,
purchaseRefRegistry,
integratorFeeRecipient, // zero address when no integrator fee is used
integratorFeeAmount, // zero when no integrator fee is used
issuedAt, // seller-declared quote issuance timestamp
expiresAt,
};
const signature = await signer.signTypedData(domain, types, message);The seller field in typed data is always the listing seller address, even when the signature is produced by an authorized quote signer. metadataHash should commit to the readable off-chain payment-link or checkout metadata you want the seller signature to protect. issuedAt is seller-declared and signed, and a signed quote is valid only between issuedAt and expiresAt with expiresAt - issuedAt <= MAX_QUOTE_TTL. The contract accepts seller-wallet signatures or authorized quote-signer signatures if authorization exists at purchase time.
The signed EIP-712 type is:
SignedReceiptQuote(uint256 listingId,address seller,address buyer,bytes32 purchaseRef,uint256 amount,bytes32 metadataHash,address settlementToken,address purchaseRefRegistry,address integratorFeeRecipient,uint256 integratorFeeAmount,uint64 issuedAt,uint64 expiresAt)
setListingQuoteSigner(listingId, signer, true) authorizes signer for one seller-owned listing.
- A listing-authorized quote signer can issue signed receipt quotes only for that listing.
- A signer authorized for one listing cannot sign valid quotes for another listing unless separately authorized there.
- A listing-authorized quote signer can set the full signed quote intent for that listing, including amount, metadataHash, buyer binding, purchaseRef, and optional integrator fee fields.
- Use
isQuoteSignerAuthorized(listingId, signer)when a frontend, backend, or dashboard needs to treat the seller wallet and delegated signers uniformly. - Deactivating a listing blocks purchases but does not revoke quote signers; revoke compromised signers explicitly.
- Treat quote signers as hot operational keys.
- Use a dedicated backend signer instead of the seller treasury key as a hot service key.
- Rotate or revoke signers when team members, servers, or environments change.
- Monitor signed quote generation in backend logs.
- If a signer is compromised, revoke it immediately with
setListingQuoteSigner(listingId, signer, false).
listingHash, purchaseRef, and metadataHash are opaque commitments and identifiers. They are
not encryption. If the underlying raw value is weak, predictable, or guessable, it may still be
guessed off-chain.
Keep human-readable product, order, and customer data off-chain in the seller backend, bot, or dashboard.
listingHashcommits to seller-defined listing metadata without exposing human-readable product data.metadataHashbinds seller-defined payment-link or checkout metadata without revealing it on-chain.purchaseRefis the protocol-scoped on-chain hash of an off-chain raw operational order reference.
For signed-quote purchases, metadataHash commits to seller-defined off-chain checkout / payment-intent
metadata — the exact intent the seller is authorizing. It is not product blobs, not secrets, and not buyer PII.
metadataHash = keccak256(utf8Bytes(canonicalize(checkoutMetadata)))
Use JSON Canonicalization Scheme (JCS)-style serialization (stable key order, normalized values) before
hashing. Never hash raw JSON.stringify() output unless the runtime guarantees deterministic key
ordering and value normalization.
Recommended v1 shape (schema: "zkreveal.checkout.metadata.v1"):
{
"schema": "zkreveal.checkout.metadata.v1",
"protocol": {
"name": "Reveal Protocol",
"version": "1",
"chainId": 421614,
"receiptStore": "0x...",
"settlementToken": "0x..."
},
"seller": "0x...",
"listing": {
"listingId": "1",
"listingHash": "0x..."
},
"quote": {
"buyer": "0x0000000000000000000000000000000000000000",
"purchaseRef": "0x...",
"amount": "1000000",
"currency": "USDC",
"decimals": 6,
"issuedAt": 1760000000,
"expiresAt": 1760003600
},
"checkout": {
"kind": "agent_topup",
"title": "Top up 10 AI credits",
"description": "Credit top-up for demo agent",
"externalOrderId": "topup_7f3a9c"
},
"integrator": {
"name": "Acme",
"recipient": "0x...",
"feeAmount": "0"
}
}Include in the hash: schema; protocol (chainId, receiptStore, settlementToken); seller;
listing.listingId; listing.listingHash; quote.buyer (even if the zero address); quote.purchaseRef;
quote.amount; quote.issuedAt / quote.expiresAt; checkout.kind; checkout.title; and optionally
checkout.externalOrderId and the success / cancel URLs.
Never in the hash: purchaseRefNonce, unlock / delivery secrets, private invite links, emails, phone
numbers, Telegram IDs / usernames, or any other buyer PII. metadataHash is a public commitment that is
verifiable against chain data — it is not encryption.
checkout.kind is an application-defined string (lowercase snake_case, 1–64 chars, matching
^[a-z0-9][a-z0-9_:-]*$, with : allowed for namespacing) such as payment_link, telegram_bot,
agent_topup, merchant_api, or a namespaced value like x402:agent_request. It is intentionally not a
fixed enum, so new checkout flows do not require a schema bump.
Direct purchaseReceipt purchases emit metadataHash = bytes32(0); purchaseSignedReceipt requires a
non-zero metadataHash. The contract only ever sees and emits the resulting bytes32; the readable
metadata lives in the seller backend, merchant API, bot session, or dashboard.
In Receipt Mode, the Reveal Protocol separates the human-readable off-chain order reference from the on-chain receipt identifier.
rawPurchaseRefis generated by the seller, bot, frontend, or backend.rawPurchaseRefstays off-chain.purchaseRefis the protocol-scopedbytes32hash submitted to settlement contracts.- Canonical replay protection is enforced through
PurchaseRefRegistry.consume(purchaseRef), which only authorized settlement modules may call. receiptIdBySellerAndPurchaseRef[seller][purchaseRef]remains inRevealReceiptStoreonly as a deterministic reconciliation helper for that store's own receipts.- The canonical hash is scoped by the Reveal Protocol domain string
zkReveal.purchaseRef.receipt.v1,chainId, settlement token address, seller address, the raw purchase reference, and the secret purchase ref nonce.
purchaseRef = keccak256(abi.encode(
"zkReveal.purchaseRef.receipt.v1",
block.chainid,
address(settlementToken),
seller,
rawPurchaseRef,
purchaseRefNonce
));Because the canonical hash already scopes by chain, settlement token, and seller, the raw reference should stay short and operational. It should identify the seller-side order in an external system, not describe the buyer or purchased content.
listingId is used only to validate that the listing exists and belongs to the provided seller.
It is not included in the final hash.
Because replay protection is enforced on the final purchaseRef hash through a shared
PurchaseRefRegistry, the same purchaseRef cannot be reused across current or future Reveal Protocol
settlement contracts that share that registry. This also prevents accidental replay across
different listings for the same seller raw order reference. Sellers should still treat each
rawPurchaseRef as a unique operational order ID and avoid reusing it across orders.
Restricting consume to authorized settlement modules prevents griefing by random wallets that
learn a purchaseRef and attempt to consume it directly without paying.
Revoking a consumer only blocks future consumes from that module. It does not unconsume or delete historical purchase refs that were already recorded in the registry.
Frontend and backend integrations should usually let the contract helper derive the canonical hash:
// rawPurchaseRef is the business identifier; purchaseRefNonce is the secret entropy.
const rawPurchaseRef = `rev_topup_${crypto.randomUUID().replaceAll("-", "")}`;
const purchaseRefNonce = ethers.hexlify(crypto.getRandomValues(new Uint8Array(32))); // secret, 32 bytes
const purchaseRef = await receiptStore.hashPurchaseRef(seller, listingId, rawPurchaseRef, purchaseRefNonce);The purchaseRef commitment binds (rawPurchaseRef, purchaseRefNonce). hashPurchaseRef takes
both a human/business rawPurchaseRef and a secret bytes32 purchaseRefNonce:
purchaseRef = keccak256(abi.encode(
"zkReveal.purchaseRef.receipt.v1", chainId, settlementToken, seller, rawPurchaseRef, purchaseRefNonce));
rawPurchaseRef— the business identifier. Recommended canonical form<namespace>_<context>_<random>(issuing brand slug, lowercased flow/service id, opaque suffix), e.g.rev_topup_4f8c1d9a2b7e6035a1c4d8e9f0b2a6c3. With a nonce present, a plain human-readable ref such asinvoice-123is also acceptable.purchaseRefNonce— a secret, high-entropy 32-byte salt generated with a CSPRNG. This is where the cryptographic strength comes from: even a guessablerawPurchaseRefcannot be brute-forced into the on-chain commitment without the nonce.
(rawPurchaseRef, purchaseRefNonce) is the off-chain entitlement bundle shared seller→buyer; only
the resulting bytes32 purchaseRef is ever submitted on-chain. The contract hashPurchaseRef is a
convenience view — purchaseReceipt/purchaseSignedReceipt take the hash directly and never see the
preimage, so format/entropy rules are a convention, not an on-chain rule. The contract only requires
rawPurchaseRef to be 1..128 bytes; purchaseRef must remain unique across any checkout flow that
shares a PurchaseRefRegistry.
Do not use emails, phone numbers, Telegram IDs, usernames, wallet labels, or predictable order
numbers as rawPurchaseRef, and do not let it describe the buyer or the purchased content — those
belong off-chain. The secret purchaseRefNonce is what keeps the commitment unguessable.
Do not put sensitive buyer data, emails, Telegram usernames, private channel names, or plaintext
secrets inside rawPurchaseRef.
Hashes are commitments and identifiers, not encryption. Weak or guessable raw references may still be vulnerable to guessing.
Receipt Mode is a proof-of-payment and settlement primitive. It is not an escrow or delivery-verification system.
- Settlement is immediate.
- The contract does not verify delivery, content correctness, access provisioning, product quality, refunds, disputes, or whether the seller actually fulfilled the order.
- Seller systems, bots, dashboards, or other off-chain workflows are responsible for fulfillment after they detect a valid receipt.
- Buyers and integrators should use trusted sellers or add their own refund or dispute layer off-chain.
- This is intentionally different from the older escrow or delivery mode designs.
ListingCreated and ReceiptPurchased are the source-of-truth events for listing and receipt
discovery by seller bots, backends, dashboards, and indexers.
Signed quote purchases emit the signed metadataHash; direct fixed-price purchases emit bytes32(0).
SellerPaid records the seller net amount after protocol and integrator fees. ProtocolFeePaid
and IntegratorFeePaid expose the rest of the payout breakdown.
Backends can reconcile purchases by:
sellerpurchaseRef
If seller systems also need product context, they should resolve it off-chain from
rawPurchaseRef, purchaseRef, listingId, or listingHash.
The contract also stores:
PurchaseRefRegistry.consumptions[purchaseRef]as the canonical replay-protection recordreceiptIdBySellerAndPurchaseRef[seller][purchaseRef]receipts[receiptId]listingCountBySeller[seller]only to enforceMAX_LISTINGS_PER_SELLER
receiptIdBySellerAndPurchaseRef is not the replay-protection source of truth. It is a local
lookup helper for seller and indexer reconciliation after settlement.
The v1 fee model is immutable at deployment:
settlementTokenfeeRecipientprotocolFeeBps
Constraints:
protocolFeeBpsis capped atMAX_PROTOCOL_FEE_BPS = 50basis points (0.5%)integratorFeeAmountin signed quotes is capped atMAX_INTEGRATOR_FEE_BPS = 450basis points (4.5%) of the quotedamount- combined protocol fee plus integrator fee cannot exceed 5% under these separate caps
feeRecipientmust be non-zero whenprotocolFeeBps > 0integratorFeeRecipientmust be the zero address whenintegratorFeeAmount = 0integratorFeeRecipientmust be non-zero whenintegratorFeeAmount > 0- official v1 deployments are intended for a 6-decimal settlement token such as USDC
MIN_PURCHASE_AMOUNT = 1e6assumes 6 decimals and means 1 USDC- there is no protocol-level maximum purchase amount in this contract
- large purchases are controlled by seller quote policy, frontend/backend limits, token allowance and balance, and operational risk controls
- deploying with an 18-decimal token changes the practical meaning of the minimum purchase amount and is not recommended unless constants are adjusted in a future version
- for Arbitrum mainnet, use the canonical or native USDC deployment intended by the project
settlementTokenshould be a standard ERC-20 such as USDC- fee-on-transfer and rebasing tokens are not supported
There is no dynamic fee mutation in v1.
RevealReceiptStore is owned and uses Ownable2Step for admin transfers.
The owner can independently pause:
- listing creation
- purchases
- quote signer updates
v1 also enforces conservative protocol limits:
- min purchase: 1 USDC (
1e6) assuming a 6-decimal settlement token - max quote TTL: 24 hours
- max listings per seller: 500
- max quote signers per seller: 3
Integration guide:
Core contract:
src/PurchaseRefRegistry.solsrc/RevealReceiptStore.sol
Key functions:
consumeisConsumedconsumedBycreateListingsetListingQuoteSignersetListingActivehashPurchaseRefpurchaseReceiptpurchaseSignedReceiptquotePurchaseReceiptpreviewSignedReceiptPurchasevalidateSignedReceiptPurchasehashSignedReceiptQuoteisQuoteSignerAuthorizedgetReceiptIdBySellerAndPurchaseRef
Key events:
ListingCreatedListingStatusChangedQuoteSignerAuthorizationChangedReceiptPurchasedSellerPaidProtocolFeePaidIntegratorFeePaid
forge fmt
forge testIf Foundry crashes during trace signature lookup in your local environment, retry with:
forge test --offline --suppress-successful-tracesThe v1 deploy script deploys PurchaseRefRegistry first and then deploys RevealReceiptStore
with that registry address wired into the constructor.
Official v1 deployments are intended for a 6-decimal settlement token such as USDC.
MIN_PURCHASE_AMOUNT = 1e6 assumes 6 decimals and means 1 USDC.
There is no protocol-level maximum purchase amount in this contract. Large purchases are
controlled by seller quote policy, frontend/backend limits, token allowance and balance, and
operational risk controls.
The deploy script enforces IERC20Metadata(SETTLEMENT_TOKEN).decimals() == 6. On Arbitrum
One mainnet (chainid 42161) the script additionally pins SETTLEMENT_TOKEN to Circle's
canonical native USDC 0xaf88d065e77c8cC2239327C5EDb3A432268e5831 and rejects USDC.e (bridged).
Required envs:
RPC_URLPRIVATE_KEYSETTLEMENT_TOKENFEE_RECIPIENTPROTOCOL_FEE_BPS— must equalEXPECTED_PROTOCOL_FEE_BPSinDeploy.s.sol(currently50)
Optional envs:
PROTOCOL_OWNER— override the default owner. If unset (or equal to the deployer), the deployer is set as the immediate owner of both contracts in their constructors and the script skipstransferOwnership, so there is noOwnable2Steppending-owner window. If set to a different address, the registry ownership is queued to that address and the target must callacceptOwnership()in a separate transaction before it actually owns the registry.
FEE_RECIPIENT may be the zero address only when PROTOCOL_FEE_BPS=0. The fee recipient
is immutable post-deploy — verify the address can receive USDC before broadcasting.
The deploy output logs both:
PurchaseRefRegistryReceiptStore
If a future Reveal Protocol settlement contract must share replay protection with an existing deployment,
it should be deployed against the same PurchaseRefRegistry address.
Typical Arbitrum One mainnet flow (solo deployer, deployer == protocol owner):
export RPC_URL="https://arb1.arbitrum.io/rpc"
export PRIVATE_KEY="0xYOUR_DEPLOYER_KEY"
export SETTLEMENT_TOKEN="0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # Arbitrum One native USDC
export FEE_RECIPIENT="0xYOUR_FEE_RECIPIENT" # separate address, immutable
export PROTOCOL_FEE_BPS="50"
# PROTOCOL_OWNER left unset → defaults to deployer → no pending-owner window
forge script script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcast --verifyArbitrum Sepolia flow (testnet, any 6-decimal USDC):
export RPC_URL="https://your-arbitrum-sepolia-rpc"
export PRIVATE_KEY="0xYOUR_DEPLOYER_KEY"
export SETTLEMENT_TOKEN="0xYOUR_TEST_USDC_ON_SEPOLIA"
export FEE_RECIPIENT="0xYOUR_FEE_RECIPIENT"
export PROTOCOL_FEE_BPS="50"
forge script script/Deploy.s.sol:Deploy --rpc-url "$RPC_URL" --broadcastAfter deployment, record:
- chain ID
PurchaseRefRegistryaddressReceiptStoreaddress- registry deploy transaction hash
- receipt store deploy transaction hash
- registry authorization transaction hash
- settlement token
- fee recipient
- protocol owner
- registry owner
- protocol fee bps
Reveal Protocol v1 is intentionally focused on receipt-mode settlement and off-chain fulfillment.
Future modules should be documented in their own specifications and repositories when they are designed or implemented.