diff --git a/_templates/sidebar-main.html b/_templates/sidebar-main.html
index c973bc7b4..71eb257e9 100644
--- a/_templates/sidebar-main.html
+++ b/_templates/sidebar-main.html
@@ -163,6 +163,46 @@
+
+
+ Tokens
+
+
+
+
+
+
+
+
+
+
+
Example apps
diff --git a/docs/index.md b/docs/index.md
index e42d5fdf5..875e97d6a 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -111,6 +111,7 @@ tutorials/create-and-fund-a-wallet
tutorials/setup-sdk-client
tutorials/identities-and-names
tutorials/contracts-and-documents
+tutorials/tokens
tutorials/example-apps
tutorials/send-funds
tutorials/setup-a-node
diff --git a/docs/tutorials/tokens.md b/docs/tutorials/tokens.md
new file mode 100644
index 000000000..eea02af93
--- /dev/null
+++ b/docs/tutorials/tokens.md
@@ -0,0 +1,22 @@
+```{eval-rst}
+.. tutorials-tokens:
+```
+
+# Tokens
+
+The following tutorials cover registering, querying, and managing tokens on Dash Platform, including minting, burning, and transferring them between identities.
+
+```{toctree}
+:maxdepth: 2
+:titlesonly:
+
+tokens/register-a-token-contract
+tokens/retrieve-token-info
+tokens/mint-tokens
+tokens/burn-tokens
+tokens/transfer-tokens-to-an-identity
+```
+
+:::{tip}
+You can clone a repository containing the code for all tutorials from GitHub or download it as a [zip file](https://github.com/dashpay/platform-readme-tutorials/archive/refs/heads/main.zip).
+:::
diff --git a/docs/tutorials/tokens/burn-tokens.md b/docs/tutorials/tokens/burn-tokens.md
new file mode 100644
index 000000000..3cb662e68
--- /dev/null
+++ b/docs/tutorials/tokens/burn-tokens.md
@@ -0,0 +1,68 @@
+```{eval-rst}
+.. tutorials-burn-tokens:
+```
+
+# Burn tokens
+
+The purpose of this tutorial is to walk through the steps necessary to burn (permanently destroy) [tokens](../../explanations/tokens.md), reducing the total supply.
+
+## Overview
+
+Burning permanently removes tokens from circulation, decreasing the token's total supply. Burning is only possible when the token contract authorizes it, which the [Register a token contract](register-a-token-contract.md) tutorial sets up. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md).
+
+## Prerequisites
+
+- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed)
+- A configured client: [Setup SDK Client](../setup-sdk-client.md)
+- A registered token contract with a balance to burn: [Tutorial: Register a token contract](register-a-token-contract.md). Set the resulting contract ID as the `TOKEN_CONTRACT_ID` environment variable.
+
+## Code
+
+```{code-block} javascript
+:caption: token-burn.mjs
+
+import { setupDashClient } from '../setupDashClient.mjs';
+
+const { sdk, keyManager } = await setupDashClient();
+const { identity, identityKey, signer } = await keyManager.getAuth();
+
+// TOKEN_CONTRACT_ID comes from token-register.mjs.
+const dataContractId = process.env.TOKEN_CONTRACT_ID;
+const tokenPosition = 0;
+const amount = 1n; // Token amounts are bigint values
+
+try {
+ if (!dataContractId) {
+ throw new Error(
+ 'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.',
+ );
+ }
+
+ const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition);
+
+ await sdk.tokens.burn({
+ dataContractId,
+ tokenPosition,
+ amount,
+ identityId: identity.id.toString(),
+ identityKey,
+ signer,
+ });
+
+ const balances = await sdk.tokens.identityBalances(identity.id, [tokenId]);
+ const totalSupply = await sdk.tokens.totalSupply(tokenId);
+
+ console.log(`Burned ${amount} token`);
+ console.log('Token ID:', tokenId);
+ console.log(`Identity token balance: ${balances.get(tokenId) ?? 0n}`);
+ console.log('Total token supply:', totalSupply?.totalSupply ?? 0n);
+} catch (e) {
+ console.error('Something went wrong:\n', e.message);
+}
+```
+
+## What's Happening
+
+After connecting to the client, we get the auth key signer with `keyManager.getAuth()`. We then call `sdk.tokens.burn()` with the contract ID, token position, amount, and signing credentials to destroy 1 token from our balance. Token amounts are `bigint` values, which is why `1n` is written with the `n` suffix.
+
+After burning, we read back the identity's balance with `sdk.tokens.identityBalances()` and the new total supply with `sdk.tokens.totalSupply()` to confirm both have decreased.
diff --git a/docs/tutorials/tokens/mint-tokens.md b/docs/tutorials/tokens/mint-tokens.md
new file mode 100644
index 000000000..5924ea3c5
--- /dev/null
+++ b/docs/tutorials/tokens/mint-tokens.md
@@ -0,0 +1,68 @@
+```{eval-rst}
+.. tutorials-mint-tokens:
+```
+
+# Mint tokens
+
+The purpose of this tutorial is to walk through the steps necessary to mint (issue) new [tokens](../../explanations/tokens.md), increasing the total supply.
+
+## Overview
+
+Minting issues new tokens to the contract owner, increasing the token's total supply up to its maximum. Minting is only possible when the token contract authorizes it, which the [Register a token contract](register-a-token-contract.md) tutorial sets up. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md).
+
+## Prerequisites
+
+- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed)
+- A configured client: [Setup SDK Client](../setup-sdk-client.md)
+- A registered token contract: [Tutorial: Register a token contract](register-a-token-contract.md). Set the resulting contract ID as the `TOKEN_CONTRACT_ID` environment variable.
+
+## Code
+
+```{code-block} javascript
+:caption: token-mint.mjs
+
+import { setupDashClient } from '../setupDashClient.mjs';
+
+const { sdk, keyManager } = await setupDashClient();
+const { identity, identityKey, signer } = await keyManager.getAuth();
+
+// TOKEN_CONTRACT_ID comes from token-register.mjs.
+const dataContractId = process.env.TOKEN_CONTRACT_ID;
+const tokenPosition = 0;
+const amount = 10n; // Token amounts are bigint values
+
+try {
+ if (!dataContractId) {
+ throw new Error(
+ 'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.',
+ );
+ }
+
+ const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition);
+
+ await sdk.tokens.mint({
+ dataContractId,
+ tokenPosition,
+ amount,
+ identityId: identity.id.toString(),
+ identityKey,
+ signer,
+ });
+
+ const balances = await sdk.tokens.identityBalances(identity.id, [tokenId]);
+ const totalSupply = await sdk.tokens.totalSupply(tokenId);
+
+ console.log(`Minted ${amount} tokens`);
+ console.log('Token ID:', tokenId);
+ console.log(`Identity token balance: ${balances.get(tokenId) ?? 0n}`);
+ console.log('Total token supply:', totalSupply?.totalSupply ?? 0n);
+} catch (e) {
+ console.error('Something went wrong:\n', e.message);
+}
+```
+
+## What's Happening
+
+After connecting to the client, we get the auth key signer with `keyManager.getAuth()`. We then call `sdk.tokens.mint()` with the contract ID, token position, amount, and signing credentials to issue 10 new tokens to our identity. Token amounts are `bigint` values, which is why `10n` is written with the `n` suffix.
+
+After minting, we read back the identity's balance with `sdk.tokens.identityBalances()` and the new total supply with `sdk.tokens.totalSupply()` to confirm both have increased.
diff --git a/docs/tutorials/tokens/register-a-token-contract.md b/docs/tutorials/tokens/register-a-token-contract.md
new file mode 100644
index 000000000..b2ad8cc07
--- /dev/null
+++ b/docs/tutorials/tokens/register-a-token-contract.md
@@ -0,0 +1,203 @@
+```{eval-rst}
+.. tutorials-register-token-contract:
+```
+
+# Register a token contract
+
+The purpose of this tutorial is to walk through the steps necessary to register a [token](../../explanations/tokens.md) on Dash Platform.
+
+## Overview
+
+Tokens on Dash Platform are defined inside a [data contract](../../explanations/platform-protocol-data-contract.md). A single contract can carry one or more tokens alongside its document types, and each token is identified by its position within the contract. Registering the contract creates the token, sets its supply limits, and establishes the rules that control who may mint, burn, transfer, or otherwise manage it.
+
+This tutorial registers an issuer-managed token: the contract owner controls minting and burning, and newly minted tokens always go to the owner identity. The token is not configured with advanced options. That keeps this tutorial focused on the normal token lifecycle used by the remaining token tutorials. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md).
+
+## Prerequisites
+
+- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed)
+- A platform address with a balance: [Tutorial: Create and Fund a Wallet](../../tutorials/create-and-fund-a-wallet.md)
+- A configured client: [Setup SDK Client](../setup-sdk-client.md)
+- A Dash Platform Identity: [Tutorial: Register an Identity](../../tutorials/identities-and-names/register-an-identity.md)
+
+## Code
+
+```{code-block} javascript
+:caption: token-register.mjs
+
+import {
+ AuthorizedActionTakers,
+ ChangeControlRules,
+ DataContract,
+ TokenConfiguration,
+ TokenConfigurationConvention,
+ TokenConfigurationLocalization,
+ TokenDistributionRules,
+ TokenKeepsHistoryRules,
+ TokenMarketplaceRules,
+ TokenTradeMode,
+} from '@dashevo/evo-sdk';
+import { setupDashClient } from '../setupDashClient.mjs';
+
+const { sdk, keyManager } = await setupDashClient();
+const { identity, identityKey, signer } = await keyManager.getAuth();
+
+const TOKEN_POSITION = 0;
+const TOKEN_NAME = 'TutorialToken';
+const TOKEN_PLURAL = 'TutorialTokens';
+const TOKEN_BASE_SUPPLY = 100n; // Token amounts are bigint values
+const TOKEN_MAX_SUPPLY = 1000n;
+
+// This contract includes one small document type so learners can still use the
+// standard document tutorials with the same contract if they want to.
+const documentSchemas = {
+ note: {
+ type: 'object',
+ properties: {
+ message: {
+ type: 'string',
+ position: 0,
+ },
+ },
+ additionalProperties: false,
+ },
+};
+
+function createTutorialTokenConfiguration(ownerId) {
+ const contractOwner = AuthorizedActionTakers.ContractOwner();
+ const noOne = AuthorizedActionTakers.NoOne();
+
+ const ownerRules = new ChangeControlRules({
+ authorizedToMakeChange: contractOwner,
+ adminActionTakers: contractOwner,
+ isChangingAuthorizedActionTakersToNoOneAllowed: true,
+ isChangingAdminActionTakersToNoOneAllowed: true,
+ isSelfChangingAdminActionTakersAllowed: true,
+ });
+ const lockedRules = new ChangeControlRules({
+ authorizedToMakeChange: noOne,
+ adminActionTakers: noOne,
+ });
+
+ return new TokenConfiguration({
+ conventions: new TokenConfigurationConvention(
+ {
+ en: new TokenConfigurationLocalization(false, TOKEN_NAME, TOKEN_PLURAL),
+ },
+ 0,
+ ),
+ conventionsChangeRules: ownerRules,
+ baseSupply: TOKEN_BASE_SUPPLY,
+ maxSupply: TOKEN_MAX_SUPPLY,
+ keepsHistory: new TokenKeepsHistoryRules({
+ isKeepingBurningHistory: true,
+ isKeepingMintingHistory: true,
+ isKeepingTransferHistory: true,
+ }),
+ maxSupplyChangeRules: lockedRules,
+ distributionRules: new TokenDistributionRules({
+ newTokensDestinationIdentity: ownerId,
+ newTokensDestinationIdentityRules: ownerRules,
+ mintingAllowChoosingDestination: false,
+ mintingAllowChoosingDestinationRules: ownerRules,
+ perpetualDistributionRules: lockedRules,
+ changeDirectPurchasePricingRules: lockedRules,
+ }),
+ marketplaceRules: new TokenMarketplaceRules(
+ TokenTradeMode.NotTradeable(),
+ lockedRules,
+ ),
+ // Minting and burning are enabled so the next tutorials can demonstrate
+ // the normal issuer-managed token lifecycle.
+ manualMintingRules: ownerRules,
+ manualBurningRules: ownerRules,
+ freezeRules: lockedRules,
+ unfreezeRules: lockedRules,
+ destroyFrozenFundsRules: lockedRules,
+ emergencyActionRules: lockedRules,
+ mainControlGroupCanBeModified: noOne,
+ description: 'Issuer-managed token for Platform token tutorials.',
+ });
+}
+
+try {
+ const identityNonce = await sdk.identities.nonce(identity.id.toString());
+
+ const dataContract = new DataContract({
+ ownerId: identity.id,
+ identityNonce: (identityNonce || 0n) + 1n,
+ schemas: documentSchemas,
+ tokens: {
+ [TOKEN_POSITION]: createTutorialTokenConfiguration(
+ identity.id.toString(),
+ ),
+ },
+ fullValidation: true,
+ });
+
+ const publishedContract = await sdk.contracts.publish({
+ dataContract,
+ identityKey,
+ signer,
+ });
+
+ const contractId =
+ publishedContract.id?.toString() || publishedContract.toJSON?.()?.id;
+
+ if (!contractId) {
+ const publishResult = publishedContract.toJSON?.() ?? publishedContract;
+ throw new Error(
+ `Contract publish returned no id: ${JSON.stringify(publishResult)}`,
+ );
+ }
+
+ const tokenId = await sdk.tokens.calculateId(contractId, TOKEN_POSITION);
+
+ console.log('Token contract registered:\n', publishedContract.toJSON());
+ console.log('Token position:', TOKEN_POSITION);
+ console.log('Token ID:', tokenId);
+ console.log('Initial owner token balance:', TOKEN_BASE_SUPPLY.toString());
+ console.log('Maximum token supply:', TOKEN_MAX_SUPPLY.toString());
+} catch (e) {
+ console.error('Something went wrong:\n', e.message);
+}
+```
+
+:::{attention}
+Make a note of the returned contract ID. The remaining token tutorials read it from the `TOKEN_CONTRACT_ID` environment variable, so set it before running them.
+
+Use the contract ID from the published contract output, not the token ID printed at the end:
+
+```text
+TOKEN_CONTRACT_ID=
+```
+:::
+
+## What's Happening
+
+After initializing the client, we get the auth key signer from the key manager. We then register a data contract that carries a token at position 0. The token starts with a base supply of 100 (minted to the owner) and a maximum supply of 1,000. Token amounts are always `bigint` values, which is why they are written with the `n` suffix (for example, `100n`).
+
+Minting and burning are enabled and restricted to the contract owner, so the [mint](mint-tokens.md) and [burn](burn-tokens.md) tutorials work out of the box. The token also keeps a full history of mints, burns, and transfers.
+
+To register the contract, we fetch the identity's current nonce and increment it, build a `DataContract` with the document schemas and the token configuration, and call `sdk.contracts.publish()`. Finally, we derive the token ID from the contract ID and token position with `sdk.tokens.calculateId()`.
+
+:::{dropdown} Token configuration details
+The token's behaviour is defined by a `TokenConfiguration`. Each group of rules is a `ChangeControlRules` object that says who may perform an action and who may change that permission. The tutorial uses two presets:
+
+- `ownerRules` — the contract owner is authorized (`AuthorizedActionTakers.ContractOwner()`).
+- `lockedRules` — no one is authorized (`AuthorizedActionTakers.NoOne()`), permanently disabling the action.
+
+The main groups:
+
+- **Supply** — `baseSupply` is minted to the owner at registration; `maxSupply` caps the total. `maxSupplyChangeRules` is locked, so the cap cannot be changed later.
+- **Minting and burning** — `manualMintingRules` and `manualBurningRules` use `ownerRules`, letting the owner issue and destroy tokens.
+- **History** — `keepsHistory` records minting, burning, and transfer events so they can be queried later.
+- **Distribution** — `distributionRules` sends newly minted tokens to the owner (`newTokensDestinationIdentity`) and prevents minting from choosing a different destination (`mintingAllowChoosingDestination: false`). Perpetual distribution is locked, so there is no automated recurring distribution.
+- **Marketplace and pricing** — `marketplaceRules` uses `TokenTradeMode.NotTradeable()`, so the token cannot be listed for direct purchase, and the trade mode itself is locked. The permission to set a direct-purchase price (`changeDirectPurchasePricingRules`, defined with the distribution rules) is also locked.
+- **Freeze and emergency actions** — `freezeRules`, `unfreezeRules`, `destroyFrozenFundsRules`, and `emergencyActionRules` are locked in this example.
+
+See the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md) for the full set of configuration fields.
+:::
+
+:::{tip}
+See this in an example app: [DashMint Lab — Contract schema](../example-apps/dashmint-lab.md#contract-schema) defines a token alongside its NFT documents, and [DashMint Lab — DashMint token flow](../example-apps/dashmint-lab.md#dashmint-token-flow) shows how the token is used.
+:::
diff --git a/docs/tutorials/tokens/retrieve-token-info.md b/docs/tutorials/tokens/retrieve-token-info.md
new file mode 100644
index 000000000..38b3af04e
--- /dev/null
+++ b/docs/tutorials/tokens/retrieve-token-info.md
@@ -0,0 +1,88 @@
+```{eval-rst}
+.. tutorials-retrieve-token-info:
+```
+
+# Retrieve token info
+
+The purpose of this tutorial is to walk through the steps necessary to retrieve information about a [token](../../explanations/tokens.md), including its contract details, total supply, status, and identity balances.
+
+## Overview
+
+Once a token contract is registered, its metadata and balances can be queried without submitting a state transition. This tutorial retrieves the token's contract info, total supply, status, and the token balances held by two identities. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md).
+
+## Prerequisites
+
+- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed)
+- A configured client: [Setup SDK Client](../setup-sdk-client.md)
+- A registered token contract: [Tutorial: Register a token contract](register-a-token-contract.md). Set the resulting contract ID as the `TOKEN_CONTRACT_ID` environment variable.
+
+## Code
+
+```{code-block} javascript
+:caption: token-info.mjs
+
+import { setupDashClient } from '../setupDashClient.mjs';
+
+const { sdk, keyManager } = await setupDashClient();
+
+// TOKEN_CONTRACT_ID comes from token-register.mjs.
+const dataContractId = process.env.TOKEN_CONTRACT_ID;
+const tokenPosition = 0;
+
+// Default recipient (testnet). Replace or override via RECIPIENT_ID.
+const recipientId =
+ process.env.RECIPIENT_ID || '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC';
+
+try {
+ if (!dataContractId) {
+ throw new Error(
+ 'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.',
+ );
+ }
+
+ const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition);
+ const contractInfo = await sdk.tokens.contractInfo(tokenId);
+ const totalSupply = await sdk.tokens.totalSupply(tokenId);
+ const statuses = await sdk.tokens.statuses([tokenId]);
+ const identityBalances = await sdk.tokens.identityBalances(
+ keyManager.identityId,
+ [tokenId],
+ );
+ const recipientBalances = await sdk.tokens.identityBalances(recipientId, [
+ tokenId,
+ ]);
+
+ // A token only has a status record once one is published on-chain (e.g. via
+ // an emergency pause), so the Map is empty for a freshly registered token.
+ const status = statuses.get(tokenId);
+
+ console.log('Token ID:', tokenId);
+ console.log('Token contract info:\n', contractInfo?.toJSON());
+ console.log(
+ 'Token status:',
+ status ? status.isPaused : '(no status published)',
+ );
+ console.log('Total token supply:', totalSupply?.totalSupply ?? 0n);
+ console.log(`Identity token balance: ${identityBalances.get(tokenId) ?? 0n}`);
+ console.log(
+ `Recipient token balance: ${recipientBalances.get(tokenId) ?? 0n}`,
+ );
+} catch (e) {
+ console.error('Something went wrong:\n', e.message);
+}
+```
+
+## What's Happening
+
+After connecting to the client, we derive the token ID from the contract ID and token position with `sdk.tokens.calculateId()`. We then query several pieces of information:
+
+- `sdk.tokens.contractInfo()` returns the token's contract metadata.
+- `sdk.tokens.totalSupply()` returns the number of tokens currently in circulation.
+- `sdk.tokens.statuses()` returns a Map of token statuses. A status record only exists once one is published on-chain (for example, after an emergency pause), so the Map is empty for a freshly registered token. We fall back to `(no status published)` in that case.
+- `sdk.tokens.identityBalances()` returns each identity's token balance, keyed by token ID.
+
+The recipient defaults to a demo testnet identity so the script can run without extra setup. Set `RECIPIENT_ID` in the .env file to your own second identity when you want the recipient balance check to reflect an identity you control.
+
+:::{tip}
+See this in an example app: [DashMint Lab — DashMint token flow](../example-apps/dashmint-lab.md#dashmint-token-flow) reads the signed-in identity's token balance to display remaining mint capacity.
+:::
diff --git a/docs/tutorials/tokens/transfer-tokens-to-an-identity.md b/docs/tutorials/tokens/transfer-tokens-to-an-identity.md
new file mode 100644
index 000000000..3d3d0c7bd
--- /dev/null
+++ b/docs/tutorials/tokens/transfer-tokens-to-an-identity.md
@@ -0,0 +1,96 @@
+```{eval-rst}
+.. tutorials-transfer-tokens-to-identity:
+```
+
+# Transfer tokens to an identity
+
+The purpose of this tutorial is to walk through the steps necessary to transfer [tokens](../../explanations/tokens.md) from one identity to another.
+
+## Overview
+
+Transferring moves tokens from the sender's balance to a recipient identity. The total supply is unchanged; only the balances of the two identities are affected. Additional details are available in the [tokens explanation](../../explanations/tokens.md) and the [token protocol reference](../../protocol-ref/token.md).
+
+## Prerequisites
+
+- [General prerequisites](../../tutorials/introduction.md#prerequisites) (Node.js / Dash SDK installed)
+- A configured client: [Setup SDK Client](../setup-sdk-client.md)
+- A registered token contract with a balance to transfer: [Tutorial: Register a token contract](register-a-token-contract.md). Set the resulting contract ID as the `TOKEN_CONTRACT_ID` environment variable.
+- A second Dash Platform Identity to receive the tokens: [Tutorial: Register an Identity](../../tutorials/identities-and-names/register-an-identity.md)
+
+## Code
+
+```{code-block} javascript
+:caption: token-transfer.mjs
+
+import { setupDashClient } from '../setupDashClient.mjs';
+
+const { sdk, keyManager } = await setupDashClient();
+const { identity, identityKey, signer } = await keyManager.getTransfer();
+
+// TOKEN_CONTRACT_ID comes from token-register.mjs.
+const dataContractId = process.env.TOKEN_CONTRACT_ID;
+const tokenPosition = 0;
+
+// Default recipient (testnet). Replace or override via RECIPIENT_ID.
+const recipientId =
+ process.env.RECIPIENT_ID || '7XcruVSsGQVSgTcmPewaE4tXLutnW1F6PXxwMbo8GYQC';
+const amount = 1n;
+
+try {
+ if (!dataContractId) {
+ throw new Error(
+ 'Set TOKEN_CONTRACT_ID in .env from token-register.mjs output.',
+ );
+ }
+
+ const senderId = identity.id.toString();
+ if (recipientId === senderId) {
+ throw new Error('Cannot transfer tokens to yourself.');
+ }
+
+ const tokenId = await sdk.tokens.calculateId(dataContractId, tokenPosition);
+ const balancesBefore = await sdk.tokens.identityBalances(recipientId, [
+ tokenId,
+ ]);
+
+ console.log(
+ `Recipient token balance before transfer: ${balancesBefore.get(tokenId) ?? 0n}`,
+ );
+
+ await sdk.tokens.transfer({
+ dataContractId,
+ tokenPosition,
+ amount,
+ senderId,
+ recipientId,
+ identityKey,
+ signer,
+ });
+
+ const balancesAfter = await sdk.tokens.identityBalances(recipientId, [
+ tokenId,
+ ]);
+
+ console.log(
+ `Transferred ${amount} token${amount === 1n ? '' : 's'} from ${senderId} to ${recipientId}`,
+ );
+ console.log('Token ID:', tokenId);
+ console.log(
+ `Recipient token balance after transfer: ${balancesAfter.get(tokenId) ?? 0n}`,
+ );
+} catch (e) {
+ console.error('Something went wrong:\n', e.message);
+}
+```
+
+## What's Happening
+
+After connecting to the client, we get the transfer key signer with `keyManager.getTransfer()`. Token transfers are authorized with the identity's transfer key rather than its auth key.
+
+We derive the token ID with `sdk.tokens.calculateId()` and read the recipient's balance before the transfer so the change is visible. A guard rejects transfers where the recipient matches the sender, since an identity cannot transfer tokens to itself.
+
+We then call `sdk.tokens.transfer()` with the contract ID, token position, amount, sender ID, recipient ID, and signing credentials to move 1 token. After the transfer, we read the recipient's balance again with `sdk.tokens.identityBalances()` to confirm it increased. The recipient defaults to a demo testnet identity, but set `RECIPIENT_ID` in the .env file to your own second identity when you want to verify a transfer to an identity you control.
+
+:::{tip}
+See this in an example app: [DashMint Lab — Transfer DashMint tokens](../example-apps/dashmint-lab.md#transfer-dashmint-tokens).
+:::
diff --git a/scripts/tutorial-sync/tutorial-code-map.yml b/scripts/tutorial-sync/tutorial-code-map.yml
index c1296b9af..f8954a871 100644
--- a/scripts/tutorial-sync/tutorial-code-map.yml
+++ b/scripts/tutorial-sync/tutorial-code-map.yml
@@ -188,6 +188,33 @@ mappings:
block_id:
caption: document-delete.mjs
+ # -- Tokens --
+
+ - source: 3-Tokens/token-register.mjs
+ doc: tokens/register-a-token-contract.md
+ block_id:
+ caption: token-register.mjs
+
+ - source: 3-Tokens/token-info.mjs
+ doc: tokens/retrieve-token-info.md
+ block_id:
+ caption: token-info.mjs
+
+ - source: 3-Tokens/token-mint.mjs
+ doc: tokens/mint-tokens.md
+ block_id:
+ caption: token-mint.mjs
+
+ - source: 3-Tokens/token-burn.mjs
+ doc: tokens/burn-tokens.md
+ block_id:
+ caption: token-burn.mjs
+
+ - source: 3-Tokens/token-transfer.mjs
+ doc: tokens/transfer-tokens-to-an-identity.md
+ block_id:
+ caption: token-transfer.mjs
+
# -- Example apps --
# DashMint Lab — React + TypeScript NFT app. Every SDK operation lives in