Contracts and Signing
Generate Clarity source for a product type, serve token URIs, and server-sign transactions for custodial users.
Two different things live here: generating contract source, and signing transactions on behalf of a custodial user.
POST /api/contracts/generate returns Clarity source and never deploys anything: your client
broadcasts it, or, for a hosted account, hands it to deploy-contract. The /api/privy/sign/*
routes are the custodial path. The server holds the user's embedded wallet, so it signs and
broadcasts on the user's behalf, the deploy included; on these routes the protocol wallet is never the signer.
A bring-your-own-wallet user signs the same actions in their own wallet instead, using the
SDK's transaction builders.
Token URI routes are keyed on (address, name), never a slug. That pair is baked into an
immutable Clarity constant at deploy time, so a slug rename must never be able to break metadata
resolution.
Generating source
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/contracts/generate | app key + app access | Generate Clarity source. productType is one of haiku, tanka, basicpage, openletter, bracket, ticket or game-lobby. With mode: "deploy" and a smartProductId the split is rewritten from the product's frozen contributions, a client-supplied creators[] is refused, and the code is pinned for the deploy check. mode: "preview" writes and pins nothing. Limited to 30 requests a minute per IP. |
POST | /api/contracts/template/generate | app key + user, entitlement | Generate from a template without a draft product. For tooling. Needs MANAGE_CONTRACTS. |
GET | /api/templates/slots | app key + user, entitlement | The slots a template declares. Use it to check a composition fits before generating. Needs CREATE_ASSETS. |
Token URIs and metadata
The token routes are public: a wallet or marketplace reads them with no key, because the address and contract name are baked into the deployed contract.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/contracts/{address}/{name}/nft-asset-class | app key | The NFT asset class for a deployed contract. |
GET | /api/contracts/{address}/{name}/tokens/{index} | public | Token URI metadata for one edition. Keyed on (address, name), never a slug. |
GET | /api/contracts/{address}/{name}/tokens/{index}/thumbnail | public | Rendered thumbnail for one edition. |
GET | /api/contracts/writing/{deployer}/{name}/tokens/{index} | public | Token URI for a writing-family contract. |
GET | /api/contracts/writing/{deployer}/{name}/tokens/{index}/thumbnail | public | Thumbnail for a writing-family edition. |
Server signing for custodial users
These routes sign for a hosted account. The session check is Privy's, so a
bring-your-own-wallet token does not authenticate here: a wallet user signs the same actions with the
SDK's transaction builders. Each request is also throttled per user. Responses are
{ "success": true, "txId": "0x..." } (the deploy route adds its fee estimate) or
{ "success": false, "error": "...", "details": "..." }, not the usual envelope. A failed
entitlement or access check is the ordinary 403 envelope.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/privy/create-wallet | app key + user | Create an embedded wallet for a new user. |
POST | /api/privy/sign/deploy-contract | app key + user, entitlement, app access | Deploy a generated contract. Needs the CREATE_* entitlement of the draft's domain: CREATE_PAGES, CREATE_EVENTS, CREATE_BRACKETS or CREATE_GAMES. Refuses code that did not come from generation. |
POST | /api/privy/sign/publish-edition | app key + user, entitlement, app access | Collect an edition. Needs COLLECT_EDITIONS. |
POST | /api/privy/sign/publish-feedback-edition | app key + user, entitlement, app access | Collect an edition and leave a note, on a product composing feedback. |
POST | /api/privy/sign/publish-open-letter-edition | app key + user, entitlement, app access | Co-sign an open letter, with sentiment. |
POST | /api/privy/sign/transfer-edition | app key + user, entitlement | Transfer an edition. Needs COLLECT_EDITIONS. No app access is required: ownership never expires. |
POST | /api/privy/sign/burn-edition | app key + user, entitlement | Burn an edition. Needs COLLECT_EDITIONS. No app access is required. |
Domain and capability actions
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/privy/sign/make-reservation | app key + user, entitlement, app access | Reserve a ticket. Needs RSVP_EVENTS. |
POST | /api/privy/sign/validate-reservation | app key + user, entitlement, app access | Validate a ticket at the door. Needs CREATE_EVENTS. |
POST | /api/privy/sign/submit-bracket | app key + user, entitlement, app access | Submit a bracket entry. Needs SUBMIT_BRACKETS. |
POST | /api/privy/sign/bracket-action | app key + user, app access | One tournament action: lock, unlock, enter-results, clear-result, play-in-result, clear-play-in, finalize-scores, claim-prize or withdraw-pool. Owner actions also need CREATE_BRACKETS and the deploying address; scoring and claiming are open to anyone signed in with access. |
POST | /api/privy/sign/game-action | app key + user, app access | One game-lobby action: join, leave, start, move, claim, claim-draw or use-bomb. See Games. |
POST | /api/privy/sign/set-detached-uri | app key + user | Pin an edition's token URI to a permanent URL. Owner-gated. |
POST | /api/privy/sign/lock-detached | app key + user | Freeze that pin forever. Owner-gated and irreversible. |
POST | /api/privy/sign/set-max-supply | app key + user, entitlement, app access | Change the supply cap, where the supply constraint allows it. Needs CREATE_PAGES. |
POST | /api/privy/sign/set-product-info | app key + user, entitlement, app access | Update the product-info attribute. Needs CREATE_PAGES. |
POST | /api/privy/sign/faucet-stx | app key + user, app access | Testnet gas top-up, capped per person per day. Testnet only. |
POST | /api/privy/sign/faucet-sbtc | app key + user, app access | Testnet sBTC top-up, capped per person per day, and refused on mainnet. Closed to everyone but operators wherever earning is meant to be the only source. |
The delegate wallet
A person's delegate wallet is a second wallet, theirs, that an assistant they connect can spend
from. It collects for them through the rt-delegates registry: what it pays for is minted to the
person. The routes and the SDK name it delegateWallet.
Every route here runs on the person's own sign-in, and the session's wallet must be their main
wallet. A request carrying an assistant's grant is refused. Money only ever moves between the
person's two wallets, so no route takes a recipient.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/privy/delegate-wallet | app key + user | The delegate wallet's address, where it stands in the registry (CREATED, NAMED or RESIGNED), and its two balances, sats and ustx, each a decimal string, or null when it could not be read. Beside it, wallet: the most the main wallet can move in (maxSats, maxUstx, after the network fee feeUstx), null where a balance could not be read. { "delegate-wallet": null } when there is none yet. |
POST | /api/privy/sign/open-delegate-wallet | app key + user | Make the delegate wallet if there is none, and have the main wallet name it in the registry. Signs nothing for a delegate wallet already named. |
POST | /api/privy/sign/fund-delegate-wallet | app key + user | Send amountUstx (a positive whole number, as a number or a decimal string) from the main wallet to the delegate wallet, for the network fees its spends pay. Refused before signing, with 409 and code: "OVER_LIMIT", when it is more than maxUstx; 503 when the balance cannot be read. |
POST | /api/privy/sign/fund-delegate-wallet-sbtc | app key + user | Send amountSats from the main wallet to the delegate wallet, for it to spend. Refused before signing with 409: OVER_LIMIT over what the wallet holds, NO_FEE when it cannot pay the fee in STX; 503 when a balance cannot be read. |
POST | /api/privy/sign/empty-delegate-wallet | app key + user | Everything in the delegate wallet back to the main wallet: the sats, then the STX left after both network fees. Refused when a balance cannot be read. |
POST | /api/privy/sign/resign-delegate-wallet | app key + user | End the delegate wallet's standing in the registry. The delegate wallet signs when it holds the fee, and the main wallet otherwise. Its balance stays put. |
Connected assistants
A person connects an assistant, such as Claude, from the assistant itself: it sends them to the
hub's /connect page, which signs them in and completes the assistant's authorization. The
assistant never holds a Rising Tide key. It calls the Rising Tide guide (rt-mcp), and the guide
calls the routes below for it.
These run on the person's own sign-in. A request carrying an assistant is refused, so an assistant can neither connect nor disconnect itself, change what it may spend, or confirm through the person's link.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/assistants/connect | app key + user | Body { externalAuthId }, the code the authorization server put on the /connect link. Completes the authorization as this person and returns redirectUri, where to send them next. 403 for a signed-in person who has not joined; 400 when the link has expired. |
GET | /api/assistants | app key + user | The person's assistants: id, name, status (ACTIVE or REVOKED), spendEnabled, appIds, dailySats, perTapSats, dailyActions, spentSatsToday, connectedAt and revokedAt. Also apps, the apps the person may let an assistant spend in (the catalog's, where they may act themselves), and limits, the most a day or a tap may be set to. |
POST | /api/assistants/revoke | app key + user | Body { id }. Disconnects one assistant: its next request is refused, and it comes back only when the person connects it again, with spending off until they turn it on. |
POST | /api/assistants/settings | app key + user | Body { id, spendEnabled?, appIds?, dailySats?, perTapSats? }. What one assistant may spend: whether at all, in which apps, and how many sats a day and a tap. 400 for an app the person cannot use themselves, or an amount outside the limits. |
GET | /api/assistants/quote | app key + user | One quote, named by ?id=, for the confirm link: what it buys, the person's own words for a reply or a signature exactly as they will be written, the price in sats, who gets paid (names, no figures), and what the assistant has left today. 404 for anyone but the person it would spend for. |
POST | /api/assistants/quote/confirm | app key + user | Body { id }. The person confirms a quote from the link, for an assistant that draws no card. The same rule as the guide's tap, below. Returns txId. |
The rest answer the guide only. They need its trusted-caller key and an x-rt-assistant header it
signs with a secret it shares with rt-api, naming the person, the assistant and the path, within a
minute. The first such call for a person and an assistant creates the grant, with spending off.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/assistants/me/status | guide | Whether this assistant's grant is open: name and spendEnabled, or 403 when the person disconnected it. |
GET | /api/assistants/me/inbox | guide | The person's notification feed, across their apps or one (?appId=). Reading it marks nothing read. |
GET | /api/assistants/me/delegate-wallet | guide | The delegate wallet's two balances (null when unread), and what this assistant may still spend today under its grant. |
GET | /api/assistants/me/matches | guide | The person's games: a seat held at a table whose match has not started (status: 'SEATED', with canStart once the table is full), matches still being played and whose turn it is, finished matches with a pot or a drawn share still theirs (canClaim), and those that ended in the last week. |
GET | /api/assistants/me/logbook | guide | What the person made, what they keep, and what they have earned and spent in all, each currency in its own field. |
POST | /api/assistants/me/quotes | guide | Body { action: 'keep' | 'reply' | 'sign' | 'join', productId, note?, rating? }. A quote to keep a copy of a page, leave a reply on a bottle (note, required), sign a letter (rating 1-10, note optional) or take a seat at a game table (productId is the lobby, priced from its buy-in) from the person's delegate wallet: the action is the one the page's deployed mint takes; priced from the page in sats; who gets paid named from the page's frozen split; the person's words checked against the contract (length, characters) and returned as words exactly as they will be written, never rewritten; the budget checked without reserving. Single use, good for five minutes. 400 or 409 with words for the person when it cannot be quoted (spending off, the app not chosen, the delegate wallet not open or short, a page made before delegate wallets, the page taking another action, an author signing their own letter, words the contract cannot hold). |
POST | /api/assistants/me/spends | guide | Body { quoteId }. The person tapped the quote card. In one transaction holding the grant: the quote is this grant's, unused and unexpired; spending is on and the app chosen; the price fits one tap, the day's sats and the day's actions. Then the delegate wallet signs the mint with the quote's words, the copy, reply, signature or seat owned by the person and the delegate wallet's sats pinned to the quoted price, and it returns action and txId at broadcast. |
POST | /api/assistants/me/plays | guide | Body { lobbyId, action: 'start' | 'move' | 'claim', input?, match? }. The person tapped the board: start the match at a full table they sit at, make a move (input, a column or a square), or claim a won pot or a drawn share (match). Checked against the table before anything is signed (a seat, the turn, room on the board, the winner), so an illegal move is refused in words rather than aborting on chain for a fee. No sats move from the delegate wallet; each tap counts against the day's actions and costs a network fee in STX. The pot goes to the person's main wallet. Returns action, match and txId at broadcast. |
A spend that never broadcasts gives its price back. Once broadcast it follows its transaction: it holds its price while the chain has not answered and once it lands. One that aborts on chain gives its price back but still counts as one of the day's actions, because its network fee was paid. One that is never mined gives its price back, and holds it again if the chain later shows it mined.