Curated NFT collections with optional merkle proof enforcement
The ERC-721 Ethscriptions Collections protocol allows creators to build curated collections of ethscriptions with rich metadata and optional access control.
Collections are an AppChain-only feature. They require smart contract execution on the L2.
Overview
Collection: A named set of ethscriptions with metadata (name, symbol, description, max supply)
Items: Individual ethscriptions added to a collection
Merkle Enforcement: Optional cryptographic restriction on which items can be added
Creating a Collection
Use the create_collection_and_add_self operation to create a collection and add the first item in one transaction:
The rule=esip6 parameter allows duplicate content. Without it, if the same data URI (including headers) was used in a previous ethscription, the new ethscription would be rejected as a duplicate. Uniqueness is based on SHA256 of the full data URI, not just the payload.
Where the base64-decoded d parameter contains:
{"metadata":{"name":"My Collection","symbol":"MYC","max_supply":"100","description":"A curated collection of digital artifacts","logo_image_uri":"","banner_image_uri":"","background_color":"","website_link":"https://example.com","twitter_link":"myhandle","discord_link":"https://discord.gg/...","merkle_root":"0x0000000000000000000000000000000000000000000000000000000000000000","initial_owner":"0x1234567890abcdef1234567890abcdef12345678"},"item":{"item_index":"0","name":"Item #1","background_color":"#FF0000","description":"The first item in the collection","attributes":[{"trait_type":"Rarity","value":"Legendary"},{"trait_type":"Color","value":"Red"}],"merkle_proof":[]}}
Metadata Object Fields
All fields must be present in exact order. Use empty strings for optional values.
Field
Description
name
Collection name
symbol
Short symbol (e.g., "MYC")
max_supply
Maximum number of items (as string)
description
Collection description (can be empty)
logo_image_uri
Logo image as Data URI (can be empty)
banner_image_uri
Banner image as Data URI (can be empty)
background_color
Default background color (can be empty)
website_link
Project website URL (can be empty)
twitter_link
Twitter/X handle (can be empty)
discord_link
Discord invite URL (can be empty)
merkle_root
Merkle root for access control (use zero bytes32 for owner-only)
initial_owner
Address that will own the collection (lowercase)
Item Object Fields
Field
Description
item_index
Position in collection (0-indexed, as string)
name
Item name
background_color
Item-specific background color
description
Item description
attributes
Array of { trait_type, value } objects
merkle_proof
Array of proof hashes (for non-owner adds)
Strict Key Order: For JSON-based operations, keys must appear in exactly the order shown in the tables above. Attribute objects must use { "trait_type": "...", "value": "..." } key order.
Adding Items to a Collection
After creating a collection, add items with add_self_to_collection:
Where the d parameter contains:
The collection_id is the L1 transaction hash of the collection creation.
Merkle Proof Enforcement
When a collection has a non-zero merkle_root, non-owners must provide a merkle proof to add items. This ensures only pre-approved items with exact metadata can be added.
How It Works
Creator generates merkle tree from approved items
Each leaf is computed from item metadata
Creator sets merkle root when creating collection
Non-owners provide proofs when adding items
Merkle Leaf Computation
Each leaf is computed as:
Merkle Tree Structure
For a 3-item collection, the tree looks like:
Where:
Proof for leaf0: [leaf1, leaf2]
Proof for leaf1: [leaf0, leaf2]
Proof for leaf2: [H(leaf0, leaf1)]
Pair Hashing
The merkle tree uses byte-wise ordering (same as OpenZeppelin):
This ensures consistent proof verification regardless of sibling order.
Adding Items with Proofs
Non-owners include the merkle proof in the item object:
Owner Bypass
Collection owners can always add items without providing merkle proofs. This allows:
Adding items not in the original tree
Making corrections
Flexibility for collection management
Example: Creating a Merkle-Enforced Collection
This walkthrough creates a 3-item collection where:
Item
Index
Added By
Merkle Proof Required?
Item 1 (Red)
0
Owner
No (owner bypass)
Item 2 (Green)
1
Non-owner
Yes
Item 3 (Blue)
2
Non-owner
Yes
Step 1: Compute Content Hashes
For each image, compute the keccak256 hash of the raw bytes:
Step 2: Build Merkle Leaves
Compute each leaf from the item metadata:
Step 3: Compute Merkle Root
Step 4: Create Collection (Owner)
The owner creates the collection with the merkle root and adds the first item:
Send a 0 ETH transaction to any address
Include the hex-encoded Data URI with op=create_collection_and_add_self
The merkle_root is set to 0x06fbc22a...
Save the transaction hash as collection_id
The owner doesn't need a merkle proof for their own item.
Step 5: Add Items (Non-Owner)
A different address adds items 2 and 3 with merkle proofs:
For Item 2 (index 1):
The proof must match exactly, and the metadata must match what was used to compute the leaf.
Operations Reference
Operation
Description
create_collection_and_add_self
Create collection and add first item
add_self_to_collection
Add item to existing collection
edit_collection
Update collection metadata
edit_collection_item
Update item metadata
transfer_ownership
Transfer collection ownership
renounce_ownership
Surrender ownership (to zero address)
remove_items
Delete items from collection
lock_collection
Prevent further additions
ESIP-6 is optional. Add rule=esip6 to your data URI only if you need to allow duplicate content (e.g., sending the same JSON command multiple times). Without it, an ethscription with identical content to an existing one will not be created. For image-based operations, add it to the header: data:image/png;rule=esip6;p=.... For text-based operations: data:;rule=esip6,{...json...}.
Editing Collections
Update collection metadata with edit_collection. Send as a data URI:
JSON payload (all fields required; pass current values to keep them, empty strings will clear fields):
Only the collection owner can edit.
Editing Items
Update item metadata with edit_collection_item (all fields required):
Only the collection owner can edit items.
Removing Items
Remove items with remove_items using ethscription IDs (transaction hashes):
Only the collection owner can remove items.
Transferring Ownership
Transfer collection ownership with transfer_ownership:
Only the current owner can transfer ownership.
Renouncing Ownership
Permanently surrender ownership with renounce_ownership:
After renouncing, no one can edit the collection or add items (unless they have valid merkle proofs for a non-zero merkle root collection).
Locking Collections
Once locked, no more items can be added:
This is irreversible. Only the collection owner can lock.
Error Handling
Error
Cause
Invalid Merkle proof
Proof doesn't match root, or metadata differs from what was used to compute the leaf
Merkle proof required
Non-owner tried to add to a collection with zero merkle root (owner-only mode)
Item slot taken
Index already has an item
Collection locked
Cannot add to locked collection
Exceeds max supply
Collection is full
Not collection owner
Only owner can perform this operation
Security Considerations
Content Hash Verification - The merkle leaf includes the content hash, ensuring exact image content is verified
Metadata Binding - All metadata is bound to the merkle proof and cannot be changed after the tree is computed
Owner Bypass - Collection owners can always add items, useful for corrections
Locking - Once locked, no more items can be added even with valid proofs
Zero Merkle Root - When merkle_root is zero, only the owner can add items
Generating Merkle Trees (TypeScript)
Below is complete TypeScript code using viem for generating merkle trees and collection calldata.
Dependencies
Helper Functions
Complete Example
Sending Transactions
To create an ethscription, send a 0 ETH transaction with the hex calldata: