Smart Products (developer)
Technical reference for the SmartProduct entity - schema, service layer, and API surface.
SmartProduct is the central protocol entity. It represents a deployable Clarity contract with associated off-chain metadata. It is a discriminated union: every row carries a productTypeId that determines the domain, plus an optional domain detail (pageDetails, eventDetails, bracketDetails, gameLobbyDetails) embedded by ?include=. A GAME lobby's seats and fee legs come from GET /api/games/{id}.
Schema (SmartProductBase)
interface SmartProductBase {
id: number;
productTypeId: number;
contractAddress: string; // empty string until deploy confirms
contractName: string;
ownerAddress: string;
title: string;
subtitle: string | null;
thumbnailUri: string | null;
slug: string | null;
status: ProductStatus; // DRAFT | PUBLISHED | LOCKED | SETTLED | FAILED | ARCHIVED
publishedAt: string | null;
lockedAt: string | null;
settledAt: string | null;
archivedAt: string | null;
minFeeStx: string; // BigInt-as-string per SDK convention
minFeeSbtc: string;
maxInstances: number | null; // null = unlimited
currentInstances: number;
isTransferable: boolean;
allowOffers: boolean;
sourceAppId: number; // App that created this product
deployTxId: string | null;
deployTxStatus: "PENDING" | "CONFIRMED" | "FAILED";
styleSetId: number | null;
manifestStorageId: string | null;
productConfig: { version: number } & Record<string, unknown>;
createdAt: string;
updatedAt: string;
// Optional embeds (`?include=...`):
productTags?: ProductTag[];
productStickers?: ProductSticker[];
}Four concrete variants extend this, each with a domain discriminator:
type SmartProductPage = SmartProductBase & { domain: "PAGE"; pageDetails?: PageDetails }
type SmartProductEvent = SmartProductBase & { domain: "EVENT"; eventDetails?: EventDetails }
type SmartProductPrediction = SmartProductBase & { domain: "PREDICTION"; bracketDetails?: BracketDetails }
type SmartProductGame = SmartProductBase & { domain: "GAME"; gameLobbyDetails?: GameLobbyDetails }
type SmartProduct = SmartProductPage | SmartProductEvent | SmartProductPrediction | SmartProductGameProductStatus transitions
| From | To | Condition |
|---|---|---|
DRAFT | PUBLISHED | Contract deployed successfully |
DRAFT | FAILED | Contract deployment failed |
PUBLISHED | LOCKED | PREDICTION domain only |
LOCKED | SETTLED | PREDICTION domain only |
DRAFT, PUBLISHED, LOCKED, FAILED or SETTLED | ARCHIVED | Manual action (LOCKED and SETTLED only in PREDICTION) |
Every domain except PREDICTION follows: DRAFT -> PUBLISHED -> ARCHIVED. See Status Machines for the full table.
API endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/smart-products | List (paginated, filterable by domain, status, productTypeId, productTypeSlug, ownerAddress, sourceAppId, collectionId, search) |
GET | /api/smart-products/:id | Get by numeric ID. Use ?include=instances,contributions,productType,appListings |
POST | /api/smart-products | Create a PAGE-domain product (status: DRAFT) |
PATCH | /api/smart-products/:id | Update metadata, deploy state or status, subject to the status machine |
DELETE | /api/smart-products/:id | Delete a draft |
POST | /api/smart-products/:id/publish | Manual DRAFT -> PUBLISHED, the operator escape hatch for a missed deploy event (the owner, or MANAGE_TOURNAMENTS) |
GET | /api/smart-products/:id/editions | The pending-aware edition read |
Contract source comes from POST /api/contracts/generate, not from this resource. Events and brackets are created through /api/events and /api/brackets.
SDK methods
rtClient.smartProducts.list(params?: SmartProductListParams)
rtClient.smartProducts.getById(id: number, include?: SmartProductDetailInclude[])
rtClient.smartProducts.createPage(input) // also createEvent, createPrediction
rtClient.smartProducts.updatePage(id, patch) // also updateEvent, updatePrediction
rtClient.smartProducts.publishPrediction(id) // manual publish; a lobby uses rtClient.gamesSee rtClient.smartProducts for the full surface.
Domain enforcement
Only PREDICTION products can be locked or settled, and they use the full six-state lifecycle; for any other domain, attempting it is refused with INVALID_STATUS_TRANSITION (409). A GAME lobby's matches have a lifecycle of their own - see Status Machines.