Deploy workflow
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
Section titled “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 startedInProgress— workflow running (any step)Completed— contract is liveFailed— workflow hit a fatal error;errorfield holds the reason
The full loop
Section titled “The full loop”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
Section titled “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
Section titled “Getting the contract address after Completed”After Completed, the deployed contract address is attached to the collection:
const collection = (await client.collection.get({ highlightId })).data!;const address = collection.contract.address; // on the target chainFailure modes
Section titled “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
Section titled “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
Section titled “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
Section titled “After Completed: finalizeBaseUri”For collections whose metadata is meant to be frozen (e.g. a sold-out limited edition), call:
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
Section titled “Related”- Mint an NFT end-to-end — the full pipeline including deploy.
- Support matrix — what combinations pass
Validate. - Errors — the full catalog of deploy-time errors.
- REST API: collection-deploy — auto-generated schema.