Rising Tide Developers
SDKCore

rtClient.smartProducts

SDK methods for creating, reading, and managing smart products across the PAGE, EVENT, PREDICTION and GAME domains.

The substrate-canonical service for SmartProduct CRUD. Per-domain write methods (createPage, createEvent, createPrediction, etc.) route to domain-specific endpoints; reads are cross-domain.

Read surface

rtClient.smartProducts.list(params?)

Paginated list across all domains. Returns PaginatedResponse<SmartProduct> - { data: { items, pagination: { page, limit, total, totalPages } }, error }.

const res = await rtClient.smartProducts.list({
  domain: "PAGE",
  productTypeSlug: "haiku",
  status: "PUBLISHED",
  page: 1,
  limit: 20,
  sortBy: "createdAt",
  sortOrder: "desc",
  include: ["productType", "styleSet"],
});
const items = res.data.items;

Filters (SmartProductListParams):

FilterTypeDescription
domain'PAGE' | 'EVENT' | 'PREDICTION' | 'GAME'Filter by product domain
productTypeSlugstringFilter by product type slug (resolves to id server-side)
productTypeIdnumberFilter by numeric product type id
statusProductStatusFilter by lifecycle status
ownerAddressstringFilter by creator's Stacks address
collectionIdnumberFilter by parent collection
pagenumber1-indexed page number
limitnumberPage size (default 20, max 100)
searchstringFree-text search over title/description
sourceAppIdnumberFilter by the app that created the product
sortBy'createdAt' | 'updatedAt' | 'publishedAt' | 'title' | 'currentInstances'Sort key
sortOrder'asc' | 'desc'Sort direction
includeSmartProductListInclude[]Server-side relation expansion: 'productType', 'styleSet', or 'tags'. CSV-serialized at the SDK boundary.

rtClient.smartProducts.getById(id, include?)

Single SmartProduct by numeric id.

const { data: product } = await rtClient.smartProducts.getById(123, [
  "productType",
  "styleSet",
  "stickers",
  "tags",
  "instances",
  "contributions",
  "appListings",
  "pageDetails",
  "bracketDetails",
]);

include accepts SmartProductDetailInclude[] - any subset of these 10 keys: 'productType', 'styleSet', 'stickers', 'tags', 'instances', 'contributions', 'appListings', 'pageDetails', 'bracketDetails', 'gameLobbyDetails'.


rtClient.smartProducts.getInstances(productId, params?)

Paginated ProductInstance rows for a smart product.

const res = await rtClient.smartProducts.getInstances(productId, {
  status: "CONFIRMED",
  limit: 50,
});

rtClient.smartProducts.getInstance(productId, instanceId)

Single ProductInstance by parent + instance id.


rtClient.smartProducts.getContributions(productId)

All ProductContribution rows for a smart product, ordered by frozen position ASC (or insertion order before freeze).

const { data: contributions } = await rtClient.smartProducts.getContributions(productId);

rtClient.smartProducts.listEditions(productId)

The pending-aware edition read: confirmed editions overlaid with live mempool pending ones and the failed registry, every item tagged pending, confirmed or failed, plus counts (seen, pending, failed, confirmed). Only confirmed counts for anything truth-gated. The correlationId from a commands.submit result is the txId of the matching item. usePendingAwareEditions from @rising-tide/ui/react polls it.


rtClient.smartProducts.getSentimentSnapshot(productId)

The off-chain rollup of a product's sentiment signals: count, mean, median, distribution, distinctAccounts, updatedAt. A product with no signals yet returns a zeroed snapshot rather than an error.


rtClient.smartProducts.getPrivateNote(productId, instanceId)

The note a purchaser left for the product owner at mint (the private-note capability). Owner only: anyone else is refused with 403. Returns { note: null } when the purchaser left none. This is the only read that returns it - public instance reads redact it. See productInstances.stageNote for the write.

PAGE domain writes

rtClient.smartProducts.createPage(input)

Create a PAGE-domain SmartProduct in DRAFT. Hits POST /api/smart-products.

const { data: page } = await rtClient.smartProducts.createPage({
  title: "morning rain",
  subtitle: "a haiku",
  content: { line1: "...", line2: "...", line3: "..." },
  author: user.stxAddress,
  productTypeSlug: "haiku",
  styleSetId: 7,
  maxEditions: 100,
  minFeeStx: "1000000",
  minFeeSbtc: "100",
  appId: APP_ID,
  contributions: {
    styleSetId: 7,
    collaborators: [{ address: "SP...", displayName: "Co", percentage: 10 }],
    fundingGroups: [{ id: 3, percentage: 5 }],
  },
});

Server materializes TEMPLATE_ARTIST, STICKER_ARTIST, COLLABORATOR, FUNDING_GROUP, and CREATOR (remainder) rows from the contributions payload in the same transaction.

Composing products

A page's on-chain behavior is a composition of capabilities (additive behavior - sentiment, a reader note, detachable storage, a one-time deploy fee) and constraints (guards - author-cannot-mint, a mint window). Specify it two ways:

  • A named recipe - a server-owned preset. Pass recipe: { name, cause, ... } and the protocol expands it into the capabilities, constraints, and the contribution pick.
  • Explicit composition - pass capabilities and constraints directly. The self-serve path: compose a product the protocol never pre-blessed, no recipe required.
