Token Lifecycle
Tokens are where most apps meet the chain: a loyalty point, a game asset, a stablecoin, an access pass. The Smart Engines token surface lets you run the full lifecycle without holding mint keys or writing chain-specific code. You describe what you want and which chain; the cluster prepares and signs the transaction with the entity’s threshold multisig.
One generic surface, chain in the body
Section titled “One generic surface, chain in the body”There are no chain-specific token URLs. Every token operation is a generic
endpoint on the validator, and the chain you want lives in the request body.
The validator resolves the correct per-chain implementation at runtime.
| Lifecycle step | Endpoint |
|---|---|
| Create | POST /api/v3/transactions/token/create/prepare |
| Mint | POST /api/v3/transactions/token/mint/prepare |
| Transfer | POST /api/v3/transactions/transfer/prepare |
| Burn | POST /api/v3/transactions/token/burn/prepare |
NFT variants follow the same shape (nft/mint/prepare, nft/transfer/prepare,
nft/burn/prepare, nft/collection/create/prepare, …).
“Prepare,” not “submit”
Section titled ““Prepare,” not “submit””Notice every route ends in /prepare. This is deliberate. Validators
prepare and sign transactions; they do not submit them as a side effect
of preparing. The prepare call returns the prepared/signed transaction bytes;
submission is a separate, explicit step.
Non-creation prepares additionally run through a canonical sign-gate that
applies the entity’s rules to the prepared bytes — a rule-denied prepare comes
back as HTTP 403, never as signed bytes. Creation operations
(token/create, topic/create, nft/collection/create) are exempt from the
sign-gate by design.
One exception: transfers broadcast server-side
Section titled “One exception: transfers broadcast server-side”The transfer path — used for entity withdraws — no longer hands the signed
bytes back for you to submit. Both
POST /api/v3/transactions/transfer/prepare (full mode) and
POST /api/v3/transactions/transfer/execute (partial mode) broadcast the
transaction server-side inside the validator and return the real on-chain
result instead of raw bytes:
{ "transactionId": "...", "status": "...", "broadcast": true }This replaces the old client-side submit path (a raw xrpl client on the dapp, a
@hashgraph/sdk re-sign on Hedera). The server-side broadcast is fail-closed
and, critically, runs strictly after the sign-gate has already allowed the
prepared bytes — a rule-denied transfer is rejected before anything reaches the
chain, never broadcast. It is wired for XRPL and Hedera, the two live
chains; partial-mode prepares that still need the owner co-signature via
execute keep returning bytes for the explicit submit step.
broadcast is terminal — branch on it first. When it comes back true,
transactionId is the real on-chain id and status is the chain’s execution
result (SUCCESS on Hedera, tesSUCCESS on XRPL); the transfer is done.
The host forwards broadcast and status verbatim from the validator on both
the prepare and the execute leg.
Withdraws: echo the idempotency key across the two steps
Section titled “Withdraws: echo the idempotency key across the two steps”An entity withdraw on the host is a two-step, caller-signed flow:
| Step | Endpoint |
|---|---|
| Prepare | POST /api/v3/baas/entities/:entityId/withdraw/prepare |
| Execute | POST /api/v3/baas/entities/:entityId/withdraw/execute |
Because it’s two calls, a retry or a double-click can turn one intended withdraw into two. To make the pair safe to retry, the flow supports an optional idempotency key that ties both steps to the same durable record on the validator:
- On prepare, you may pass your own
idempotencyKeyOptional. If you omit it, the host generates one for you. - Either way, the prepare response echoes the key back as its
idempotencyKeyfield. - Echo that same key on execute (the execute body’s
idempotencyKey). Both steps then resolve to the validator’s one durable idempotency record — arming its dedup, the stale-envelope guard, and the per-route cooldown exemption, so a re-sent withdraw is deduplicated instead of firing twice.
// 1) Prepare — omit idempotencyKey and let the host mint one (or supply your own).const prepared = await fetch( `${hostUrl}/api/v3/baas/entities/${entityId}/withdraw/prepare`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}` }, body: JSON.stringify({ chain: 'xrpl', destination: 'rDest...', amount: '1000000' }), },).then((r) => r.json());
// The host echoes the key it claimed — keep it.const idempotencyKey = prepared.idempotencyKey;
// 2) …sign prepared bytes client-side…
// 3) Execute — echo the SAME key so both steps share one record.await fetch( `${hostUrl}/api/v3/baas/entities/${entityId}/withdraw/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}` }, body: JSON.stringify({ chain: 'xrpl', signedTransactionBytes, idempotencyKey }), },);Echoing the key is no longer optional on Hedera. The validator will only
co-sign a transaction it can tie to a prepare, so it checks the signed bytes
against the prepared bytes stored on the record the key resolves to. A Hedera
execute that arrives without the key cannot name its transfer (the host sends no
destination or amount, they live inside the signed bytes) and is refused with a
400 that tells you to send the key.
On XRPL a caller that ignores the key still gets no dedup benefit and no stale-envelope check, as before.
The lifecycle, end to end
Section titled “The lifecycle, end to end”sequenceDiagram
participant App as Your App (SDK)
participant Val as Validator cluster
participant DKG as Threshold signers (validators)
participant Chain as Target chain
App->>Val: token/create/prepare { chain, ... }
Val->>DKG: Threshold-sign with entity multisig
DKG-->>Val: Signed prepared bytes
Val-->>App: prepared transaction
App->>Chain: submit (explicit step)
Chain-->>App: tokenId
App->>Val: token/mint/prepare { chain, tokenId, amount, ... }
Val-->>App: signed prepared bytes
App->>Chain: submit → supply increases
App->>Val: transfer/prepare { chain, tokenId, to, amount }
Val-->>App: signed prepared bytes
App->>Chain: submit → ownership moves
App->>Val: token/burn/prepare { chain, tokenId, amount }
Val-->>App: signed prepared bytes
App->>Chain: submit → supply decreases
Note over Val,Chain: Cluster auto-watches the token entity on-chain afterward
Who can sign: the multisig authority
Section titled “Who can sign: the multisig authority”A token created through this surface is a cluster-governed entity. Its admin/mint authority is the DKG-derived native multisig (an XRPL SignerList or a Hedera KeyList), not a single key. So minting more supply or burning isn’t “whoever has the secret key” — it’s a threshold of validators cooperating under the entity’s on-chain multisig, which the rules engine governs.
Sovereignty modes
Section titled “Sovereignty modes”Token operations honor the platform’s security mode for the entity, which is
either partial or full. The mode controls how much of the signing authority
the cluster holds versus the customer, and it flows through the same prepare
endpoints (as the securityMode and entityId request fields). See the
glossary for the precise
definitions.