# Highlight Docs (full) > Single-file concatenation of every Highlight documentation page. > Source URL for each section is listed at the top of the block. > Regenerated from the MDX sources on every build. Canonical site: https://docs.highlightv2.xyz --- # Highlight Docs Source: https://docs.highlightv2.xyz/ > Launch, sell, and manage on-chain digital collectibles with the Highlight dashboard, SDK, and REST API. Highlight is a platform for creating and selling digital collectibles on-chain. Use the dashboard if you want to launch without writing code, or the TypeScript SDK if you're building your own mint experience. These docs have three paths. Pick yours. ## For creators You have a collection to launch and don't want to think about contracts. Start in the dashboard. - **[Creator quick start](/getting-started/quick-start/)** — Your first collection, from wallet connect to live mint page. - **[Collection types](/concepts/collections/)** — Editions, series, and generative — pick the right shape for your drop. - **[Sales & gates](/concepts/sales/)** — Fixed price, Dutch auction, ranked auction, and who's allowed to mint. - **[Creator guides](/guides/creator/creating-a-collection/)** — Step-by-step dashboard walkthroughs. ## For developers You're building on top of Highlight — a custom mint page, an agent integration, or an automated drop pipeline. The SDK is the canonical path. - **[Developer quick start](/getting-started/developer-quick-start/)** — Install the SDK, authenticate, make your first call. - **[Mint an NFT end-to-end](/guides/developer/mint-first-nft/)** — The full flow: auth → create → upload → deploy → sell → mint. - **[Deploy workflow](/guides/developer/deploy-workflow/)** — Drive the contract-deploy state machine from code. - **[REST API](/rest-api/operations/user-signin/)** — Auto-generated from the OpenAPI spec. Every route, every shape, always in sync. - **[Error reference](/reference/errors/)** — Every typed error the backend can raise and what status code it maps to. ## For AI agents You're an AI agent (or building one) that needs to act against Highlight. The SDK is typed end-to-end and every route has a stable error taxonomy — treat this as your tool surface. - **[llms.txt](/llms.txt)** — Canonical index of this documentation, optimized for LLM ingestion. - **[llms-full.txt](/llms-full.txt)** — Every concept, guide, and reference page concatenated into one file. - **[Support matrix](/reference/support-matrix/)** — What combinations of collection type, sale type, and access mode are valid. The platform rejects everything not on the matrix. - **[Auth reference](/reference/auth/)** — SIWE, Privy, and API keys — machine-readable auth flows. ## What Highlight does, in one screen - **Collections** — Editions (open / limited / 1-of-1), series (each token unique), or generative (code-generated at mint). - **Contracts** — Deployed to any supported chain. Contract type is derived from collection type. - **Sales** — Fixed price, Dutch auction, or ranked auction. Public (anyone) or gated (via a reusable gate). - **Gates** — Allowlist, token ownership, token attribute, currency balance, Farcaster follow. Combine with ALL or ANY. - **Media** — Upload once, reuse across tokens. R2-backed with optional Arweave archive. - **Tokens** — Minted NFTs with typed metadata, queryable by owner, attributes, or name. The API URL is `https://api.highlightv2.xyz`. The SDK is `@highlightxyz/sdk`. --- # Developer quick start Source: https://docs.highlightv2.xyz/getting-started/developer-quick-start/ > Install @highlightxyz/sdk, authenticate, and make your first API call in under 5 minutes. This is the shortest path from zero to a successful API call. For the full end-to-end (create, upload, deploy, mint) walk-through, see [mint an NFT end-to-end](/guides/developer/mint-first-nft/). ## Install the SDK ```bash bun add @highlightxyz/sdk ``` The SDK is an ESM-only TypeScript package. Works on Bun, Node 20+, and modern browsers. ## Try a public call first A small set of endpoints work without auth. Use one to verify your setup: ```typescript import { createHighlightClient } from "@highlightxyz/sdk"; const client = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", }); const chains = (await client.config.chains()).data!; console.log(chains.map((c) => `${c.name} (${c.chainId})`)); ``` If you see a list of chains, you're wired up. ## Authenticate For anything write-related, you need a JWT. SIWE is the canonical path: ```typescript import { createSiweMessage } from "viem/siwe"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const { nonce } = (await client.user.siwe.nonce()).data!; const message = createSiweMessage({ address: account.address, chainId: 1, domain: new URL("https://api.highlightv2.xyz").host, nonce, uri: "https://api.highlightv2.xyz", version: "1", issuedAt: new Date(), expirationTime: new Date(Date.now() + 10 * 60 * 1000), }); const signature = await account.signMessage({ message }); const { tokens } = (await client.user.signin({ body: { providerType: "Siwe", message, signature }, })).data!; const authed = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", auth: () => tokens.accessToken, security: [{ type: "http", scheme: "bearer" }], }); ``` Access tokens last 15 minutes. See [reference/auth](/reference/auth/) for refresh patterns, Privy sign-in, and API keys. ## Your first authenticated call ```typescript const mine = (await authed.collection.list({ blockchains: [84532], // Base Sepolia testnet: true, types: ["OpenEdition", "LimitedEdition", "OneOfOne", "Series", "GenerativeSeries"], limit: 20, })).data!; console.log(`${mine.pagination.total} collections`); ``` `blockchains`, `testnet`, and `types` are **required** — this is by design; unbounded listing is never what you want. ## Response shape Every method returns `{ data, error, response }`. No exceptions on non-2xx: ```typescript const res = await authed.collection.get({ highlightId: "does-not-exist" }); if (res.error) { console.log(`${res.response?.status}: ${JSON.stringify(res.error)}`); // e.g. "404: { name: \"CollectionNotFoundError\", ... }" } ``` See [reference/errors](/reference/errors/) for the full typed-error catalog. ## Where to go next - **Full pipeline** — [Mint an NFT end-to-end](/guides/developer/mint-first-nft/) - **Deploy details** — [Deploy workflow](/guides/developer/deploy-workflow/) - **Media** — [Media upload flow](/guides/developer/media-upload/) - **Access control** — [Gated sales](/guides/developer/gated-sales/) - **Auction mechanics** — [Ranked auctions](/guides/developer/ranked-auctions/) - **All SDK clients** — [Clients overview](/sdk/clients/) - **All REST routes** — [REST API](/rest-api/operations/user-signin/) --- # Introduction Source: https://docs.highlightv2.xyz/getting-started/introduction/ > What Highlight is, who it's for, and what you can build with it. Highlight is a platform for creating, selling, and managing digital collectibles on-chain. It has two entry points: - A **no-code dashboard** at `highlight.xyz` for creators who want to launch without writing code. - A **TypeScript SDK** and **REST API** for developers and AI agents who want to build on top. Both speak to the same backend. Anything you can do in the dashboard, you can do via the SDK. ## What you can do - **Create collections** — Open, limited, and 1-of-1 editions (ERC-721 or ERC-1155); multi-token series; generative art that renders at mint time. - **Configure sales** — Fixed price, Dutch auction, or ranked auction. Schedule, set supply limits, configure payment recipients. - **Gate access** — Allowlists, token ownership, token attributes, currency balances, Farcaster follow. Combine conditions with ALL/ANY logic. - **Deploy contracts** — Pick any supported chain. The platform derives the right contract implementation from the collection type. - **Manage media** — Upload once to R2, optionally archive to Arweave for permanent storage. - **Mint** — Public vectors or executor-signed gated claims, all through a single SDK call. ## Who it's for - **Creators** who want to launch a drop — start with the [creator quick start](/getting-started/quick-start/). - **Developers** building custom mint flows, dashboards, or integrations — start with the [developer quick start](/getting-started/developer-quick-start/). - **AI agents** acting against Highlight on a user's behalf — see [llms.txt](/llms.txt) and the [support matrix](/reference/support-matrix/). ## What makes it different - **Contract-type selection is automatic.** You don't choose between `EditionsDFS1155` and `SingleEditionDFS` — the backend picks based on your collection type and token standard. - **The support matrix is explicit and tested.** Every valid combination is in [reference/support-matrix](/reference/support-matrix/), and every row is exercised end-to-end on Sepolia. - **Errors are typed.** Every failure has a stable `name` and payload shape — see [reference/errors](/reference/errors/). - **Gated sales don't require on-chain registration.** They use executor-signed claims, so you can add gated sales to already-live collections. ## Next - [Creator quick start](/getting-started/quick-start/) — first collection via dashboard - [Developer quick start](/getting-started/developer-quick-start/) — first SDK call - [Concepts](/concepts/collections/) — the data model --- # Creator quick start Source: https://docs.highlightv2.xyz/getting-started/quick-start/ > Launch your first Highlight collection from the dashboard in under 10 minutes. This walks you through creating a collection using the Highlight dashboard — no code. For the SDK version of this flow, see the [developer quick start](/getting-started/developer-quick-start/). ## Prerequisites - A wallet (MetaMask, Rainbow, or any WalletConnect-compatible provider) - Some testnet ETH on the chain you want to deploy to (Base Sepolia is a good starting point) - Your artwork file(s) — image, video, audio, or zip for series/generative ## 1. Sign in Go to **highlight.xyz** and connect your wallet. You'll sign a SIWE message — this proves wallet ownership without spending gas. ## 2. Create a collection Click **Create** and pick a collection type: - **Open Edition** — unlimited supply of the same artwork - **Limited Edition** — finite supply (same artwork) - **One of One** — a single unique piece - **Series** — multiple tokens, each with unique metadata - **Generative** — code generates the artwork at mint time Each type has a tailored wizard. See [concepts/collections](/concepts/collections/) if you're not sure which to pick. ## 3. Fill in the details Across all types you'll provide: - **Name & description** — shown on the mint page - **Logo & featured image** — used for discovery and previews - **Artwork** — the image/video/zip that backs the tokens - **Royalty recipient** — where secondary-sale royalties go (optional) - **Contract name & symbol** — the on-chain name (e.g. "MYDROP") ## 4. Configure the sale Pick a sale type: - **Fixed Price** — a single price throughout - **Dutch Auction** — starts high, drops on a schedule (up to 5 price points) - **Ranked Auction** — sealed-bid, highest bidders win Then set: - **Price** (or reserve, for ranked) - **Supply limits** — per transaction, per wallet, total for this sale - **Start / end times** - **Payment recipient(s)** — split across multiple wallets if needed - **Access** — public (anyone) or gated (see [setting up gates](/guides/creator/setting-up-gates/)) - **Gas sponsorship** — creator pays gas for collectors (fixed price + public only) See [sale types](/concepts/sale-types/) for full details on each option. ## 5. Review & deploy Preview everything. Deployment asks for **one wallet signature**. After that, the platform handles metadata uploads, contract deployment, and vector registration. You'll watch progress live. ## 6. Share your mint page Once the status flips to **Live**, you'll get a public URL. Share it — collectors arrive, connect their wallet, and mint. ## After launch - **Dashboard management** — edit name, logo, payouts, featured token, external links. See [managing collections](/guides/creator/managing-collections/). - **Stats** — total minted, revenue, collector count. - **Disable the sale** — stop mints permanently (cannot easily be reversed). ## Next - [Collection types explained](/concepts/collections/) - [Configuring sales](/guides/creator/configuring-sales/) - [Setting up gates](/guides/creator/setting-up-gates/) --- # Collections Source: https://docs.highlightv2.xyz/concepts/collections/ > The top-level unit of Highlight — what a collection is and the five types you can create. A **collection** is the top-level container for a drop. Every collection has a type (which determines its on-chain shape and which sales are valid), a backing contract, optionally a sale, and minted tokens. ## The five types | Type | Supply model | When to use | |---|---|---| | `OpenEdition` | Unlimited | Max distribution — every collector can mint | | `LimitedEdition` | Fixed cap | Scarcity-driven drops with a supply limit | | `OneOfOne` | Exactly 1 | Unique, singular pieces | | `Series` | Fixed, unique per token | Multiple tokens with individual metadata | | `GenerativeSeries` | Fixed or open | Code-driven art rendered at mint time | Editions (`Open`, `Limited`, `OneOfOne`) support both **ERC-721** and **ERC-1155**. Series and generative are always ERC-721. ## Lifecycle A collection moves through explicit statuses: 1. **Draft** — configured but not deployed. Only the owner can see it. 2. **InProgress** — deployment transaction submitted, awaiting confirmation. 3. **Live** — contract deployed, sale is claimable. 4. **Migrated** — imported from an external source. 5. **Disabled** — frozen. No further mints. The transition from `Draft` → `InProgress` → `Live` is managed by the [deploy workflow](/guides/developer/deploy-workflow/). ## What's in a collection - **Identifiers** — `id` (UUID, internal) and `highlightId` (public, URL-safe). - **Contract** — a deployed smart contract; type is derived from the collection type + token standard. See [contracts](/concepts/contracts/). - **Type-specific detail** — `CollectionEditionDetail`, `CollectionSeriesDetail`, or `CollectionGenerativeDetail` (mutually exclusive). - **Sale** — a draft collection has at most one sale at a time; a live collection can accept additional gated sales. See [sales](/concepts/sales/). - **Media** — logo, featured image, and the artwork/code the tokens reference. - **Tokens** — the minted NFTs. See [tokens & metadata](/concepts/tokens-and-metadata/). - **Royalty** — optional EIP-2981 royalty recipient + amount. ## Reusable vs non-reusable contracts Some contract implementations can back multiple collections (cheap to deploy a new collection against them); others are one-per-collection. The [support matrix](/reference/support-matrix/) spells out which is which, but as a rule: `LimitedEdition`/`OneOfOne` on ERC-721, and all ERC-1155 editions, use reusable contracts. ## Related - [Editions](/concepts/editions/) — supply models for single-artwork collections - [Series](/concepts/series/) — multi-token collections - [Generative](/concepts/generative-art/) — code-based collections - [Support matrix](/reference/support-matrix/) — valid combinations - [Creating a collection (creator)](/guides/creator/creating-a-collection/) — dashboard walkthrough - [Mint an NFT end-to-end (developer)](/guides/developer/mint-first-nft/) — SDK walkthrough --- # Contracts Source: https://docs.highlightv2.xyz/concepts/contracts/ > Every collection is backed by a smart contract. The platform picks the implementation automatically from the collection type and token standard. Every collection on Highlight is backed by a **smart contract** deployed on-chain. You don't choose the implementation — the platform picks one based on your collection type and token standard. ## Contract implementations | Implementation | Standard | Used for | Reusable | |---|---|---|---| | `EditionsDFS1155Implementation` | ERC-1155 | All edition types (Open, Limited, 1of1) | Yes | | `SingleEditionDFSImplementation` | ERC-721 | Open Edition only | No | | `MultipleEditionsDFSImplementation` | ERC-721 | Limited Edition, 1of1 | Yes | | `SeriesImplementation` | ERC-721 | Collectors Choice series | No | | `RandomSeriesImplementation` | ERC-721 | Standard series | No | | `GenerativeSeriesImplementation` | ERC-721 | All generative (Open and Limited) | No | "Reusable" means the same deployed contract can back multiple collections. Non-reusable contracts get one per collection. All deployments use `ObservabilityV2` for on-chain event emission. ## Multi-chain Contracts are deployed per-chain. A collection belongs to exactly one chain. Use `client.config.chains()` to discover supported chains. ## Deploy lifecycle | Collection status | Contract status | |---|---| | `Draft` | Not deployed | | `InProgress` | Deploy transaction submitted, awaiting confirmation | | `Live` | Deployed, mints are possible | The multi-step deploy workflow (Validate → UploadMetadata → [UploadCode] → GenerateTransaction → AwaitTransaction → RecordTransaction) is explained in [deploy workflow](/guides/developer/deploy-workflow/). ## Related - [Collections](/concepts/collections/) — types overview - [Deploy workflow](/guides/developer/deploy-workflow/) — the full state machine - [Support matrix](/reference/support-matrix/) — contract selection rules - [SDK: contracts](/sdk/contracts/) — contract record queries --- # Editions Source: https://docs.highlightv2.xyz/concepts/editions/ > Open editions, limited editions, and 1/1s — single-artwork collections with different supply models. Editions are the simplest collection type — a single artwork that collectors can mint. The three edition types differ only in supply: ## Open Edition - **Supply**: Unlimited - **Use case**: Maximize distribution. Every collector who wants the piece can mint it. - Supports all sale types (fixed price, Dutch auction, ranked auction) ## Limited Edition - **Supply**: Fixed cap set by the creator - **Use case**: Scarcity-driven drops. Once the supply is minted, no more tokens can be created. - Supports all sale types including ranked auction ## One of One - **Supply**: Exactly 1 - **Use case**: Unique, singular pieces. Only one collector will own this token. - Supports fixed price and Dutch auction sales (ranked auction is not supported for 1/1) ## Edition metadata All edition types share the same metadata structure: - **Name** — The token name - **Description** — Token description - **Image** — Primary visual asset - **Animation URL** — Optional animated asset (video, HTML) — field: `animationUrl` - **Attributes** — Key-value trait pairs ## Token standard Editions support two token standards: - **ERC-721** — Each token is a unique NFT. Standard for most edition types. - **ERC-1155** — Semi-fungible tokens. More gas-efficient for large open editions where all tokens share the same metadata. The token standard is selected during collection creation. ## Related - [Collections](/concepts/collections/) — Collection types overview - [Sales](/concepts/sales/) — How sales work with editions - [Creating a Collection](/guides/creator/creating-a-collection/) — Step-by-step guide --- # Gates Source: https://docs.highlightv2.xyz/concepts/gates/ > Access control for sales — allowlists, token gating, and other conditions. A **gate** is a set of conditions that restrict who can participate in a sale. Gates are reusable — create one gate and apply it to multiple sales. ## Condition types | Condition | Description | |-----------|-------------| | **Allowlist** | Only specific wallet addresses can mint | | **Token Ownership** | Must own tokens from a specific contract (requires `chainId`) | | **Specific Token** | Must own a specific token ID from a contract (requires `chainId`) | | **Token Attribute** | Must own a token with a matching trait (requires `chainId`) | | **Currency Balance** | Must hold a minimum balance of native or ERC-20 currency (requires `chainId`) | | **Farcaster Follow** | Must follow a specific Farcaster account | ## Match modes When a gate has multiple conditions, the match mode determines how they combine: - **ALL** — The wallet must meet every condition (AND logic) - **ANY** — The wallet must meet at least one condition (OR logic) ## Examples ### Early access for holders Create a gate with a **Token Ownership** condition requiring collectors to hold at least 1 token from your previous collection. Apply it to an early-access sale before the public sale opens. ### Curated allowlist Create a gate with an **Allowlist** condition containing specific wallet addresses. Use it for private sales, team mints, or VIP access. ### Multi-condition gate Combine **Token Ownership** + **Currency Balance** with ALL match mode to require collectors to both hold a specific NFT and have a minimum ETH balance. ## Related - [Sales](/concepts/sales/) — How gates attach to sales - [Setting Up Gates](/guides/creator/setting-up-gates/) — Step-by-step guide - [Gates API](/sdk/gates/) — API reference --- # Generative Art Source: https://docs.highlightv2.xyz/concepts/generative-art/ > Code-based collections where artwork is generated programmatically at mint time. **Generative** collections use code (typically JavaScript/HTML) to produce unique artwork for each token. The code runs at mint time, creating a one-of-a-kind output based on a random seed. ## How it works 1. **Upload your code** — Package your generative script as a zip file 2. **Configure capture** — Define how the output is captured as an image or video 3. **Test and preview** — Generate test outputs to verify your code works 4. **Deploy** — Collectors mint tokens, each generating a unique piece ## Capture settings The capture system renders your code and produces a static asset for each token: - **Trigger type** — How the capture is initiated (delay-based or selector-based) - **Delay** — Time to wait before capturing (for animation-based pieces) - **Viewport** — The rendering resolution - **Selector** — CSS selector to target for capture - **Selector type** — `SVG` or `ELEMENT` — determines how the selector is used for capture - **Capture area** — Capture the entire viewport or a specific element via CSS selector - **GPU rendering** — Enable for WebGL/GPU-intensive code ## Edition types Generative collections support two supply models: - **Limited** — Fixed number of outputs - **Open** — Unlimited outputs ## Curation Creators can curate their generative output: - **Allowed hashes** — Whitelist specific outputs by their hash - **Required parameters** — Enforce specific generation parameters ## Deploy without sale Generative collections (and series) support deploying a contract **without configuring a sale**. This is useful when you want to deploy the contract first and configure the sale later, or when minting will be handled through a custom integration. ## Sale compatibility Generative collections support: - Fixed price sales (Open and Limited) - Dutch auction sales (Open and Limited) - Ranked auction sales — **Limited only**, public only Ranked auctions are not available for open generative (`maxTotal = 0`) or for gated access. See the [support matrix](/reference/support-matrix/#generative) for details. ## Related - [Collections](/concepts/collections/) — Collection types overview - [Media & Storage](/concepts/media-and-storage/) — Asset upload and storage - [Creating a Collection](/guides/creator/creating-a-collection/) — Step-by-step guide --- # Mechanics Source: https://docs.highlightv2.xyz/concepts/mechanics/ > The on-chain engines behind each sale type. Built-in mechanics cover fixed price, Dutch, ranked, and gasless; custom mechanics let you plug in your own. A **mechanic** is the on-chain contract that executes a sale. Where [sale types](/concepts/sale-types/) define the pricing strategy at the API level, mechanics define the on-chain logic that actually mints tokens. For most sales you never touch mechanics directly — picking a sale type wires the right one automatically. ## Built-in mechanics | Type | What it does | |---|---| | `SEED_BASED_MINT` | Deterministic token assignment from a seed | | `DISCRETE_DUTCH_AUCTION` | Step-based Dutch auction with scheduled price drops | | `RANKED_AUCTION` | Sealed-bid auction with optional rebate | | `GASLESS` | Gas-sponsored minting via meta-transactions | These are seeded by the platform and immutable. They're deployed per chain; the platform tracks deployment addresses per chain. ## Choose-token (Series) A special mint path, not a mechanic: **choose-token** lets collectors select which token IDs they want to mint. It's only available for `Series` collections on `FIXED_PRICE` or `DUTCH_AUCTION` sales. - `FIXED_PRICE` + any access mode → executor-signed `gatedSeriesMintChooseToken`. - Public `DUTCH_AUCTION` → on-chain `mechanicMintChoose` (needs a registered `mechanicVectorId`). - Gated `DUTCH_AUCTION` → executor-signed `gatedSeriesMintChooseToken`. Ranked auctions never support choose-token. ## Custom mechanics Register your own mint contract: 1. Deploy your mechanic contract on each chain you want it to run on. 2. Call `client.mechanic.create` to register it, including its `configSchema` (JSON Schema for sale `typeConfig`) and `encodingAbi` (ABI used to encode on-chain calldata). 3. Add chain deployments via `client.mechanic.addDeployment`. 4. Reference it when creating sales: `type: "CUSTOM"` + `mechanicId`. See [SDK: mechanics](/sdk/mechanics/) for the code surface. ## Related - [Sale types](/concepts/sale-types/) — pricing strategies and which mechanic backs each - [Support matrix](/reference/support-matrix/) — which mint method each combination uses - [Series](/concepts/series/) — choose-token in context - [SDK: mechanics](/sdk/mechanics/) — register & manage mechanics --- # Media & Storage Source: https://docs.highlightv2.xyz/concepts/media-and-storage/ > Uploading assets, file types, and decentralized storage with Arweave. Highlight handles media upload, processing, and storage for your collection assets. All media is stored centrally with optional decentralized backup to Arweave. ## Supported media types | Type | Use case | Max size | |------|----------|----------| | **Image** | Token artwork, collection logos | 50 MB | | **Video** | Animated token artwork | 500 MB | | **Audio** | Audio-based tokens | 250 MB | | **Archive** | Series assets, generative code | 1 GB | | **Metadata** | Token metadata JSON | 1 MB | ## Upload flow 1. **Create an upload session** — Provide `fileName`, `mimeType`, and `fileSize` to get a signed upload URL 2. **Upload your file** — Send the file to the signed URL via PUT 3. **Poll for readiness** — Check `media.status` until it reaches `Ready` The media type (`Image`, `Video`, `Audio`, `Archive`, `Metadata`) is inferred from the `mimeType` and `fileName`. The platform validates file size against the type limits above. ## Media status lifecycle Every media asset has an explicit `status`: - **Pending** — Upload session created, waiting for bytes - **Processing** — Upload received, extraction in progress (archives only) - **Ready** — Fully available for use - **Failed** — Processing failed (retryable via `POST /media/:id/retry`) For non-archive types, status goes directly from `Pending` to `Ready` on upload. For archives, the extraction step runs automatically and the asset transitions through `Processing` to `Ready`. ## Arweave backup For permanent decentralized storage, you can sync media to Arweave: - **Pending** — Sync has been requested - **Succeeded** — File is stored on Arweave with a permanent URI - **Failed** — Sync encountered an error Arweave backup is optional but recommended for long-term preservation of token assets. ## Archives Archives (zip files) are used for: - **Series collections** — Bundle all token images and metadata in a single zip - **Generative collections** — Package your generative code (HTML, JS, CSS, assets) Uploaded zips are automatically extracted. The original zip is deleted after successful extraction. The media entity includes an `archive` field with the manifest of extracted files, each with a relative `key`, `size`, and `mimeType`. ## Related - [Generative Art](/concepts/generative-art/) — Code upload for generative collections - [Creating a Collection](/guides/creator/creating-a-collection/) — Media upload during creation - [Media API](/sdk/media/) — API reference --- # Sale Types Source: https://docs.highlightv2.xyz/concepts/sale-types/ > Fixed price, Dutch auction, and ranked auction — how each sale type works. Highlight supports three built-in sale types plus custom mechanics, each suited to different pricing strategies. ## Fixed Price The simplest sale type. Every token costs the same price throughout the sale. - **Pricing**: Single fixed price - **Minting**: Collectors pay the price and receive their token immediately - **Use case**: Standard drops, open mints, gated mints ## Dutch Auction The price starts high and drops on a schedule until all tokens are sold or the sale ends. - **Pricing**: Starts at a high price, decreases at intervals - **Schedule**: 1-5 price levels with a configurable drop interval (hours or minutes) - **Minting**: Collectors can mint at any point at the current price - **Use case**: Price discovery — lets the market determine fair value ### How price drops work You configure a series of scheduled prices and a drop interval: 1. Sale starts at the highest price 2. After each interval, the price drops to the next level 3. The price stays at the lowest level until the sale ends ## Ranked Auction A sealed-bid auction where the highest bidders win tokens. - **Pricing**: Minimum reserve bid required - **Bidding**: Collectors submit bids during the sale window - **Settlement**: After the sale ends, the highest bidders can claim tokens - **Rebates**: Optional rebate for winning bids (all winners pay the same clearing price) - **Use case**: High-demand drops where you want competitive pricing ### Auction flow 1. **Bidding phase** — Collectors submit bids above the reserve price 2. **Sale ends** — Bidding closes 3. **Claim phase** — Winning bidders confirm their transactions to receive tokens ## Compatibility | Sale type | Edition | Series | Generative | |-----------|---------|--------|------------| | Fixed Price | All | All | All | | Dutch Auction | All | All | All | | Ranked Auction | Limited only | Standard only | Limited only | Ranked auctions are on-chain only — the `GATED + RANKED_AUCTION` combination is always rejected. See the [support matrix](/reference/support-matrix/) for the complete, per-row combination list. ## Custom Register your own sale mechanic smart contract and use it as a sale type. - **Mechanic**: Requires a registered mechanic with on-chain deployments - **Config**: Freeform configuration passed to the mechanic contract - **Use case**: Novel sale mechanics not covered by built-in types ## Related - [Sales](/concepts/sales/) — Sale configuration overview - [Mechanics](/concepts/mechanics/) — Choose-token and custom mechanics - [Configuring Sales](/guides/creator/configuring-sales/) — Step-by-step guide - [Sales API](/sdk/sales/) — API reference --- # Sales Source: https://docs.highlightv2.xyz/concepts/sales/ > How sales work on Highlight — lifecycle, access modes, configuration, and the rules for adding sales to draft vs live collections. A **sale** defines how collectors can purchase tokens from a collection. A collection can have multiple sales over its lifetime — for example, a gated early-access sale followed by a public drop. ## Draft vs live — who can have what sales - **Draft collection** — exactly **one sale at a time** (public or gated). Delete the existing draft sale before adding a different one. - **Live collection** — **gated sales** can be added freely (they don't register anything on-chain; they activate immediately). **Public sales** on live collections are not yet implemented — they require on-chain vector registration, which is on the roadmap. - **Live sales cannot be deleted.** Pause them or set an `endAt`. ## Access modes - **`PUBLIC`** — open to anyone. Registers an on-chain vector or mechanic that the `MintManager` reads from. - **`GATED`** — restricted to wallets that satisfy a [gate](/concepts/gates/). No on-chain registration; the backend signs each claim with an executor key. Because gated sales are fully off-chain from the contract's perspective, you can attach a gated sale to an already-live collection, run it, end it, and attach another — all without touching the chain outside of mints. ## Configuration | Setting | Description | |---------|-------------| | **Sale type** | `FIXED_PRICE`, `DUTCH_AUCTION`, `RANKED_AUCTION`, or `CUSTOM` | | **Access mode** | `PUBLIC` or `GATED` | | **Price / reserve** | Token price in native currency or ERC-20 | | **`maxPerTransaction`** | Per-transaction mint cap | | **`maxPerWallet`** | Per-wallet accumulation cap | | **`maxTotal`** | Cap on total tokens this sale can mint | | **`startAt` / `endAt`** | Sale window | | **`paymentRecipient`** | Where proceeds go | | **Gas sponsorship** | Creator-paid gas — only `FIXED_PRICE` + `PUBLIC` | | **`gateId`** | Required when access is `GATED`, forbidden otherwise | | **`mechanicId`** | Required when type is `CUSTOM` | | **`typeConfig`** | Type-specific config for `DUTCH_AUCTION`, `RANKED_AUCTION`, `CUSTOM` | | **`collectorMessage`** | Optional copy shown on the mint page | ## Immutable vs mutable fields Once a sale exists, these fields **cannot be changed** — delete and re-add to change them: - `type` - `accessMode` - `gateId` - `gasSponsored` - `mechanicId` - `typeConfig` For a **live public sale**, on-chain fields (`price`, `startAt`, `endAt`, `currency`, limits, `paymentRecipient`) also can't be changed yet. Only `name`, `collectorMessage`, `customMintFee`, and `paused` are mutable. For a **draft sale**, everything is freely updatable. ## Constraints - **Gas sponsorship**: only `FIXED_PRICE` + `PUBLIC`. - **Ranked auctions**: `PUBLIC` only. Gated ranked auctions are rejected. - **Ranked auctions** only work on `LimitedEdition`, standard `Series`, and `Limited` `GenerativeSeries`. - **Custom sales** require a registered `mechanicId` with a deployment on the sale's chain. - See [support matrix](/reference/support-matrix/) for the full combination set. ## Advanced — burn-redeem and donation Fixed price sales support two advanced modes via `typeConfig`. **Burn-redeem** — collectors burn tokens from another contract to redeem: - `burnAddress` — contract to burn from - `mechanicAddress` — on-chain mechanic handling the burn - `tokenId`, `burnAmount` — which and how many - `chainId` — optional, for cross-chain burns **Donation** — collectors can pay above the reserve: - `reserveEtherPrice` — minimum - `maxPerWallet` — optional wallet cap Both only apply when `type: "FIXED_PRICE"`. ## Related - [Sale types](/concepts/sale-types/) — pricing strategies - [Gates](/concepts/gates/) — access control - [Support matrix](/reference/support-matrix/) — every valid combination - [Sales SDK](/sdk/sales/) — code-level reference - [Gated sales guide](/guides/developer/gated-sales/) — claim flow --- # Series Source: https://docs.highlightv2.xyz/concepts/series/ > Multi-token collections where each token has unique metadata. Two variants — Collectors Choice (collectors pick tokens) and Standard (sequential or random). A **Series** is a collection of multiple unique tokens — each with its own name, image, and attributes. Unlike editions (where every token shares the same artwork), series tokens are individually distinct. ## How series work - **Fixed supply** — the total number of tokens is set at creation. - **Unique metadata per token** — each gets its own name, image, animation, and attributes. - **Bulk upload** — one zip containing all per-token assets and metadata. Series are always ERC-721. ## Two variants | Variant | Who picks the token ID | Backing contract | |---|---|---| | **Collectors Choice** | Collector — `tokenIds` passed with claim | `SeriesImplementation` | | **Standard** | Platform — sequential or random | `RandomSeriesImplementation` | Both contracts are non-reusable (one per collection). ## Revealed vs unrevealed Orthogonal to the variant — either variant can be revealed or unrevealed. - **Revealed** — metadata visible before mint. Ideal with Collectors Choice so collectors can browse and pick. - **Unrevealed** — metadata hidden until post-mint. "Blind mint" experience. ## Choose-token mechanic Collectors Choice collections always use the `gatedSeriesMintChooseToken` executor-signed path (even on public sales) because the collector specifies exact token IDs. Exception: public Dutch auctions on Collectors Choice use `mechanicMintChoose` (fully on-chain with selection). See [support matrix: series](/reference/support-matrix/#series) for the full mint-method mapping. Ranked auctions are not supported for Collectors Choice. Standard series support ranked auctions (public only). ## Related - [Collections](/concepts/collections/) — types overview - [Mechanics](/concepts/mechanics/) — choose-token internals - [Support matrix](/reference/support-matrix/) — valid combinations - [Creating a collection](/guides/creator/creating-a-collection/) — dashboard walkthrough --- # Tokens & Metadata Source: https://docs.highlightv2.xyz/concepts/tokens-and-metadata/ > Token structure, attributes, and how metadata works across collection types. A **token** is an individual NFT within a collection. Each token has metadata that describes it — its name, image, traits, and other properties. ## Token metadata Every token includes: | Field | Description | |-------|-------------| | **Name** | Display name of the token | | **Description** | Text description | | **Image** | Primary visual asset URL | | **Animation URL** | Optional animated asset URL (video, HTML, etc.) — field: `animationUrl` | | **Attributes** | Key-value trait pairs | ## Metadata by collection type ### Editions (Open, Limited, 1/1) All tokens in an edition share the same metadata. When you set the name, image, and description for the edition, every minted token inherits those values. ### Series Each token has unique metadata. You upload individual assets and define attributes per token. Tokens can be queried and filtered by their attributes. ### Generative Token metadata is generated at mint time. The image is captured from your generative code's output, and the seed used for generation is stored as part of the token's data. ## Attributes Attributes are key-value pairs that describe traits of a token: ```json { "trait_type": "Background", "value": "Gold" } ``` Attributes are used for: - **Display** — Showing traits on token detail pages - **Filtering** — Searching tokens by trait values - **Gate conditions** — Token attribute gates check for specific traits ## Querying tokens Tokens can be listed and filtered by: - Collection - Owner wallet address - Attribute values - Name search - Minted date (`mintedAt`) ## Related - [Collections](/concepts/collections/) — Collection types overview - [Gates](/concepts/gates/) — Token attribute gate conditions - [Tokens API](/sdk/tokens/) — API reference --- # Configuring Sales Source: https://docs.highlightv2.xyz/guides/creator/configuring-sales/ > Set up pricing, scheduling, gas sponsorship, and payment recipients for your sales. This guide covers the sale configuration options available when setting up a mint on Highlight. ## Choosing a sale type Select the pricing model that fits your drop: - **Fixed Price** — Simple, predictable pricing - **Dutch Auction** — Price discovery through scheduled drops - **Ranked Auction** — Competitive bidding for high-demand drops See [Sale Types](/concepts/sale-types/) for detailed descriptions of each. ## Setting the price - For **fixed price**, set a single price in the native currency - For **Dutch auction**, configure 1-5 price levels and the drop interval - For **ranked auction**, set the minimum reserve bid ## Supply limits Control how many tokens can be minted: - **Max per transaction** — Limit how many tokens a collector can mint in one transaction - **Max per wallet** — Limit total tokens a single wallet can accumulate - **Max total** — Total tokens available through this sale (independent of collection supply) ## Payment recipients Configure where sale proceeds go. You can split payments across multiple wallets with percentage-based splits. ## Gas sponsorship Enable gas sponsorship to cover transaction fees for your collectors. When enabled, collectors mint for free (excluding the token price). **Constraints:** - Gas sponsorship is only available for **fixed price** sales - Gas sponsorship is **not available** for gated sales (must be public access) ## Access control Set the sale access mode: - **Public** — Anyone can mint - **Gated** — Only wallets meeting [gate conditions](/concepts/gates/) can mint ## Sale replacement Each collection supports **one sale at a time**. Updating the sale configuration replaces the existing sale. To transition between sale phases (e.g., from gated early-access to public), update the sale configuration when you're ready to switch. ## Want to do this programmatically? See the [Sales API](/sdk/sales/) for creating and managing sales via the SDK. ## Related - [Sale Types](/concepts/sale-types/) — How each sale type works - [Setting Up Gates](/guides/creator/setting-up-gates/) — Configure access control - [Sales API](/sdk/sales/) — API reference --- # Creating a Collection Source: https://docs.highlightv2.xyz/guides/creator/creating-a-collection/ > Step-by-step walkthrough of the collection creation wizard. This guide walks through the creation wizard for each collection type on the Highlight dashboard. ## Starting the wizard From the dashboard, click **Create** and choose your collection type. Each type has a tailored multi-step wizard. ## Edition creation flow For Open Edition, Limited Edition, and One of One: 1. **Basics** — Name, description, and collection metadata 2. **Details** — Edition size (limited/1/1), royalty configuration 3. **Upload** — Upload your artwork (image, video, or animation) 4. **Mint** — Configure your sale (pricing, gates, payment recipients) 5. **Review** — Preview everything before deployment ## Series creation flow 1. **Basics** — Name, description, collection metadata 2. **Upload** — Upload a zip file containing all token assets 3. **Details** — Configure series size and reveal status 4. **Mint** — Configure your sale and optional choose-token mechanic 5. **Review** — Preview before deployment ## Generative creation flow 1. **Collection details** — Name, description, collection metadata 2. **Upload assets** — Upload your generative code as a zip file 3. **Test script** — Configure and test capture settings (trigger, delay, viewport, GPU) 4. **Size and output** — Configure collection size and output settings 5. **Preview images** — Review generated outputs and curate 6. **Series details** — Contract properties (name, symbol, transferability) 7. **Mint details** — Configure your sale (or deploy without a sale) 8. **Review and deploy** — Final preview before deployment ## After creation Once you complete the wizard, your collection is in **Draft** status. You'll need to [deploy the contract](/guides/creator/deploying-a-contract/) to make it live. ## Want to do this programmatically? See the [Collections API](/sdk/collections/) for creating collections via the SDK. ## Related - [Collections](/concepts/collections/) — Collection types explained - [Configuring Sales](/guides/creator/configuring-sales/) — Detailed sale setup - [Deploying a Contract](/guides/creator/deploying-a-contract/) — Deploy your collection --- # Deploying a Contract Source: https://docs.highlightv2.xyz/guides/creator/deploying-a-contract/ > Deploy your collection's smart contract on-chain. Once your collection is fully configured, you need to deploy its smart contract to make it live. This guide walks through the deployment process. ## Prerequisites - A collection in **Draft** status with complete configuration - A connected wallet with enough funds for the deployment transaction - All media uploaded and processed ## Deployment steps ### 1. Initiate deployment From your collection's management page, click **Deploy**. Highlight validates your configuration and prepares the deployment. ### 2. Validation The platform checks that: - All required fields are complete - Media assets are uploaded and processed - Sale configuration is valid - Contract parameters are correct ### 3. Metadata upload Token metadata is uploaded to storage before the contract is deployed. ### 4. Sign the transaction Your wallet will prompt you to sign the deployment transaction. Review the transaction details and confirm. ### 5. Wait for confirmation The deployment transaction is submitted to the blockchain. Highlight monitors the transaction and updates the status. ### 6. Contract is live Once confirmed, your collection status changes to **Live** and collectors can start minting through your configured sales. ## Deployment status You can check deployment progress at any time: - **Validate** — Configuration is being verified - **UploadMetadata** — Token metadata is being stored - **GenerateTransaction** — Generating the deployment transaction data - **AwaitTransaction** — Waiting for your wallet signature and transaction submission - **RecordTransaction** — Recording the confirmed on-chain transaction Overall deployment status: `Pending` → `InProgress` → `Completed` (or `Failed`) ## Want to do this programmatically? See the [Contracts API](/sdk/contracts/) for deploying contracts via the SDK. ## Related - [Contracts](/concepts/contracts/) — How contracts work - [Contracts API](/sdk/contracts/) — API reference --- # Managing Collections Source: https://docs.highlightv2.xyz/guides/creator/managing-collections/ > Edit, configure, and monitor your live collections. After deploying a collection, you can manage it from the Highlight dashboard. ## Collection management From the dashboard, click on any collection to access its management page. Available sections: ### Details Edit your collection's name, description, and metadata. Some fields may be locked after deployment. ### Logo Update the collection logo displayed on the dashboard and mint page. ### Payouts View and update payment recipient configuration for your sales. ### Stats Monitor your collection's performance: - Total minted tokens - Total sales revenue - Collector count ### Featured Token Select a token to feature prominently on your collection page. ### External Links Add links to your project's website, social media, or community channels. ### Options Additional collection settings including disabling the collection. ## Disabling a collection If you need to stop minting, you can disable your collection. This changes the status to **Disabled** and prevents any further mints. This action cannot be easily reversed. ## Want to do this programmatically? See the [Collections API](/sdk/collections/) for managing collections via the SDK. ## Related - [Collections](/concepts/collections/) — Collection types overview - [Collections API](/sdk/collections/) — API reference --- # Minting Experience Source: https://docs.highlightv2.xyz/guides/creator/minting-experience/ > What collectors see when they visit your mint page. Every deployed collection on Highlight has a public mint page where collectors can purchase tokens. This guide describes the collector-facing experience. ## The mint page Each collection gets a unique mint URL. The page displays: - Collection artwork and metadata - Sale details (price, supply, time remaining) - Mint button and quantity selector - Connected wallet status ## Sale-specific experiences ### Fixed price Collectors see the price and available supply. They select a quantity, click mint, and confirm the transaction in their wallet. ### Dutch auction Collectors see the current price and the price schedule. They can mint at any time at the current price, or wait for a lower price (risking the sale selling out). ### Ranked auction During the bidding phase, collectors submit bids above the reserve price. After the sale ends, winning bidders return to claim their tokens. ## Gated sales If a sale is gated, the mint page verifies the collector's wallet against the gate conditions before allowing a mint. If the wallet doesn't qualify, the collector sees a message explaining the requirements. ## Gas-sponsored mints When a creator enables gas sponsorship, collectors don't pay transaction fees — only the token price (if any). This creates a smoother minting experience. ## Choose token (Series) For Series collections with choose-token enabled, collectors can browse available tokens and select which specific one they want to mint. ## Related - [Sales](/concepts/sales/) — Sale configuration - [Sale Types](/concepts/sale-types/) — How each sale type works - [Gates](/concepts/gates/) — Access control --- # Setting Up Gates Source: https://docs.highlightv2.xyz/guides/creator/setting-up-gates/ > Create and configure access control gates for your sales. Gates let you control who can participate in a sale. This guide walks through creating and managing gates on the Highlight dashboard. ## Creating a gate 1. Navigate to **Gates** in the dashboard 2. Click **Create Gate** 3. Add one or more conditions 4. Choose a match mode (ALL or ANY) 5. Save the gate ## Configuring conditions ### Allowlist Add specific wallet addresses that should have access. You can paste addresses one at a time or in bulk. ### Token Ownership Require collectors to hold tokens from a specific contract: - Enter the contract address - Set the minimum number of tokens required ### Specific Token Require ownership of a specific token ID from a contract: - Enter the contract address - Enter the token ID ### Token Attribute Require ownership of a token with a specific trait: - Enter the contract address - Specify the trait type and value (e.g., "Background: Gold") ### Currency Balance Require a minimum wallet balance: - Choose native currency (ETH) or an ERC-20 token - Set the minimum balance amount ### Farcaster Follow Require the collector to follow a specific Farcaster account: - Enter the Farcaster FID (user ID) ## Applying gates to sales When configuring a sale, set the access mode to **Gated** and select your gate. A single gate can be reused across multiple sales. ## Want to do this programmatically? See the [Gates API](/sdk/gates/) for creating and managing gates via the SDK. ## Related - [Gates](/concepts/gates/) — How gates work - [Configuring Sales](/guides/creator/configuring-sales/) — Attach gates to sales - [Gates API](/sdk/gates/) — API reference --- # Deploy workflow Source: https://docs.highlightv2.xyz/guides/developer/deploy-workflow/ > The multi-step state machine that takes a draft collection to Live — with the complete polling pattern. Deploying a collection isn't a single request. The backend runs a workflow — validating, uploading metadata, building the transaction, waiting for the client to submit it on-chain, then recording confirmation. This guide covers the full loop. ## The state machine Every deployment progresses through these steps in order: | Step | Owner | What happens | |---|---|---| | `Validate` | Backend | Checks collection is fully configured. Missing media, invalid sale, or matrix violations fail here. | | `UploadMetadata` | Backend | Publishes token-metadata directory to R2 and computes the baseURI. Idempotent. | | `UploadCode` | Backend | Generative collections only: archives the code directory. | | `GenerateTransaction` | Backend | Assembles the deploy transaction data — constructor args, implementation address, initializer. | | `AwaitTransaction` | **Client** | Backend hands you a transaction; you submit it with your wallet and call `deployConfirm`. | | `RecordTransaction` | Backend | Waits for receipt, stores contract address, flips collection to `Live`. | Overall deployment `status` is one of: - `Pending` — workflow queued, not yet started - `InProgress` — workflow running (any step) - `Completed` — contract is live - `Failed` — workflow hit a fatal error; `error` field holds the reason ## The full loop ```typescript import { createWalletClient, http } from "viem"; import { baseSepolia } from "viem/chains"; const walletClient = createWalletClient({ account, chain: baseSepolia, transport: http(), }); async function deploy(highlightId: string) { // 1. Kick off the workflow await client.collection.deploy({ highlightId }); // 2. Poll until the backend either finishes or needs our signature while (true) { const state = (await client.collection.deployStatus({ highlightId })).data!; if (state.status === "Failed") { throw new Error(`Deploy failed: ${state.error}`); } if (state.status === "Completed") { return state; } if (state.currentStep === "AwaitTransaction" && state.contractConfig) { // 3. Client's turn — sign and submit. // contractConfig is a flat { chainId, to, data, value }. // `to` is null for contract-creation transactions, so pass undefined to viem. const cfg = state.contractConfig; const hash = await walletClient.sendTransaction({ to: (cfg.to ?? undefined) as `0x${string}` | undefined, data: cfg.data as `0x${string}`, value: BigInt(cfg.value ?? "0"), gas: 5_000_000n, }); // 4. Tell the backend about the txHash and keep polling await client.collection.deployConfirm({ highlightId, txHash: hash }); } await new Promise((r) => setTimeout(r, 3000)); } } ``` ## Idempotency `deploy` is idempotent: calling it twice on the same draft returns the existing deployment. `deployConfirm` is idempotent against the same `txHash`. If your client crashes mid-flow, reconnect and call `deployStatus` — the workflow picks up where it left off. ## Getting the contract address after `Completed` After `Completed`, the deployed contract address is attached to the collection: ```typescript const collection = (await client.collection.get({ highlightId })).data!; const address = collection.contract.address; // on the target chain ``` ## Failure modes | Failure | Where | What to do | |---|---|---| | `Validate` fails | backend | Check the `error` field — usually a missing `logoMediaId`, bad `type`+`tokenStandard` combo, or an invalid sale. Fix and call `deploy` again. | | Metadata publish times out | backend | The workflow retries internally. If it stays `Failed`, check your media assets' `status`. | | Transaction reverts on-chain | chain | `RecordTransaction` fails. The `error` field holds the revert reason. Fix inputs and redeploy. | | Transaction submitted but receipt times out | chain | Call `deployConfirm` again with the same hash after the receipt lands; the backend picks it up. | ## Generative: the extra `UploadCode` step Generative collections insert `UploadCode` between `UploadMetadata` and `GenerateTransaction`. It archives the `codeMediaId` to permanent storage. Nothing for the client to do — just expect one more step when polling. ## Deploying without a sale Generative and series collections can deploy without a configured sale. This is useful when the sale is driven by an off-chain or custom mechanic and you want the contract live first. Omit the `addSale` step before calling `deploy`. Editions **require** a sale before deploy — the platform rejects a naked edition deploy. ## After `Completed`: `finalizeBaseUri` For collections whose metadata is meant to be frozen (e.g. a sold-out limited edition), call: ```typescript await client.collection.finalizeBaseUri({ highlightId }); ``` This publishes the metadata directory to Arweave and flips the on-chain baseURI. Idempotent — safe to call once your drop is done or from a cron. ## Related - [Mint an NFT end-to-end](/guides/developer/mint-first-nft/) — the full pipeline including deploy. - [Support matrix](/reference/support-matrix/) — what combinations pass `Validate`. - [Errors](/reference/errors/) — the full catalog of deploy-time errors. - [REST API: collection-deploy](/rest-api/operations/collection-deploy/) — auto-generated schema. --- # Gated sales Source: https://docs.highlightv2.xyz/guides/developer/gated-sales/ > Restrict who can mint using allowlists, token ownership, or on-chain conditions — with the complete claim flow. Gated sales restrict minting to wallets that satisfy a **gate**. A gate is a reusable bundle of conditions; attach it to a sale by setting `accessMode: "GATED"` and `gateId`. Unlike public sales, gated sales have **no on-chain vector**. The backend signs each claim with the platform executor key; collectors submit the signed claim to the chain. This is why gated sales can be added to `Live` collections — they don't need an on-chain vector registration. ## When to use gated | Goal | Use | |---|---| | Allowlist of specific addresses | `ALLOWLIST` condition | | Holder-only early access | `TOKEN_OWNERSHIP` condition | | Attribute-based (e.g. holders of "Gold" traits) | `TOKEN_ATTRIBUTE` condition | | Requires minimum ETH/ERC20 balance | `CURRENCY_BALANCE` condition | | Farcaster-based access | `FARCASTER_FOLLOW` condition | Combine conditions with `matchMode: "ALL"` (all must pass) or `"ANY"` (one must pass). Ranked auctions cannot be gated. The platform rejects that combination at validation time. ## 1. Create a gate Gates are owned by an account and reusable across many sales. ```typescript const gate = (await client.gate.create({ name: "Allowlist + Holders", matchMode: "ANY", conditions: [ { type: "ALLOWLIST", config: { addresses: [ "0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222", ], }, }, { type: "TOKEN_OWNERSHIP", config: { contractAddress: "0xYourPreviousCollection", chainId: 8453, minAmount: 1, }, }, ], })).data!; const gateId = gate.id; ``` All on-chain conditions (`TOKEN_OWNERSHIP`, `SPECIFIC_TOKEN`, `TOKEN_ATTRIBUTE`, `CURRENCY_BALANCE`) require a `chainId`. The gate evaluator reads state on that chain at claim time. ## 2. Attach the gate to a sale ```typescript await client.collection.addSale({ highlightId, type: "FIXED_PRICE", accessMode: "GATED", gateId, startAt: new Date().toISOString(), price: "0.005", currency: "ETH", maxPerTransaction: 1, maxPerWallet: 2, maxTotal: 500, paymentRecipient: creatorAddress, // gasSponsored: true — REJECTED for gated sales }); ``` Gated sales can be added to draft **or** live collections. Either way, the sale is immediately usable — there's no on-chain registration step. ## 3. Collector claim (gated) The claim shape is identical to a public sale. The backend decides whether to return a public-vector claim or an executor-signed claim based on the sale's access mode — the collector doesn't care. ```typescript const claim = (await client.collection.claimSale({ highlightId, saleId: gatedSale.id, amount: 1, })).data!; ``` If the caller's wallet doesn't satisfy the gate, the call returns `SaleGateCheckFailedError` (403) and the payload lists which condition failed. Submit the returned transaction. The claim shape is flat — `{ chainId, to, data, value }`: ```typescript const hash = await walletClient.sendTransaction({ to: (claim.to ?? undefined) as `0x${string}` | undefined, data: claim.data as `0x${string}`, value: BigInt(claim.value ?? "0"), gas: 500_000n, }); ``` On-chain, the mint is authorized by the executor signature embedded in `claim.data`. The platform contracts verify the signature and the single-use nonce. ## 4. Gated + choose-token (Series) For Series collections, collectors can pick specific token IDs. Pass `tokenIds` alongside `amount`: ```typescript const claim = (await client.collection.claimSale({ highlightId, saleId: chooseTokenSale.id, amount: 3, tokenIds: [7, 42, 101], })).data!; ``` The support matrix dictates which mint method backs the call — see [support matrix: series](/reference/support-matrix/#collectors-choice). ## 5. Listing & updating a gate ```typescript const gates = (await client.gate.list()).data!; const detail = (await client.gate.get({ gateId })).data!; // Replace conditions wholesale — partial updates are not supported await client.gate.update({ gateId, name: "Updated gate", matchMode: "ALL", conditions: [ { type: "ALLOWLIST", config: { addresses: ["0x..."] } }, ], }); await client.gate.remove({ gateId }); ``` A gate currently attached to any live sale cannot be deleted — detach first (delete the sale or replace the gate by re-adding the sale). ## Constraints - A gated sale **requires** `gateId`. - A public sale **must not** have a `gateId`. - `RANKED_AUCTION` + `GATED` is rejected. - `gasSponsored: true` is rejected on gated sales (gas sponsorship requires a public on-chain vector). - Gate condition evaluation happens at claim time, not sale creation. If a collector's balance drops after you add them via an allowlist, a token-ownership condition they relied on could fail. ## Related - [Gates concepts](/concepts/gates/) — higher-level overview - [Sales reference](/sdk/sales/) — all sale configuration options - [Errors: gates and sales](/reference/errors/#gate-errors) — typed failures - [Support matrix](/reference/support-matrix/) — what combinations are valid --- # Media upload flow Source: https://docs.highlightv2.xyz/guides/developer/media-upload/ > The three-step upload handshake — create session, PUT bytes, poll for Ready. Covers Files and Directories (zips). Every media asset — logos, token images, series asset bundles, generative code — goes through the same three-step flow: 1. **Create an upload session.** The backend creates a pending `MediaAsset` and returns a signed URL. 2. **PUT the bytes** to that URL. 3. **Poll** until the asset's `status` is `Ready`. Files (images, videos, audio, metadata JSON) go `Pending` → `Ready` directly after upload. Directories (zips) go `Pending` → `Processing` → `Ready` as the backend extracts them. ## MediaKind Every upload declares a `kind`: | Kind | Use for | |---|---| | `File` | Single images, videos, audio, metadata JSON | | `Directory` | Zip archives for series asset bundles or generative code | The kind is **declared, not inferred**. A zip uploaded with `kind: File` stays a blob; you need `Directory` to trigger extraction. ```typescript import { MediaKind } from "@highlightxyz/sdk"; ``` ## Size limits | Kind | Max size | |---|---| | File (image) | 50 MB | | File (audio) | 250 MB | | File (video) | 500 MB | | File (metadata) | 1 MB | | Directory | 1 GB | Exceeding returns `MediaTooLargeError` at session creation time (before you waste bandwidth uploading). ## The full upload ```typescript import { readFileSync } from "node:fs"; import { MediaKind } from "@highlightxyz/sdk"; async function uploadFile( client: ReturnType, accessToken: string, path: string, mimeType: string, kind: MediaKind, ): Promise { const buffer = readFileSync(path); // 1. Create session const session = (await client.media.createUploadSession({ kind, fileName: path.split("/").pop()!, mimeType, fileSize: buffer.byteLength, })).data!; // 2. PUT bytes const putRes = await fetch(session.upload.url, { method: session.upload.method, headers: { ...session.upload.headers, authorization: `Bearer ${accessToken}`, }, body: buffer, }); if (!putRes.ok) { throw new Error(`Upload PUT failed: ${putRes.status} ${await putRes.text()}`); } // 3. Poll until Ready return waitForReady(client, session.media.id); } async function waitForReady( client: ReturnType, mediaId: string, timeoutMs = 120_000, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const media = (await client.media.get({ mediaId })).data!; if (media.status === "Ready") return mediaId; if (media.status === "Failed") throw new Error(`Media ${mediaId} failed`); await new Promise((r) => setTimeout(r, 2000)); } throw new Error(`Media ${mediaId} did not become Ready within ${timeoutMs}ms`); } ``` ## Uploading a directory (zip) Series asset bundles and generative code are zip archives: ```typescript const codeMediaId = await uploadFile( client, tokens.accessToken, "./generative-code.zip", "application/zip", MediaKind.Directory, ); ``` After upload, the backend extracts the archive. `status` transitions `Pending` → `Processing` → `Ready`. The `waitForReady` helper handles both cases — just give it enough timeout for extraction (~2 minutes is typical). Once `Ready`, enumerate extracted files: ```typescript const children = (await client.media.listChildren({ mediaId: codeMediaId, limit: 100, })).data!; for (const child of children.entries) { console.log(child.key, child.size, child.mimeType); } // Fetch a specific child's full Entity (with a resolved URL) const entry = (await client.media.getChild({ mediaId: codeMediaId, path: "index.html", })).data!; console.log(entry.url); ``` ## Why polling? The PUT handler runs synchronously for File kinds — most images are `Ready` on the next poll. Directory extraction runs in the background (zip decompression, per-file mime detection, storage writes for every child). Poll at 2-second intervals; don't re-PUT on a slow response. ## Retrying a failed Directory A Directory that ends up `Failed` (corrupt zip, extraction crash, etc.) can be retried: ```typescript await client.media.process({ mediaId }); ``` `process` is idempotent — `Ready` assets come back unchanged; `Processing` assets keep going; `Failed` Directories retry. ## Archive to Arweave Media lives on R2 by default. To freeze it permanently on Arweave: ```typescript await client.media.publish({ mediaId }); ``` Files publish the blob; Directories publish a manifest over all children. Idempotent. After `publish` succeeds, the media has both R2 and Arweave storage locations. For a collection's metadata specifically, prefer `client.collection.finalizeBaseUri(highlightId)` — it archives the token-metadata directory and flips the on-chain baseURI in one call. ## Deleting media ```typescript await client.media.delete({ mediaId }); ``` Media in-use by a deployed contract or a generative collection's code can't be deleted — the call returns `MediaInUseError` (409). Arweave locations are immutable and stay on-chain even after deletion; only R2 objects are freed. ## Related - [Media & storage concepts](/concepts/media-and-storage/) — higher-level overview - [Mint an NFT end-to-end](/guides/developer/mint-first-nft/) — media in context - [Errors](/reference/errors/#media-errors) — typed failures - [REST API: media](/rest-api/operations/media-upload/) — auto-generated schema --- # Mint an NFT end-to-end Source: https://docs.highlightv2.xyz/guides/developer/mint-first-nft/ > The full developer flow — sign in, create a draft, upload art, deploy the contract, configure a sale, let a collector mint. This guide walks the complete flow from nothing to a minted token, using only the SDK. Every step is copy-pasteable against `api.highlightv2.xyz` on a testnet chain. For a quick-fire first call, use the [developer quick start](/getting-started/developer-quick-start/). Come back here when you need to see the whole pipeline. ## What you'll build A public, fixed-price Open Edition on Base Sepolia, with one image, deployed end-to-end, ready for a collector to claim. ~100 lines of code. ## Prerequisites - Node 20+ or Bun 1.1+ - A wallet (viem `Account`) with some testnet ETH on your target chain - The API base URL: `https://api.highlightv2.xyz` ```bash bun add @highlightxyz/sdk viem ``` ## 1. Sign in The full auth details live in [reference/auth](/reference/auth/). Short version: SIWE gives you a 15-minute JWT. ```typescript import { createHighlightClient, MediaKind } from "@highlightxyz/sdk"; import { createSiweMessage } from "viem/siwe"; import { privateKeyToAccount } from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const baseUrl = "https://api.highlightv2.xyz"; const anon = createHighlightClient({ baseUrl }); const { nonce } = (await anon.user.siwe.nonce()).data!; const message = createSiweMessage({ address: account.address, chainId: 1, domain: new URL(baseUrl).host, nonce, uri: baseUrl, version: "1", issuedAt: new Date(), expirationTime: new Date(Date.now() + 10 * 60 * 1000), }); const signature = await account.signMessage({ message }); const { tokens } = (await anon.user.signin({ body: { providerType: "Siwe", message, signature }, })).data!; const client = createHighlightClient({ baseUrl, auth: () => tokens.accessToken, security: [{ type: "http", scheme: "bearer" }], }); ``` From here on, `client` is authenticated. ## 2. Upload the artwork Media upload is a three-step dance: create a session, PUT bytes, poll until `Ready`. See [media upload flow](/guides/developer/media-upload/) for the full reference. ```typescript import { readFileSync } from "node:fs"; const buffer = readFileSync("./artwork.png"); const session = (await client.media.createUploadSession({ kind: MediaKind.File, fileName: "artwork.png", mimeType: "image/png", fileSize: buffer.byteLength, })).data!; // PUT the bytes. The upload URL is a worker endpoint on the same origin. await fetch(session.upload.url, { method: session.upload.method, headers: { ...session.upload.headers, authorization: `Bearer ${tokens.accessToken}`, }, body: buffer, }); // Poll until Ready const logoMediaId = session.media.id; let status = "Pending"; while (status !== "Ready") { await new Promise((r) => setTimeout(r, 2000)); status = (await client.media.get({ mediaId: logoMediaId })).data!.status; if (status === "Failed") throw new Error("Media processing failed"); } ``` ## 3. Create the draft collection The collection starts in `Draft`. We'll bind it to a brand-new contract on Base Sepolia (chain 84532). ```typescript const collection = (await client.collection.create({ name: "My First Drop", description: "An open edition minted via the Highlight SDK.", type: "OpenEdition", logoMediaId, image: session.media.url!, contract: { chainId: 84532, name: "MYDROP", symbol: "MYDROP", standard: "ERC721", }, })).data!; const highlightId = collection.highlightId; ``` ## 4. Fill in edition-specific details Each collection type has its own PATCH endpoint for type-specific fields. For editions: ```typescript await client.collection.updateEditionDetails({ highlightId, edition: { size: 0, // 0 = open supply name: "My First Drop", description: "An open edition minted via the Highlight SDK.", imageMediaId: logoMediaId, attributes: [{ trait_type: "Series", value: "Genesis" }], }, }); ``` ## 5. Configure the sale A `FIXED_PRICE` / `PUBLIC` sale — the simplest supported combination. See the [support matrix](/reference/support-matrix/) for everything else. ```typescript const sale = (await client.collection.addSale({ highlightId, type: "FIXED_PRICE", accessMode: "PUBLIC", startAt: new Date().toISOString(), price: "0.001", currency: "ETH", maxPerTransaction: 5, maxPerWallet: 10, maxTotal: 1000, paymentRecipient: account.address, })).data!; ``` ## 6. Deploy the contract Deployment is an async workflow. Kick it off, then poll until it needs a wallet signature. See [deploy workflow](/guides/developer/deploy-workflow/) for the full state machine. ```typescript import { createWalletClient, http } from "viem"; import { baseSepolia } from "viem/chains"; const walletClient = createWalletClient({ account, chain: baseSepolia, transport: http(), }); await client.collection.deploy({ highlightId }); // Poll until the workflow has a tx ready for us to sign let deployState = (await client.collection.deployStatus({ highlightId })).data!; while (deployState.currentStep !== "AwaitTransaction" && deployState.status !== "Completed") { if (deployState.status === "Failed") throw new Error(deployState.error ?? "deploy failed"); await new Promise((r) => setTimeout(r, 3000)); deployState = (await client.collection.deployStatus({ highlightId })).data!; } // Sign & submit the deployment transaction. // `contractConfig.to` is null for contract-creation transactions — pass undefined to viem. const cfg = deployState.contractConfig!; const hash = await walletClient.sendTransaction({ to: (cfg.to ?? undefined) as `0x${string}` | undefined, data: cfg.data as `0x${string}`, value: BigInt(cfg.value ?? "0"), gas: 5_000_000n, }); // Tell Highlight the txHash; the indexer picks it up from there await client.collection.deployConfirm({ highlightId, txHash: hash }); // Wait for the collection to go Live while ((await client.collection.get({ highlightId })).data!.status !== "Live") { await new Promise((r) => setTimeout(r, 5000)); } ``` ## 7. Let a collector mint From the collector's perspective, minting is a single SDK call that returns a contract execution to submit on-chain. For a public fixed-price sale, no executor signature is involved. ```typescript const claim = (await client.collection.claimSale({ highlightId, saleId: sale.id, amount: 1, })).data!; // claim is { chainId, to, data, value } — a flat contract-execution shape. const mintHash = await walletClient.sendTransaction({ to: (claim.to ?? undefined) as `0x${string}` | undefined, data: claim.data as `0x${string}`, value: BigInt(claim.value ?? "0"), gas: 500_000n, }); ``` Once the transaction confirms, the indexer picks up the `Transfer` event, and a `Token` record appears on the collection. ```typescript const tokens = (await client.token.list({ highlightId, limit: 10 })).data!; console.log(`${tokens.pagination.total} tokens minted so far`); ``` ## What to learn next - **Gated sales** — require an allowlist or token ownership. See [gated sales](/guides/developer/gated-sales/). - **Dutch auctions** — scheduled price drops. See [sale types](/concepts/sale-types/). - **Ranked auctions** — sealed-bid, on-chain. See [ranked auctions](/guides/developer/ranked-auctions/). - **Series with unique tokens** — upload a zip, let collectors choose. See [series](/concepts/series/). - **Generative** — deploy HTML/JS code that renders at mint time. See [generative](/concepts/generative-art/). --- # Ranked auctions Source: https://docs.highlightv2.xyz/guides/developer/ranked-auctions/ > Sealed-bid, on-chain ranked auctions — configure, take bids, settle, and claim. A ranked auction is a sealed-bid auction that runs fully on-chain. Bidders submit over the sale window; when the sale ends, the top `maxTotal` bids win. Optional: rebates, so all winners pay the same clearing price. Ranked auctions are **on-chain only** — there's no gated variant. They're supported for `LimitedEdition`, standard `Series`, and `Limited` `GenerativeSeries`. See the [support matrix](/reference/support-matrix/) for exact combinations. ## Configuration ```typescript const sale = (await client.collection.addSale({ highlightId, type: "RANKED_AUCTION", accessMode: "PUBLIC", startAt: new Date().toISOString(), endAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), price: "0.01", // reserve price, used for on-chain min currency: "ETH", maxPerTransaction: 1, maxPerWallet: 5, maxTotal: 100, // winners count paymentRecipient: creatorAddress, typeConfig: { reserveBidAmount: "0.01", rebateWinningBids: true, }, })).data!; ``` `typeConfig` fields: - `reserveBidAmount` — minimum bid, as a decimal string of the currency's major unit (e.g. `"0.01"` ETH). - `rebateWinningBids` — when `true`, all winners pay the clearing price (lowest winning bid); higher bids are rebated. ## Bidding (three steps) Each bid is a three-step flow: build, submit on-chain, confirm with the backend. ```typescript // 1. Build the bid transaction const bid = (await client.collection.bidSale({ highlightId, saleId: sale.id, bidAmount: "0.05", })).data!; // bid is { chainId, to, data, value } — a flat contract-execution shape. // 2. Submit it on-chain const bidHash = await walletClient.sendTransaction({ to: (bid.to ?? undefined) as `0x${string}` | undefined, data: bid.data as `0x${string}`, value: BigInt(bid.value ?? "0"), }); await publicClient.waitForTransactionReceipt({ hash: bidHash }); // 3. Tell the backend about the confirmed bid so it appears in auction state await client.collection.confirmBid({ highlightId, saleId: sale.id, txHash: bidHash, }); ``` `confirmBid` is called **after every bid** (not at auction-end). It records the on-chain bid in the backend's ranked-auction ledger so it shows up in collection/sale reads and is eligible to win. To **update** an existing bid, include the `bidId` emitted by the on-chain bid event. The `bidId` isn't in the `bidSale` SDK response — it's in the `EditionEdgeRankedAuctionBid` log on the receipt of the first bid transaction. Read it from the receipt, persist it client-side, then pass it back: ```typescript await client.collection.bidSale({ highlightId, saleId: sale.id, bidAmount: "0.06", bidId: existingBidIdFromReceipt, }); ``` ## Claiming after the auction ends Once `endAt` passes, winning bidders claim their tokens via `claimSale` — the same call used for fixed-price and Dutch auctions: ```typescript const claim = (await client.collection.claimSale({ highlightId, saleId: sale.id, amount: 1, })).data!; const claimHash = await walletClient.sendTransaction({ to: (claim.to ?? undefined) as `0x${string}` | undefined, data: claim.data as `0x${string}`, value: BigInt(claim.value ?? "0"), gas: 500_000n, }); ``` The backend verifies the caller is on the winning ledger, builds a claim against the mechanic contract, and (if `rebateWinningBids`) issues the rebate in the same transaction. Calling `claimSale` before `endAt` on a ranked auction returns `AuctionNotEndedError` (400). ## Ranked auction state The indexer tracks bids in real time. You can observe state via the collection get call — the `sale` object includes the current high bid and validity hash for the auction's bid tree. For server-side code that needs to react to bids, the `packages/highlight` backend maintains per-chain indexer DOs that watch `RankedAuction` events. You don't call the indexer directly; you just poll `client.collection.get` at whatever cadence your UI needs. ## Failure modes | Error | When | |---|---| | `SaleTypeNotSupportedError` (400) | Ranked auction on an unsupported collection (open editions, 1-of-1, collectors-choice series, open generative) | | `InvalidTypeConfigError` (400) | Missing `reserveBidAmount` or `rebateWinningBids` | | `AuctionNotEndedError` (400) | `claimSale` called before `endAt` on a ranked-auction sale | | `NoValidBidsError` (400) | Caller has no valid winning bids when trying to claim | | Ranked + Gated | Rejected at validation — 400 with a schema error | ## Related - [Sale types concepts](/concepts/sale-types/) — ranked auction overview - [Support matrix](/reference/support-matrix/) — exact valid combinations - [Sales reference](/sdk/sales/) — full sale API surface - [Errors: sale errors](/reference/errors/#sale-errors) — typed failures --- # Clients overview Source: https://docs.highlightv2.xyz/sdk/clients/ > Every sub-client on the HighlightClient — what it's for and where to learn more. `@highlightxyz/sdk` is a generated client: one TypeScript class per OpenAPI tag, hung as lazy getters off `HighlightClient`. Every method is typed end-to-end — parameters, responses, and errors — from the backend's Zod schemas. ```typescript import { createHighlightClient } from "@highlightxyz/sdk"; const client = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", auth: () => accessToken, security: [{ type: "http", scheme: "bearer" }], }); ``` ## The sub-clients | Client | Methods | Docs | |---|---|---| | `client.collection` | `list`, `create`, `get`, `finalizeBaseUri`, `updateEditionDetails`, `updateSeriesDetails`, `updateGenerativeDetails`, `getSales`, `addSale`, `updateSale`, `deleteSale`, `claimSale`, `bidSale`, `confirmBid`, `confirmSale`, `deploy`, `deployStatus`, `deployConfirm` | [Collections](/sdk/collections/), [Sales](/sdk/sales/) | | `client.token` | `list`, `get` | [Tokens](/sdk/tokens/) | | `client.gate` | `list`, `get`, `create`, `update`, `remove` | [Gates](/sdk/gates/) | | `client.media` | `createUploadSession`, `upload`, `process`, `get`, `listChildren`, `getChild`, `publish`, `delete` | [Media](/sdk/media/) | | `client.mechanic` | `list`, `get`, `create`, `update`, `addDeployment`, `removeDeployment` | [Mechanics](/sdk/mechanics/) | | `client.contract` | `list`, `get` | [Contracts](/sdk/contracts/) | | `client.config` | `chains`, `systemContract` | [Config](/sdk/config/) | | `client.user` | `signin` | [Auth reference](/reference/auth/) | | `client.user.siwe` | `nonce` | [Auth reference](/reference/auth/#sign-in-with-siwe-wallet) | | `client.apiKey` | `list`, `create`, `revoke` | [Auth reference](/reference/auth/#api-keys) | | `client.health` | `get` | — | A few sub-clients exist purely for infra/admin (indexer routes hang directly off `HighlightClient` as `postIndexerChainsByChainIdActivate` etc.) — you won't normally call them from client code. ## Response shape Every method returns a discriminated result: ```typescript type SdkResponse = { data?: T; // populated on 2xx error?: unknown; // populated on non-2xx — typed NamedError payload response?: Response; // the underlying fetch Response }; ``` No exceptions on non-2xx. Your code decides whether to throw. ```typescript const res = await client.collection.get({ highlightId }); if (res.error) { throw new Error(`Failed (${res.response?.status}): ${JSON.stringify(res.error)}`); } const collection = res.data; ``` For an ergonomic `unwrap` helper, see the pattern in [`packages/testing/src/helpers.ts`](https://github.com/highlight-xyz/sdk). ## Type imports All enums and entity types are re-exported from the root: ```typescript import { CollectionType, ContractStandard, SaleType, SaleAccessMode, GateMatchMode, MediaKind, MediaScope, MediaAssetStatus, GenerativeEditionType, } from "@highlightxyz/sdk"; import type { HighlightClient, HighlightClientConfig, CollectionListResponses, CollectionGetResponses, } from "@highlightxyz/sdk"; ``` Per-operation response types are named `Responses` and `Errors` — e.g. `CollectionCreateResponses`, `MediaUploadErrors`. ## Related - [SDK setup](/sdk/setup/) — installation and client configuration - [Developer quick start](/getting-started/developer-quick-start/) — first call - [REST API](/rest-api/operations/user-signin/) — auto-generated route schemas --- # Collections Source: https://docs.highlightv2.xyz/sdk/collections/ > Create, list, and manage collections via the SDK. The client.collection sub-client covers the full collection lifecycle plus its sales and deploy workflow. `client.collection` covers the full collection lifecycle. The auto-generated REST schema is at [REST API → Collection](/rest-api/operations/collection-list/) — this page is a usage overlay. ## List ```typescript const res = await client.collection.list({ blockchains: [8453, 84532], testnet: false, types: ["OpenEdition", "LimitedEdition"], status: "Live", page: 1, limit: 20, sortBy: "CREATED_DATE", sortDirection: "DESC", }); ``` `blockchains`, `testnet`, and `types` are **required**. Unbounded listing isn't supported. ## Create ```typescript const res = await client.collection.create({ name: "My Edition", description: "An open edition collection", type: "OpenEdition", logoMediaId: "media-uuid", image: "https://...", contract: { chainId: 8453, name: "MYED", symbol: "MYED", standard: "ERC721", }, }); ``` Creates a **draft** collection and either creates a new draft contract (via `contract`) or binds to an existing one (via `contractId`). Not both. Optional: `royalty: { address, amount }` and `tokenManager` (third-party manager address). ## Get ```typescript const res = await client.collection.get({ highlightId }); ``` Public for `Live` collections; requires ownership for `Draft`. ## Type-specific details After create, fill in type-specific fields via the matching endpoint: ```typescript await client.collection.updateEditionDetails({ highlightId, edition: { size: 0, name, description, imageMediaId, attributes }, }); await client.collection.updateSeriesDetails({ highlightId, series: { isCollectorsChoice: false, size: 50, assetsMediaId }, }); await client.collection.updateGenerativeDetails({ highlightId, generative: { size: 100, codeMediaId, editionType: "Limited", captureSettings: { trigger: "Delay", delay: 5000, viewPortWidth: 1000, viewPortHeight: 1000, }, }, }); ``` Only the update endpoint matching the collection's `type` is valid. Others return `InvalidTypeSpecificDetailsError`. ## Finalize metadata to Arweave Once a drop is complete (sold out, ended, or manually frozen), publish the metadata directory to Arweave: ```typescript await client.collection.finalizeBaseUri({ highlightId }); ``` Idempotent — safe to call repeatedly. ## Sales, deploy, and claim These sub-operations live on the same `client.collection` sub-client but are documented separately: - **Sales** — [sdk/sales](/sdk/sales/) - **Deploy workflow** — [guides/developer/deploy-workflow](/guides/developer/deploy-workflow/) ## Statuses | Status | Meaning | |---|---| | `Draft` | Configured, not yet deployed | | `InProgress` | Deploy transaction submitted | | `Live` | Contract deployed; sales are claimable | | `Migrated` | Imported from an external source | | `Disabled` | Frozen; no further mints | ## Related - [Collections concept](/concepts/collections/) - [Mint an NFT end-to-end](/guides/developer/mint-first-nft/) - [Support matrix](/reference/support-matrix/) - [REST API: collection](/rest-api/operations/collection-list/) --- # Config Source: https://docs.highlightv2.xyz/sdk/config/ > Read supported chains and system contract addresses. Both endpoints are public. `client.config` exposes platform-wide configuration — which chains the service supports and which system contracts live where. Both endpoints are public (no auth). ## List supported chains ```typescript const res = await client.config.chains(); // Array: each with chainId, name, testnet flag, rpcAlias, currencies, ... ``` Use this to populate chain-pickers, show a currency's decimals, or filter collections by chain. ## Get a system contract address ```typescript const res = await client.config.systemContract({ chainId: 8453, type: "MintManager", }); // res.data.address ``` ## System contract types All the named system contracts the platform tracks: - `CollectionFactory` - `MintManager` - `AuctionManager` - `PermissionsRegistry` - `EditionsMetadataRenderer` - `NonTransferableTokenManager` - `ConsensualNonTransferableTokenManager` - `ConsensualNonTransferableTokenManager2` - `GenerativeSeriesImplementation` - `SeriesImplementation` - `RandomSeriesImplementation` - `MultipleEditionsImplementation` - `MultipleEditionsDFSImplementation` - `SingleEditionImplementation` - `SingleEditionDFSImplementation` - `EditionsDFS1155Implementation` - `MinimalForwarder` - `Observability` - `ObservabilityV2` - `MintFeeOracle` - `GengineObservability` - `ReferralManager` - `RazorWorksImplementation` - `BitstreamSeedManager` Returns `SystemContractNotFoundError` (500) when no record exists for the chain + type combination. ## Related - [Contracts concept](/concepts/contracts/) - [REST API: config](/rest-api/operations/config-chains/) --- # Contracts Source: https://docs.highlightv2.xyz/sdk/contracts/ > Read contract records with client.contract. Deploy operations live on client.collection. `client.contract` is read-only — it lists and gets existing contract records. The deployment workflow lives on `client.collection` — see [deploy workflow](/guides/developer/deploy-workflow/). ## List ```typescript const res = await client.contract.list({ limit: 20, cursor: "optional-next-cursor", }); ``` Cursor-paginated. Scoped to the authenticated account. ## Get ```typescript const res = await client.contract.get({ contractId }); ``` ## Deployment Deploy operations live on `client.collection`: ```typescript await client.collection.deploy({ highlightId }); const status = await client.collection.deployStatus({ highlightId }); await client.collection.deployConfirm({ highlightId, txHash: "0x..." }); ``` See the [deploy workflow guide](/guides/developer/deploy-workflow/) for the full state machine and polling pattern. ## Contract implementations The platform derives the contract implementation from the collection type + token standard: | Collection type | ERC721 | ERC1155 | |---|---|---| | `OpenEdition` | `SingleEditionDFSImplementation` | `EditionsDFS1155Implementation` | | `LimitedEdition` / `OneOfOne` | `MultipleEditionsDFSImplementation` | `EditionsDFS1155Implementation` | | `Series` (Collectors Choice) | `SeriesImplementation` | — | | `Series` (Standard) | `RandomSeriesImplementation` | — | | `GenerativeSeries` | `GenerativeSeriesImplementation` | — | See [support matrix](/reference/support-matrix/) for reusability and the full combination set. ## Related - [Contracts concept](/concepts/contracts/) - [Collections](/sdk/collections/) - [Deploy workflow](/guides/developer/deploy-workflow/) - [REST API: contract](/rest-api/operations/contract-list/) --- # Gates Source: https://docs.highlightv2.xyz/sdk/gates/ > Create, update, and delete access control gates with client.gate. Gates are reusable bundles of conditions that restrict who can mint from a sale. A gate is owned by an account; attach a gate to a sale by setting `accessMode: "GATED"` and `gateId` when creating the sale. For the full access-controlled sale flow, see [gated sales](/guides/developer/gated-sales/). ## List ```typescript const res = await client.gate.list(); ``` Lists gates owned by the authenticated account. ## Get ```typescript const res = await client.gate.get({ gateId }); ``` ## Create ```typescript const res = await client.gate.create({ name: "Holder Gate", matchMode: "ALL", // "ALL" (AND) | "ANY" (OR); default "ALL" conditions: [ { type: "TOKEN_OWNERSHIP", config: { contractAddress: "0x...", chainId: 8453, minAmount: 1, // optional, defaults to 1 }, }, ], }); ``` ## Update Replaces the entire gate. Partial updates are not supported for `conditions`: ```typescript await client.gate.update({ gateId, name: "Renamed", matchMode: "ANY", conditions: [ /* full replacement */ ], }); ``` ## Delete ```typescript await client.gate.remove({ gateId }); ``` Gates referenced by a live sale cannot be deleted. ## Condition types Every condition has `type` and `config`. On-chain conditions require a `chainId`. ### ALLOWLIST ```typescript { type: "ALLOWLIST", config: { addresses: ["0x1111...", "0x2222..."], }, } ``` ### TOKEN_OWNERSHIP ```typescript { type: "TOKEN_OWNERSHIP", config: { contractAddress: "0x...", chainId: 8453, minAmount: 1, // optional }, } ``` ### SPECIFIC_TOKEN ```typescript { type: "SPECIFIC_TOKEN", config: { contractAddress: "0x...", chainId: 8453, tokenId: "42", // string }, } ``` ### TOKEN_ATTRIBUTE ```typescript { type: "TOKEN_ATTRIBUTE", config: { contractAddress: "0x...", chainId: 8453, traitType: "Background", traitValue: "Gold", }, } ``` ### CURRENCY_BALANCE ```typescript { type: "CURRENCY_BALANCE", config: { chainId: 8453, contractAddress: "0x...", // optional — omit for native currency minBalance: "0.1", // decimal string of the currency's major unit }, } ``` ### FARCASTER_FOLLOW ```typescript { type: "FARCASTER_FOLLOW", config: { fid: 12345, }, } ``` ## Constraints - `RANKED_AUCTION + GATED` is rejected at sale validation. - Gate condition checks run at **claim time**, not gate-creation time — token-ownership conditions can fail later if the caller transfers out. - Gates can be reused across many sales. ## Related - [Gates concept](/concepts/gates/) - [Gated sales guide](/guides/developer/gated-sales/) — attach + claim flow - [Errors: gate errors](/reference/errors/#gate-errors) - [REST API: gate](/rest-api/operations/gate-list/) --- # Mechanics Source: https://docs.highlightv2.xyz/sdk/mechanics/ > Discover built-in sale mechanics and register custom ones with client.mechanic. A mechanic is the on-chain sale engine — e.g. the Dutch auction or ranked auction contract. Built-in mechanics are seeded by the platform; you can also register your own. ## List ```typescript // All mechanics (built-in + custom for this account) const res = await client.mechanic.list(); // Filter to just custom mechanics (or just built-ins) const custom = await client.mechanic.list({ type: "CUSTOM" }); ``` No auth required. ## Get ```typescript const res = await client.mechanic.get({ mechanicId }); ``` ## Built-in types | Type | What it does | |---|---| | `DISCRETE_DUTCH_AUCTION` | Step-based Dutch auction with scheduled prices | | `RANKED_AUCTION` | Sealed-bid auction with optional rebate | | `SEED_BASED_MINT` | Standard minting with seed-based token assignment | | `GASLESS` | Gas-sponsored minting via meta-transactions | | `CUSTOM` | User-registered custom mechanic | The first four are seeded and immutable. You cannot create, update, or delete them. ## Register a custom mechanic ```typescript const res = await client.mechanic.create({ slug: "my-custom-mechanic", name: "My Custom Mechanic", description: "Description of the mechanic's behavior", configSchema: { /* JSON Schema for typeConfig validation */ }, encodingAbi: [ /* ABI for encoding on-chain calldata */ ], }); ``` Reference the returned `mechanicId` when creating `CUSTOM` sales. ## Update ```typescript await client.mechanic.update({ mechanicId, name: "Renamed", description: "...", configSchema: { /* ... */ }, }); ``` Built-in mechanics can't be updated. `slug` is immutable. ## Chain deployments A mechanic needs at least one deployment per chain it's usable on. ```typescript await client.mechanic.addDeployment({ mechanicId, chainId: 8453, address: "0xDeployedMechanicAddress", }); ``` Remove a deployment (the `chainId` goes in the body, not the URL): ```typescript await client.mechanic.removeDeployment({ mechanicId, chainId: 8453, }); ``` ## Using a mechanic in a sale For custom sales: ```typescript await client.collection.addSale({ // ... base fields ... type: "CUSTOM", mechanicId: "your-mechanic-uuid", typeConfig: { // validated against the mechanic's configSchema }, }); ``` Built-in mechanics are wired automatically when you pick `DUTCH_AUCTION` or `RANKED_AUCTION` — you don't set `mechanicId` for those. ## Related - [Mechanics concept](/concepts/mechanics/) - [Sale types](/concepts/sale-types/) - [Sales](/sdk/sales/) - [REST API: mechanic](/rest-api/operations/mechanic-list/) --- # Media Source: https://docs.highlightv2.xyz/sdk/media/ > Upload, process, archive, and enumerate media assets with client.media. `client.media` handles every file that touches the platform: token images, series asset bundles, and generative code. For the end-to-end pattern, see [media upload flow](/guides/developer/media-upload/). ## MediaKind Every asset declares its kind up front — kind is **not** inferred from MIME type: | Kind | Use for | |---|---| | `MediaKind.File` | Single blob (image, video, audio, metadata JSON) | | `MediaKind.Directory` | Zip archive — extracted after upload | ```typescript import { MediaKind } from "@highlightxyz/sdk"; ``` ## Create an upload session ```typescript const res = await client.media.createUploadSession({ kind: MediaKind.File, fileName: "artwork.png", mimeType: "image/png", fileSize: 1024000, // scope: "CollectionLogo" | "CollectionAsset" | ... (optional) }); // { data: { media: Media.Entity, upload: { url, method, headers } } } ``` Returns a signed upload URL. `fileSize` is validated against the per-kind limit up front (see [size limits](#size-limits)). ## PUT the bytes You can either use `fetch` directly against `upload.url` or call `client.media.upload` which does the same with the SDK's auth headers: ```typescript // Preferred: direct PUT await fetch(session.upload.url, { method: session.upload.method, headers: { ...session.upload.headers, authorization: `Bearer ${accessToken}`, }, body: buffer, }); ``` ## Drive processing After PUT, `File` assets transition straight to `Ready`. `Directory` assets trigger extraction and go `Pending` → `Processing` → `Ready`. If you need to force progression (rare — mostly for retrying a failed Directory extraction): ```typescript await client.media.process({ mediaId }); ``` Idempotent. `Ready` and `Processing` assets are returned as-is. ## Get status ```typescript const res = await client.media.get({ mediaId }); // res.data.status: "Pending" | "Processing" | "Ready" | "Failed" // res.data.url: string | null (resolved URL when Ready) // res.data.locations: Array<{ provider, role, ref, ... }> // res.data.archive?: { totalFiles, manifest: [...] } (Directory only) ``` Poll at ~2s intervals until `status === "Ready"` or `"Failed"`. ## Enumerate a Directory's children ```typescript const res = await client.media.listChildren({ mediaId, limit: 100, cursor: nextCursor, }); // res.data.entries[].key, .size, .mimeType, .id ``` Fetch one child's full Entity (including a resolved URL): ```typescript const res = await client.media.getChild({ mediaId, path: "index.html", }); // res.data.url, .status, .locations ``` ## Archive to Arweave ```typescript await client.media.publish({ mediaId }); ``` - `File` → publishes the blob to Arweave. - `Directory` → publishes a manifest over all children. Idempotent. Adds an `Arweave` entry to the asset's `locations`. For collection metadata specifically, prefer `client.collection.finalizeBaseUri` — it archives the token-metadata directory and flips the on-chain baseURI in one call. ## Delete ```typescript await client.media.delete({ mediaId }); ``` Returns `MediaInUseError` (409) if the media is referenced by a deployed contract or live generative code. Arweave locations are immutable and remain on-chain. ## Size limits | Kind | Max | |---|---| | File (image) | 50 MB | | File (audio) | 250 MB | | File (video) | 500 MB | | File (metadata) | 1 MB | | Directory | 1 GB | ## Related - [Media & storage concept](/concepts/media-and-storage/) - [Media upload flow](/guides/developer/media-upload/) — end-to-end pattern - [Errors: media errors](/reference/errors/#media-errors) — typed failures - [REST API: media](/rest-api/operations/media-create-upload-session/) --- # Sales Source: https://docs.highlightv2.xyz/sdk/sales/ > Configure sales, build claim and bid transactions via client.collection's sale methods. Sales are managed through methods on the `client.collection` sub-client. The canonical route schema is at [REST API → Collection](/rest-api/operations/collection-add-sale/) — this page is a usage overlay focused on SDK patterns and constraints. See [support matrix](/reference/support-matrix/) for which collection + sale + access combinations are valid. Anything outside it is rejected. ## List sales on a collection ```typescript const res = await client.collection.getSales({ highlightId }); ``` Returns every sale currently attached (draft or live). ## Add a sale ```typescript await client.collection.addSale({ highlightId, type: "FIXED_PRICE", accessMode: "PUBLIC", startAt: new Date().toISOString(), price: "0.01", currency: "ETH", maxPerTransaction: 5, maxPerWallet: 10, maxTotal: 1000, paymentRecipient: "0x...", // Optional: // endAt: "2026-12-31T00:00:00Z", // gateId: "gate-uuid", // required when accessMode = "GATED" // gasSponsored: true, // only FIXED_PRICE + PUBLIC // mechanicId: "mech-uuid", // only type = "CUSTOM" // typeConfig: { ... }, // required for DUTCH_AUCTION, RANKED_AUCTION, CUSTOM // collectorMessage: "Welcome!", // customMintFee: "0.0001", // name: "Early access", }); ``` Rules: - **Draft collection** — one sale at a time. Delete the existing draft before adding a new one. - **Live collection** — only gated sales can be added (they have no on-chain component). Public sales on live collections aren't yet implemented. ## Sale-type-specific `typeConfig` ### Dutch auction ```typescript await client.collection.addSale({ // ... base fields ... type: "DUTCH_AUCTION", typeConfig: { scheduledPrices: ["1.0", "0.75", "0.5", "0.25"], priceDropInterval: { hours: 1, minutes: 0 }, }, }); ``` 1–5 price levels. Price drops on the interval until it reaches the floor. ### Ranked auction ```typescript await client.collection.addSale({ // ... base fields ... type: "RANKED_AUCTION", typeConfig: { reserveBidAmount: "0.1", rebateWinningBids: true, }, }); ``` Gated ranked auctions are rejected. See [ranked auctions guide](/guides/developer/ranked-auctions/). ### Custom ```typescript await client.collection.addSale({ // ... base fields ... type: "CUSTOM", mechanicId: "your-mechanic-uuid", typeConfig: { /* whatever the mechanic expects */ }, }); ``` Register mechanics with `client.mechanic.create` — see [sdk/mechanics](/sdk/mechanics/). ## Update a sale ```typescript await client.collection.updateSale({ highlightId, saleId, price: "0.02", paused: true, // ...other mutable fields }); ``` **Immutable** (need delete + re-add to change): `type`, `accessMode`, `gateId`, `gasSponsored`, `mechanicId`, `typeConfig`. **On a live public sale**, only `name`, `collectorMessage`, `customMintFee`, and `paused` can be updated. On-chain fields (`price`, `startAt`, `endAt`, `currency`, limits, `paymentRecipient`) can't be changed yet. ## Delete a sale ```typescript await client.collection.deleteSale({ highlightId, saleId }); ``` Only draft sales. Live sales must be paused or end-dated via update. ## Claim (mint) ```typescript const res = await client.collection.claimSale({ highlightId, saleId, amount: 1, // tokenIds: [7, 42], // only for choose-token Series sales }); ``` Returns a signed contract execution (`res.data.tx`). The caller submits the transaction on-chain. ## Bid (ranked auction only) ```typescript const res = await client.collection.bidSale({ highlightId, saleId, bidAmount: "0.5", // bidId: existingBidId, // to update an existing bid }); ``` See [ranked auctions guide](/guides/developer/ranked-auctions/) for the full flow including settlement. ## Confirm bid Called **after each bid tx confirms on-chain** (not at auction end) to record the bid in the backend's ranked-auction ledger so it's eligible to win. Pass the hash of the bid transaction: ```typescript await client.collection.confirmBid({ highlightId, saleId, txHash }); ``` The end-of-auction claim uses `claimSale` — see [ranked auctions guide](/guides/developer/ranked-auctions/). ## Confirm sale (future) `client.collection.confirmSale` exists in the SDK surface but returns `400` until on-chain vector registration for live-collection public sales is implemented. ## Related - [Sales concept](/concepts/sales/) — what a sale is - [Sale types](/concepts/sale-types/) — pricing strategies - [Gated sales](/guides/developer/gated-sales/) — access-controlled flows - [Ranked auctions](/guides/developer/ranked-auctions/) — auction flow - [Support matrix](/reference/support-matrix/) — valid combinations - [Errors: sale errors](/reference/errors/#sale-errors) — typed failures --- # SDK setup Source: https://docs.highlightv2.xyz/sdk/setup/ > Install, configure, and use @highlightxyz/sdk against api.highlightv2.xyz. `@highlightxyz/sdk` is a fully typed TypeScript client, auto-generated from the backend's OpenAPI schema. Every parameter, response, and error type matches the API exactly — there's no manual drift. ## Install ```bash bun add @highlightxyz/sdk ``` ESM-only. Works on Bun 1.1+, Node 20+, and modern browsers. ## Create a client Minimum config — public endpoints only: ```typescript import { createHighlightClient } from "@highlightxyz/sdk"; const client = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", }); ``` With a JWT (from [SIWE or Privy sign-in](/reference/auth/)): ```typescript const client = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", auth: () => accessToken, security: [{ type: "http", scheme: "bearer" }], }); ``` The `auth` callback is invoked on every request, so you can refresh a cached token lazily. With an [API key](/reference/auth/#api-keys) (for server-to-server): ```typescript const client = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", auth: () => process.env.HIGHLIGHT_API_KEY!, security: [{ type: "http", scheme: "bearer" }], }); ``` API keys use the same bearer scheme as JWTs — just pass the plaintext. ## Making requests All methods take a single flat parameters object. Path, query, and body params are merged: ```typescript // Public read const collection = (await client.collection.get({ highlightId: "abc123" })).data; // Auth'd list const list = (await client.collection.list({ blockchains: [8453], testnet: false, types: ["OpenEdition"], })).data; // Auth'd create const created = (await client.collection.create({ name: "My Drop", description: "Genesis collection", type: "OpenEdition", logoMediaId: mediaUuid, image: imageUrl, contract: { chainId: 8453, name: "DROP", symbol: "DROP", standard: "ERC721" }, })).data; ``` ## Response shape Every method returns a discriminated result — no exceptions on non-2xx: ```typescript type SdkResponse = { data?: T; error?: unknown; response?: Response; }; ``` Pattern: ```typescript const res = await client.collection.get({ highlightId }); if (res.error) { if (res.response?.status === 404) return null; throw new Error(`${res.response?.status}: ${JSON.stringify(res.error)}`); } return res.data; ``` Error payloads are typed [NamedError](/reference/errors/) shapes — match on `res.error.name` for fine-grained dispatch. ## Sub-clients All domain operations hang off `client`: | Client | Purpose | Docs | |---|---|---| | `client.collection` | Collections, sales, deploy | [Collections](/sdk/collections/), [Sales](/sdk/sales/), [Deploy workflow](/guides/developer/deploy-workflow/) | | `client.token` | Minted token queries | [Tokens](/sdk/tokens/) | | `client.gate` | Access control | [Gates](/sdk/gates/) | | `client.media` | Upload & storage | [Media](/sdk/media/), [Upload flow](/guides/developer/media-upload/) | | `client.mechanic` | Built-in & custom mechanics | [Mechanics](/sdk/mechanics/) | | `client.contract` | Contract records | [Contracts](/sdk/contracts/) | | `client.config` | Chains & system contracts | [Config](/sdk/config/) | | `client.user.signin`, `client.user.siwe.nonce` | Sign in | [Auth](/reference/auth/) | | `client.apiKey` | API key management | [Auth: API keys](/reference/auth/#api-keys) | | `client.health` | Liveness check | — | See [clients overview](/sdk/clients/) for the full method list. ## Type imports Re-exported enums and types from the root: ```typescript import { CollectionType, ContractStandard, SaleType, SaleAccessMode, GateMatchMode, MediaKind, MediaScope, MediaAssetStatus, GenerativeEditionType, } from "@highlightxyz/sdk"; import type { HighlightClient, HighlightClientConfig, Client, CollectionListResponses, CollectionGetResponses, MediaCreateUploadSessionResponses, } from "@highlightxyz/sdk"; ``` ## Related - [Developer quick start](/getting-started/developer-quick-start/) — first call - [Clients overview](/sdk/clients/) — every method at a glance - [Authentication](/reference/auth/) — SIWE, Privy, API keys - [Errors](/reference/errors/) — typed error catalog --- # Tokens Source: https://docs.highlightv2.xyz/sdk/tokens/ > Read minted tokens from a collection. client.token exposes list and get — token records are created by the indexer, not by callers. `client.token` is read-only. Token records are populated automatically by the indexer when it observes on-chain transfers — you don't create them directly via the SDK. ## List tokens ```typescript const res = await client.token.list({ highlightId, page: 1, limit: 20, sort: "mintedAt", // "tokenId" | "name" | "mintedAt" | "createdAt" order: "desc", // "asc" | "desc" ownerAddress: "0x...", // optional — filter by owner search: "rare", // optional — substring match on name }); ``` For public (`Live`) collections, this works without auth. Draft collections require ownership. ## Get a single token ```typescript const res = await client.token.get({ highlightId, tokenId: "1", // the on-chain token ID as a string }); ``` ## Token shape ```typescript { id: "uuid", collectionId: "uuid", tokenId: "1", name: "Token Name", description: "Token description", image: "https://...", animationUrl: "https://...", externalUrl: "https://...", attributes: [ { trait_type: "Background", value: "Gold" }, { trait_type: "Rarity", value: "Rare" }, ], properties: { /* freeform */ }, imageMediaId: "uuid", animationMediaId: "uuid", mintedAt: "2026-04-01T00:00:00Z", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-04-01T00:00:00Z", } ``` Token metadata shape depends on the collection type: - **Editions** — every token shares the same metadata. - **Series** — each token has unique metadata, uploaded as part of the series asset bundle. - **Generative** — metadata (including captured image) is materialized from the generator at mint time. ## Related - [Tokens & metadata concept](/concepts/tokens-and-metadata/) - [Collections](/sdk/collections/) - [REST API: token](/rest-api/operations/token-list/) --- # Authentication Source: https://docs.highlightv2.xyz/reference/auth/ > SIWE, Privy, and API keys — every way to authenticate against the Highlight API. Most write operations against the Highlight API require authentication. Public read endpoints (getting a live collection, listing its tokens, querying platform config) work without any credentials. The backend accepts two credential types: 1. **JWT access tokens** — obtained by signing in with a wallet (SIWE) or a Privy token. Short-lived. 2. **API keys** — long-lived, user-created credentials for server-to-server use. Both are passed as `Authorization: Bearer ` headers. The SDK wires this for you. ## Sign in with SIWE (wallet) SIWE (Sign In With Ethereum, [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361)) is the canonical way to authenticate a wallet. It's a two-step flow: ```typescript import { createHighlightClient } from "@highlightxyz/sdk"; import { createSiweMessage } from "viem/siwe"; const client = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", }); // 1. Get a short-lived nonce const nonceRes = await client.user.siwe.nonce(); const { nonce } = nonceRes.data!; // 2. Build and sign a SIWE message with the user's wallet const message = createSiweMessage({ address: account.address, chainId: 1, domain: "your-app.example.com", nonce, uri: "https://your-app.example.com", version: "1", issuedAt: new Date(), expirationTime: new Date(Date.now() + 10 * 60 * 1000), }); const signature = await account.signMessage({ message }); // 3. Exchange the signed message for a JWT const signinRes = await client.user.signin({ body: { providerType: "Siwe", message, signature }, }); const { tokens, user } = signinRes.data!; // tokens.accessToken, tokens.refreshToken ``` The `domain` in your SIWE message must match the host the backend expects. During local development this is typically the API's public host. ## Sign in with Privy If your app uses [Privy](https://privy.io) for wallet auth, trade the Privy `id_token` for a Highlight JWT: ```typescript const signinRes = await client.user.signin({ body: { providerType: "Privy", token: privyIdToken }, }); const { tokens, user } = signinRes.data!; ``` The backend verifies the Privy token out-of-band and issues its own JWT. ## Using the token Configure the SDK once, then every subsequent call is authenticated: ```typescript const authedClient = createHighlightClient({ baseUrl: "https://api.highlightv2.xyz", auth: () => accessToken, security: [{ type: "http", scheme: "bearer" }], }); // Every call now carries Authorization: Bearer await authedClient.collection.list({ blockchains: [8453], testnet: false, types: ["OpenEdition"], }); ``` The `auth` callback is invoked on every request, so you can refresh tokens in-flight by reading from a cache or async source. ## Access token lifetime JWT access tokens expire after **15 minutes**. Refresh by calling `signin` again (or by holding a refresh token and implementing your own rotation). For long-running processes, re-authenticate on a timer well before the 15-minute boundary — the e2e tests re-sign every 10 minutes. ## API keys For server-to-server automation where repeated SIWE is inconvenient, mint an API key once and reuse it. ```typescript // Must be authenticated with a JWT to create keys const keyRes = await authedClient.apiKey.create({ name: "prod-automation", expiresAt: "2027-01-01T00:00:00Z", // optional }); const { plaintext, apiKey } = keyRes.data!; // plaintext is shown ONCE — save it now ``` List and revoke: ```typescript const keys = await authedClient.apiKey.list(); await authedClient.apiKey.revoke({ apiKeyId: "key-uuid" }); ``` Use an API key like a JWT — pass the returned `plaintext` as the bearer token. ## Public endpoints The following endpoints work without any `Authorization` header: - `GET /health` - `GET /config/chains` - `GET /config/system-contract` - `GET /mechanic` and `GET /mechanic/:id` - `POST /user/signin` and `POST /user/signin/siwe/nonce` - `GET /collection/:highlightId` — when the collection is `Live` (drafts require ownership) - `GET /collection/:highlightId/tokens` — when the parent collection is public - `GET /media/:mediaId` — via the image-transformation path, not the metadata endpoint Everything else requires auth. ## Authorization errors - `401 Unauthorized` — missing/invalid/expired token. - `403 Forbidden` — authenticated but not the owner of the resource (e.g. someone else's draft collection). See the [error reference](/reference/errors/) for the full list of typed errors. ## Related - [Developer quick start](/getting-started/developer-quick-start/) — First authenticated call. - [SDK setup](/sdk/setup/) — Client configuration reference. - [REST API: user-signin](/rest-api/operations/user-signin/) — Full OpenAPI schema. --- # Errors Source: https://docs.highlightv2.xyz/reference/errors/ > Every typed error the Highlight API can raise, mapped to its HTTP status and feature. Every domain failure in the Highlight backend raises a named error with a stable `name` (the class name) and a typed payload. Route handlers map each error class to an HTTP status code — your client can either switch on the status code or pattern-match against the error payload shape. ## Error response shape Typed domain errors come back as JSON: ```json { "name": "CollectionNotFoundError", "message": "Collection 0x...abc not found", "data": { "highlightId": "0x...abc" } } ``` - `name` — the class name. Unique across the platform; safe to switch on. - `message` — human-readable; safe to log, not safe to parse. - `data` — structured payload (e.g. `highlightId`, `chainId`). Untyped runtime failures (unhandled exceptions, 5xx from upstream services) come back as a generic `500` with a more minimal shape. ## Collection errors | Error | Status | When | |---|---|---| | `CollectionNotFoundError` | 404 | `highlightId` doesn't exist | | `CollectionNotPublicError` | 404 | Collection exists but is `Draft` and caller isn't the owner | | `CollectionNotOwnedError` | 403 | Caller authenticated but not the collection owner | | `CollectionNotDraftError` | 400 | Attempting a draft-only operation on a live collection | | `CollectionContractNotFoundError` | 404 | Referenced `contractId` doesn't exist | | `CollectionContractNotOwnedError` | 403 | Caller doesn't own the referenced contract | | `InvalidCollectionInputError` | 400 | Schema-level validation failure (e.g. missing logo) | | `InvalidTypeSpecificDetailsError` | 400 | Edition/series/generative details don't match the collection type | | `CollectionMediaNotFoundError` | 404 | Referenced `mediaId` doesn't exist | | `CollectionMediaNotOwnedError` | 403 | Caller doesn't own the referenced media | | `CollectionMediaUploadNotReadyError` | 400 | Media isn't `Ready` yet — wait for processing | | `CollectionMediaInvalidPurposeError` | 400 | Media kind doesn't fit the slot (e.g. Directory as logo) | | `CollectionMediaNotOnchainReadyError` | 400 | Media not published to archive provider yet | | `BaseUriNotSetError` | 400 | Cannot finalize metadata before base URI is computed | ## Sale errors | Error | Status | When | |---|---|---| | `SaleNotFoundError` | 404 | `saleId` doesn't exist or isn't on this collection | | `SaleNotPublicError` | 403 | Attempting to act on a gated sale without gate pass | | `SaleNotLiveError` | 400 | Claim/bid against a non-live sale | | `SaleAlreadyExistsError` | 409 | Draft collections allow one sale at a time | | `SaleCannotDeleteLiveError` | 400 | Live sales cannot be deleted — pause or end-date | | `SaleTypeNotSupportedError` | 400 | Combination not in the [support matrix](/reference/support-matrix/) | | `InvalidSalePriceError` | 400 | Price is zero, negative, or malformed | | `InvalidTypeConfigError` | 400 | `typeConfig` fails schema validation for the sale type | | `ChooseTokenNotSupportedError` | 400 | `tokenIds` passed for a non-series or non-fixed/dutch sale | | `AuctionNotEndedError` | 400 | Cannot claim a ranked-auction sale before `endAt` | | `NoValidBidsError` | 400 | Claiming a ranked auction but the caller has no valid winning bids | | `VectorIdRequiredError` | 400 | Public Dutch auction claim needs a registered vectorId | | `SaleGateCheckFailedError` | 403 | Caller's wallet doesn't satisfy the gate conditions | | `AccountContextRequiredError` | 401 | Claim/bid without auth | | `AccountWalletRequiredError` | 400 | Account has no wallet configured | | `ContractAddressRequiredError` | 400 | Gated claim against an undeployed contract | | `SaleMechanicNotFoundError` | 400 | Referenced `mechanicId` doesn't exist | | `SaleMechanicDeploymentNotFoundError` | 400 | Mechanic has no deployment on this chain | ## Gate errors | Error | Status | When | |---|---|---| | `GateNotFoundError` | 404 | `gateId` doesn't exist | | `GateNotOwnedError` | 403 | Caller doesn't own the gate | | `ConditionTypeNotSupportedError` | 400 | Unknown `type` in a condition config | ## Media errors | Error | Status | When | |---|---|---| | `MediaNotFoundError` | 404 | `mediaId` doesn't exist | | `MediaNotOwnedError` | 403 | Caller doesn't own the media | | `MediaUploadNotFoundError` | 404 | No upload session on this media | | `MediaTooLargeError` | 400 | `fileSize` exceeds per-kind limit | | `MediaKindMismatchError` | 400 | Uploading bytes of the wrong MIME for the declared kind | | `MediaInUseError` | 409 | Deleting media referenced by a deployed contract | | `MediaStorageUnavailableError` | 502 | Upstream storage provider rejected the write | | `DirectoryNotFinalizedError` | 400 | Using a Directory before extraction is `Ready` | | `LocationNotFoundError` | 404 | No storage location record for this media | | `ProviderUploadFailedError` | 502 | Upload to R2/Arweave failed; safe to retry | ## Zip extraction errors | Error | Status | When | |---|---|---| | `ExtractionNotFoundError` | 404 | No extraction record for this media | | `ExtractionAlreadyExistsError` | 409 | Extraction already running | | `ExtractionNotRetryableError` | 400 | Extraction is in a state that can't be retried | | `ExtractionMediaNotZipError` | 400 | Media kind isn't Directory | | `ExtractionMediaNotUploadedError` | 400 | Bytes never PUT to the upload URL | | `ExtractionMediaNotFoundError` | 404 | Parent media record missing | ## SIWE errors | Error | Status | When | |---|---|---| | `SiweInvalidMessageError` | 400 | Message isn't valid EIP-4361 | | `SiweDomainMismatchError` | 400 | Message `domain` doesn't match the backend's expected host | | `SiweChainNotAllowedError` | 400 | `chainId` in the message isn't on the allow-list | | `SiweNonceInvalidError` | 400 | Nonce is expired, unknown, or reused | | `SiweInvalidSignatureError` | 400 | Signature doesn't recover to the message's `address` | ## User / API key errors | Error | Status | When | |---|---|---| | `ProviderVerificationFailedError` | 400 | Privy token rejected | | `InsufficientCredentialsError` | 400 | Sign-in request missing required fields | | `ApiKeyNotFoundError` | 404 | `apiKeyId` doesn't exist | | `ApiKeyNotOwnedError` | 403 | Caller doesn't own the key | | `ApiKeyRevokedError` | 401 | Key was revoked | | `ApiKeyExpiredError` | 401 | Key's `expiresAt` has passed | | `InvalidApiKeyError` | 401 | Key doesn't match any record | ## Deployment errors | Error | Status | When | |---|---|---| | `DeploymentNotFoundError` | 404 | No deployment for this collection | | `DeploymentAlreadyExistsError` | 409 | Deploy already initiated; poll status instead | ## Shared infrastructure errors These map to `500` because they indicate misconfiguration or upstream failure, not client error. | Error | Status | When | |---|---|---| | `SystemContractNotFoundError` | 500 | No system-contract record for chain + type | | `RpcUrlNotConfiguredError` | 500 | Chain has no RPC URL in config | | `EncryptionKeyNotConfiguredError` | 500 | KMS key missing | | `ExecutorKeyNotConfiguredError` | 500 | Platform executor key missing for this chain | | `ExecutorKeyDecryptionFailedError` | 500 | KMS decrypt failed | | `SigningKeyNotConfiguredError` | 500 | Sale-signing key missing | | `SiweConfigNotConfiguredError` | 500 | SIWE domain/allow-list missing | ## Shared validation errors | Error | Status | When | |---|---|---| | `ChainNotFoundError` | 400 | `chainId` isn't supported | | `CurrencyNotSupportedError` | 400 | `currency` not configured on this chain | ## Handling errors in the SDK The generated SDK returns a discriminated `{ data, error, response }` shape — no exceptions on non-2xx. Example: ```typescript const res = await client.collection.get({ highlightId }); if (res.error) { if (res.response?.status === 404) { // CollectionNotFoundError or CollectionNotPublicError return null; } if (res.response?.status === 403) { // CollectionNotOwnedError throw new Error("Not authorized to view this collection"); } throw res.error; } return res.data; ``` For finer-grained dispatch, inspect `res.error.name`: ```typescript if (res.error && typeof res.error === "object" && "name" in res.error) { switch (res.error.name) { case "CollectionNotFoundError": case "CollectionNotPublicError": return null; case "CollectionNotOwnedError": throw new Error("Forbidden"); default: throw res.error; } } ``` ## Related - [Support matrix](/reference/support-matrix/) — What combinations are valid (most 400s come from invalid combinations). - [Authentication](/reference/auth/) — Causes of 401s and 403s. --- # Glossary Source: https://docs.highlightv2.xyz/reference/glossary/ > Vocabulary used across the Highlight platform, SDK, and docs. ## Access mode The scope of who can mint from a sale. Either `PUBLIC` (anyone) or `GATED` (only wallets that pass the gate's conditions). Gated sales have no on-chain vector or mechanic — mints are authorized by an executor-signed claim. ## Archive provider The long-term storage destination for media. Today: Arweave. Used to freeze a collection's metadata directory once the drop is complete (see `collection.finalizeBaseUri`). ## Claim The act of minting a token from a sale. In SDK terms: `client.collection.claimSale({ highlightId, saleId, amount, tokenIds? })`. Returns a `ContractExecution` the caller submits on-chain. ## Collection The top-level unit. Every collection has a type, a contract, optionally one or more sales, and minted tokens. Has a `highlightId` (public, URL-safe) and a UUID (internal). ## Collection type One of `OpenEdition`, `LimitedEdition`, `OneOfOne`, `Series`, `GenerativeSeries`. Determines which contract implementation is selected at deploy time. ## Contract The deployed smart contract that holds tokens. Maps 1:1 or 1:N to collections depending on whether the contract type is reusable. The platform picks the contract type from the collection type plus token standard. ## Contract implementation The specific on-chain implementation (e.g. `EditionsDFS1155Implementation`, `SeriesImplementation`). Derived; not set directly. ## Deploy workflow The multi-step state machine that takes a draft collection to `Live`. Steps: `Validate` → `UploadMetadata` [→ `UploadCode` for generative] → `GenerateTransaction` → `AwaitTransaction` → `RecordTransaction`. Overall status: `Pending`, `InProgress`, `Completed`, `Failed`. ## Directory (media) A `MediaKind` representing an extracted zip archive. Used for series asset bundles and generative code. After PUT-upload, the backend extracts the archive and exposes children at `/media/:mediaId/children/:path`. ## Executor key The platform-held secret used to sign gated mints and certain on-chain admin actions. Scoped per-chain, stored encrypted. Never exposed to clients. ## File (media) A `MediaKind` representing a single blob — an image, video, audio, or metadata JSON. ## Gate A reusable access-control rule bundle owned by an account. Contains one or more conditions and a `matchMode` (`ALL` or `ANY`). Attached to a sale by setting `accessMode: "GATED"` and `gateId`. ## Gate condition One rule inside a gate: `ALLOWLIST`, `TOKEN_OWNERSHIP`, `SPECIFIC_TOKEN`, `TOKEN_ATTRIBUTE`, `CURRENCY_BALANCE`, or `FARCASTER_FOLLOW`. ## Gas sponsorship Creator-paid gas for the collector's mint transaction. Only valid for `FIXED_PRICE` + `PUBLIC` sales. Encoded via the `gasSponsored: true` flag. ## HighlightId The public, URL-safe identifier for a collection. Distinct from the internal UUID. Appears in every `/collection/:highlightId/...` route. ## Mechanic An on-chain sale contract plugged into the `MintManager`. Built-ins: `DISCRETE_DUTCH_AUCTION`, `RANKED_AUCTION`, `SEED_BASED_MINT`, `GASLESS`. Custom mechanics can be registered via `client.mechanic.create` and deployed per chain. ## MintManager The root on-chain contract every claim goes through. Resolves the mechanic or vector for the sale and executes the mint. ## Provider type The auth provider used at sign-in. Either `Siwe` or `Privy`. Determines the body shape of `/user/signin`. ## Ranked auction Sealed-bid auction where highest bidders win tokens, optionally with a rebate so all winners pay the clearing price. On-chain only — gated ranked auctions are rejected. ## Sale A purchase mechanism attached to a collection. Has `type`, `accessMode`, price fields, supply limits, an optional `gateId` (when gated), and an optional `mechanicId` (for custom). Immutable fields can't be changed — delete and re-create to change the sale's shape. ## Series A collection where every token has unique metadata. Two variants: `Collectors Choice` (collector picks `tokenIds`) and `Standard` (sequential). ## Standard (token) ERC-721 or ERC-1155. Editions support both; series and generative are always ERC-721. For ERC-1155 editions, mint fees are halved. ## Token A minted NFT. Belongs to a collection, identified by its on-chain `tokenId`. Carries `name`, `description`, `image`, `attributes`, and optional freeform `properties`. ## TypeConfig Sale-type-specific configuration. Dutch auctions carry `scheduledPrices` and `priceDropInterval`; ranked auctions carry `reserveBidAmount` and `rebateWinningBids`; custom sales carry whatever the registered mechanic expects. ## Vector An on-chain purchase record registered with `MintManager` for public sales. Has a `vectorId`. Gated sales have no vector; Dutch auctions use a `mechanicVectorId` instead. --- # Support matrix Source: https://docs.highlightv2.xyz/reference/support-matrix/ > Exactly which combinations of collection type, sale type, and access mode Highlight supports. Anything not on this matrix is rejected. This page is the canonical source of truth for what the platform supports. Every row here is covered by end-to-end tests in `@highlight/testing`; anything not on this page will be rejected by validation or deployment. If you're an AI agent, this is your ground truth. The schema docstring in `packages/testing/matrices/` is regenerated with every release — this page mirrors it. ## Dimensions Every sale is parameterized by four things: | Dimension | Values | |---|---| | **Collection type** | `OpenEdition`, `LimitedEdition`, `OneOfOne`, `Series`, `GenerativeSeries` | | **Token standard** | `ERC721`, `ERC1155` (editions only — series and generative are always `ERC721`) | | **Sale type** | `FIXED_PRICE`, `DUTCH_AUCTION`, `RANKED_AUCTION`, `CUSTOM` | | **Access mode** | `PUBLIC`, `GATED` | ## Universal rules 1. **Ranked Auction + Gated is always rejected.** Ranked auctions are on-chain only; there's no executor-signed path. 2. **Ranked Auction is only valid for finite-supply collections.** `LimitedEdition`, standard `Series`, and `Limited` generative. Never for open editions, 1-of-1s, collectors-choice series, or open generative. 3. **Gas sponsorship requires `FIXED_PRICE` + `PUBLIC`.** Any other combination rejects the `gasSponsored: true` flag. 4. **Gated sales have no on-chain vector or mechanic.** The backend signs claim transactions with an executor key; the collector submits. 5. **Every deployment uses `ObservabilityV2`.** No exceptions. ## Editions The contract implementation is derived from `type` + `tokenStandard`: | Collection type | ERC721 contract | ERC1155 contract | |---|---|---| | `OpenEdition` | `SingleEditionDFSImplementation` (non-reusable) | `EditionsDFS1155Implementation` (reusable) | | `LimitedEdition` | `MultipleEditionsDFSImplementation` (reusable) | `EditionsDFS1155Implementation` (reusable) | | `OneOfOne` | `MultipleEditionsDFSImplementation` (reusable) | `EditionsDFS1155Implementation` (reusable) | **Reusable** contracts can back multiple collections; non-reusable contracts get one per collection. ### Supported sale combinations | Type | Sale | Access | Mint method | Supported | |---|---|---|---|---| | OpenEdition | Fixed Price | PUBLIC | `vectorMint721` | ✓ | | OpenEdition | Fixed Price | GATED | `gatedNumMint` | ✓ | | OpenEdition | Dutch Auction | PUBLIC | `mechanicMintNum` | ✓ | | OpenEdition | Dutch Auction | GATED | `gatedNumMint` | ✓ | | OpenEdition | Ranked Auction | * | — | ✗ | | LimitedEdition | Fixed Price | PUBLIC | `vectorMint721` | ✓ | | LimitedEdition | Fixed Price | GATED | `gatedNumMint` | ✓ | | LimitedEdition | Dutch Auction | PUBLIC | `mechanicMintNum` | ✓ | | LimitedEdition | Dutch Auction | GATED | `gatedNumMint` | ✓ | | LimitedEdition | Ranked Auction | PUBLIC | `mechanicMintNum` | ✓ | | LimitedEdition | Ranked Auction | GATED | — | ✗ | | OneOfOne | Fixed Price | PUBLIC | `vectorMint721` | ✓ | | OneOfOne | Fixed Price | GATED | `gatedNumMint` | ✓ | | OneOfOne | Dutch Auction | PUBLIC | `mechanicMintNum` | ✓ | | OneOfOne | Dutch Auction | GATED | `gatedNumMint` | ✓ | | OneOfOne | Ranked Auction | * | — | ✗ | Valid for both `ERC721` and `ERC1155`. ERC1155 splits the on-chain mint fee in half (matching the `MintFeeOracle` behavior). ## Series Series collections come in two variants. The variant drives the contract implementation and the mint method. | Variant | Contract | Collector picks token? | |---|---|---| | Collectors Choice | `SeriesImplementation` | Yes — collector passes `tokenIds` | | Standard | `RandomSeriesImplementation` | No — sequential/random assignment | Both contracts are **non-reusable**. ### Collectors Choice Every mint path is executor-signed (even public sales) because collectors pick specific token IDs. | Sale | Access | Mint method | Supported | |---|---|---|---| | Fixed Price | PUBLIC | `gatedSeriesMintChooseToken` | ✓ | | Fixed Price | GATED | `gatedSeriesMintChooseToken` | ✓ | | Dutch Auction | PUBLIC | `mechanicMintChoose` | ✓ | | Dutch Auction | GATED | `gatedSeriesMintChooseToken` | ✓ | | Ranked Auction | * | — | ✗ | ### Standard | Sale | Access | Mint method | Supported | |---|---|---|---| | Fixed Price | PUBLIC | `vectorMint721` | ✓ | | Fixed Price | GATED | `gatedNumMint` | ✓ | | Dutch Auction | PUBLIC | `mechanicMintNum` | ✓ | | Dutch Auction | GATED | `gatedNumMint` | ✓ | | Ranked Auction | PUBLIC | `mechanicMintNum` | ✓ | | Ranked Auction | GATED | — | ✗ | ## Generative Single contract, two variants driven by `maxTotal`: | Variant | Rule | Contract | |---|---|---| | Open | `maxTotal = 0` | `GenerativeSeriesImplementation` (non-reusable) | | Limited | `maxTotal > 0` | `GenerativeSeriesImplementation` (non-reusable) | | Variant | Sale | Access | Mint method | Supported | |---|---|---|---|---| | Open | Fixed Price | PUBLIC | `vectorMint721` | ✓ | | Open | Fixed Price | GATED | `gatedNumMint` | ✓ | | Open | Dutch Auction | PUBLIC | `mechanicMintNum` | ✓ | | Open | Dutch Auction | GATED | `gatedNumMint` | ✓ | | Open | Ranked Auction | * | — | ✗ | | Limited | Fixed Price | PUBLIC | `vectorMint721` | ✓ | | Limited | Fixed Price | GATED | `gatedNumMint` | ✓ | | Limited | Dutch Auction | PUBLIC | `mechanicMintNum` | ✓ | | Limited | Dutch Auction | GATED | `gatedNumMint` | ✓ | | Limited | Ranked Auction | PUBLIC | `mechanicMintNum` | ✓ | | Limited | Ranked Auction | GATED | — | ✗ | ## Sale lifecycle rules - A **draft collection** may have **one sale at a time**. Delete before replacing. - After a collection goes **Live**, gated sales can still be added (they have no on-chain component and activate immediately). Adding a new **public** sale to a live collection is not yet implemented — it returns `400`. - Live sales **cannot be deleted**. Pause or set `endAt` instead. - The following fields are **immutable** once a sale exists: `type`, `accessMode`, `gateId`, `gasSponsored`, `mechanicId`, `typeConfig`. Re-create the sale to change them. - On a **live public sale**, only `name`, `collectorMessage`, `customMintFee`, and `paused` can be updated. Other on-chain fields require a new sale. ## Source The canonical matrix definitions live in the repo at: - `packages/testing/matrices/edition-functionality-matrix.md` - `packages/testing/matrices/series-functionality-matrix.md` - `packages/testing/matrices/generative-functionality-matrix.md` Each row in the tables above corresponds to a test ID (e.g. `E01`, `S03`, `G08`) that's exercised end-to-end on Sepolia by the `@highlight/testing` package.