Skip to content

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.


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, …).


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:

  1. On prepare, you may pass your own idempotencyKey Optional. If you omit it, the host generates one for you.
  2. Either way, the prepare response echoes the key back as its idempotencyKey field.
  3. 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.


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

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.


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.