const { data: page } = await rtClient.smartProducts.createPage({
  title: "a note in a bottle",
  content: { content: "..." },
  author: user.stxAddress,
  productTypeSlug: "basic-page",
  maxEditions: 100,
  minFeeStx: "1000000",
  minFeeSbtc: "100",
  appId: APP_ID,
  // Explicit composition - behavior only.
  capabilities: [
    { id: "feedback" },
    { id: "deploy-fee", config: { feeSats: 2500 } },
  ],
  constraints: [{ id: "author-cannot-mint" }],
});

A third array, attributes, names what the product is (product-info, location). Every product composes product-info and the server supplies it from the fields the input already carries, so most callers never set it. See Capabilities, Constraints and Attributes.

deploy-fee charges in sats of sBTC unless its config names currency: "STX", in which case feeSats is read as microSTX.

recipe and capabilities/constraints are mutually exclusive. Pass the same composition to rtClient.contracts.processWritingContract so one set of capabilities drives both contract generation and the create call.

You compose behavior; the protocol controls the economics. Capabilities and constraints describe only what the product does. Every payment decision - the revenue split rates, and the deploy-fee recipient - is derived server-side from trusted sources (the StyleSet royalty, the bound ContractTemplate, the App record, protocol policy) and frozen into the contract. A client can never assert who gets paid: you name a deploy fee's amount (feeSats), never its recipient - that resolves to the app's payout wallet, or the protocol fee address when no payout wallet is set. The chainhook position-verifier re-reads the deployed contract to confirm the frozen split.


rtClient.smartProducts.updatePage(id, patch)

PATCH metadata or deploy state on a PAGE.

await rtClient.smartProducts.updatePage(productId, {
  title: "morning pond",
  deployTxId: "0xabc...",
  deployTxStatus: "PENDING",
});

rtClient.smartProducts.deletePage(id)

Hard-delete a DRAFT PAGE. Authorized for the owner or holders of MANAGE_LISTINGS. PUBLISHED pages cannot be deleted - archive instead.

EVENT domain writes

Same shape - createEvent(CreateEventInput), updateEvent(id, patch), deleteEvent(id) - hitting /api/events. See CreateEventInput in the SDK types for the event-specific fields (locationId, startTime, endTime, maxReservations, etc.).

PREDICTION domain writes

rtClient.smartProducts.createPrediction(input)

Atomic create: SmartProduct + BracketDetails + ProductContribution rows (derived from the asset-pick input). Hits POST /api/brackets.

const { data: bracket } = await rtClient.smartProducts.createPrediction({
  productTypeId: BRACKET_TYPE_ID,
  contractAddress: user.stxAddress,
  contractName: "bracket-001",
  ownerAddress: user.stxAddress,
  sourceAppId: APP_ID,
  title: "March 2026",
  bracketSize: 64,
  hasPlayIn: false,
  styleSetId: 11,
  minFeeStx: "5000000",
  minFeeSbtc: "500",
  isTransferable: true,
  allowOffers: false,
  productConfig: { bracketSize: 64, numTeams: 64, teams: [], games: [], results: {}, isLocked: false, maxBracketsPerWallet: 1, poolPercentage: 50 },
  contributions: {
    styleSetId: 11,
    stickers: [{ stickerId: 9, x: 100, y: 200, scale: 1, rotation: 0 }],
  },
});

Bracket contributions sum to (100 - poolPercentage) server-side via targetSumForProduct - the pool share is paid by tournament settlement, not by distribute-fees.


rtClient.smartProducts.updatePrediction(id, patch)

PATCH a DRAFT bracket. Contract identity (contractAddress, contractName) is only mutable while DRAFT; metadata is mutable at any status.


rtClient.smartProducts.deletePrediction(id)

Hard-delete a DRAFT bracket. Rejects 409 INVALID_STATE on any non-DRAFT status. Used by cleanup-on-failure when contract deploy errors out.


rtClient.smartProducts.publishPrediction(id)

Manual DRAFT -> PUBLISHED for a bracket. The deploy chainhook auto-publishes on confirmation; this is the operator escape hatch when a chain event was missed.

Cross-app listing

await rtClient.smartProducts.getListedApps(productId);      // ApiResponse<App[]>
await rtClient.smartProducts.listOnApps(productId, [1, 2]); // idempotent
await rtClient.smartProducts.removeAppListing(productId, appId);

Sticker placement

Sticker rows live alongside the smart product and carry their own STICKER_ARTIST ProductContribution. The server keeps both in sync.

const { data: sticker } = await rtClient.smartProducts.addSticker(productId, {
  stickerId: 12,
  x: 100,
  y: 200,
  scale: 1,
  rotation: 0,
});

await rtClient.smartProducts.updateStickerPlacement(productId, sticker.id, {
  x: 120,
  scale: 1.1,
});

await rtClient.smartProducts.removeSticker(productId, sticker.id);

All three reject 409 PRODUCT_DEPLOYED once deployTxId is set or status !== "DRAFT" - the on-chain creator-info map would diverge.