diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index f8859d679ae..0966c69e631 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -66,7 +66,21 @@
- [Strategy Tests](testing/strategy-tests.md)
- [Test Configuration](testing/test-configuration.md)
-# SDK
+# Evo SDK (JavaScript/TypeScript)
+
+- [Overview](evo-sdk/overview.md)
+- [Getting Started](evo-sdk/getting-started.md)
+- [Trusted Mode and Proofs](evo-sdk/trusted-mode.md)
+- [State Transitions](evo-sdk/state-transitions.md)
+- [Wallet Utilities](evo-sdk/wallet-utilities.md)
+- [Networks and Environments](evo-sdk/networks-and-environments.md)
+- [Tutorials]()
+ - [Car Sales Management](evo-sdk/tutorials/car-sales.md)
+ - [Creating a Basic Token](evo-sdk/tutorials/basic-token.md)
+ - [Card Game with Tokens](evo-sdk/tutorials/card-game.md)
+ - [React Integration](evo-sdk/tutorials/react-integration.md)
+
+# Rust SDK
- [Builder Pattern](sdk/builder-pattern.md)
- [Fetch Traits](sdk/fetch-traits.md)
diff --git a/book/src/evo-sdk/getting-started.md b/book/src/evo-sdk/getting-started.md
new file mode 100644
index 00000000000..7dca58e5dda
--- /dev/null
+++ b/book/src/evo-sdk/getting-started.md
@@ -0,0 +1,105 @@
+# Getting Started
+
+## Installation
+
+```sh
+npm install @dashevo/evo-sdk
+```
+
+The package is **ESM-only** (`"type": "module"`) and written in TypeScript with
+full type definitions included. In CommonJS projects use a dynamic `import()`:
+
+```js
+const { EvoSDK } = await import('@dashevo/evo-sdk');
+```
+
+Requirements: Node.js ≥ 18.18 or any modern browser with WebAssembly support.
+
+## Quick start
+
+```typescript
+import { EvoSDK } from '@dashevo/evo-sdk';
+
+// Pick your network: testnetTrusted() for development, mainnetTrusted() for production
+const sdk = EvoSDK.testnetTrusted();
+
+// Query the current epoch (connect() is called automatically on first use)
+const epoch = await sdk.epoch.current();
+console.log('Current epoch:', epoch.index);
+
+// Fetch an existing identity by its base58 ID
+const identity = await sdk.identities.fetch('4EfA9Jrvv3nnCFdSf7fad59851iiTRZ6Wcu6YVJ4iSeF');
+console.log('Balance:', identity?.getBalance());
+```
+
+## Connecting
+
+Calling `connect()` explicitly is **optional**. The SDK connects automatically
+when you call any facade method for the first time. However, you can call
+`connect()` explicitly if you want to control when the WASM module is
+initialized and quorum keys are prefetched:
+
+```typescript
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect(); // optional — triggers WASM init and quorum prefetch now
+```
+
+Calling `connect()` more than once is a no-op.
+
+### Factory helpers
+
+| Helper | Equivalent |
+|--------|-----------|
+| `EvoSDK.testnet()` | `new EvoSDK({ network: 'testnet' })` |
+| `EvoSDK.mainnet()` | `new EvoSDK({ network: 'mainnet' })` |
+| `EvoSDK.testnetTrusted()` | `new EvoSDK({ network: 'testnet', trusted: true })` |
+| `EvoSDK.mainnetTrusted()` | `new EvoSDK({ network: 'mainnet', trusted: true })` |
+| `EvoSDK.local()` | `new EvoSDK({ network: 'local' })` |
+| `EvoSDK.localTrusted()` | `new EvoSDK({ network: 'local', trusted: true })` |
+
+### Custom addresses
+
+To connect to specific masternodes (useful for testing or private networks):
+
+```typescript
+const sdk = EvoSDK.withAddresses(
+ ['https://52.12.176.90:1443'],
+ 'testnet',
+);
+await sdk.connect();
+```
+
+## Connection options
+
+All factory helpers and the constructor accept an optional `ConnectionOptions`
+object:
+
+```typescript
+const sdk = EvoSDK.testnetTrusted({
+ settings: {
+ connectTimeoutMs: 5000,
+ timeoutMs: 10000,
+ retries: 3,
+ banFailedAddress: true,
+ },
+ logs: 'info', // 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
+ proofs: true, // request proofs with every query
+ version: 8, // pin a specific protocol version
+});
+```
+
+### Logging
+
+The SDK delegates logging to the underlying Rust/WASM layer. Pass a simple
+level string or a full `EnvFilter` directive:
+
+```typescript
+// Simple level
+const sdk = EvoSDK.testnetTrusted({ logs: 'debug' });
+
+// Granular filter
+const sdk = EvoSDK.testnetTrusted({ logs: 'wasm_sdk=debug,rs_dapi_client=warn' });
+
+// Change level at runtime (static, affects all instances)
+await EvoSDK.setLogLevel('trace');
+```
diff --git a/book/src/evo-sdk/networks-and-environments.md b/book/src/evo-sdk/networks-and-environments.md
new file mode 100644
index 00000000000..964aa714f06
--- /dev/null
+++ b/book/src/evo-sdk/networks-and-environments.md
@@ -0,0 +1,74 @@
+# Networks and Environments
+
+The Evo SDK supports three built-in network configurations plus custom
+addresses for private or development networks.
+
+## Built-in networks
+
+| Network | Factory | DAPI discovery | Use case |
+|---------|---------|---------------|----------|
+| **Testnet** | `EvoSDK.testnetTrusted()` | Automatic via seed nodes | Development and testing |
+| **Mainnet** | `EvoSDK.mainnetTrusted()` | Automatic via seed nodes | Production applications |
+| **Local** | `EvoSDK.localTrusted()` | `127.0.0.1:1443` | Docker-based local development |
+
+For each network, the SDK discovers DAPI endpoints from seed nodes and rotates
+between them automatically. Failed nodes are temporarily banned so the SDK
+retries against healthy nodes.
+
+## Local development with Docker
+
+When running a local Platform network via
+[dashmate](https://github.com/dashpay/platform/tree/master/packages/dashmate),
+use the `local` network:
+
+```typescript
+const sdk = EvoSDK.localTrusted();
+await sdk.connect();
+```
+
+This connects to `https://127.0.0.1:1443` by default. If your local setup uses
+different ports, use custom addresses:
+
+```typescript
+const sdk = EvoSDK.withAddresses(
+ ['https://127.0.0.1:2443'],
+ 'local',
+);
+await sdk.connect();
+```
+
+## Custom masternode addresses
+
+For private devnets, specific nodes, or debugging:
+
+```typescript
+const sdk = EvoSDK.withAddresses(
+ [
+ 'https://52.12.176.90:1443',
+ 'https://34.217.100.50:1443',
+ ],
+ 'testnet',
+);
+await sdk.connect();
+```
+
+When custom addresses are provided, the SDK does not perform automatic node
+discovery — it uses only the addresses you supply.
+
+## Browser vs Node.js
+
+The SDK works identically in both environments. The underlying WASM module
+handles platform differences transparently.
+
+**Node.js considerations:**
+
+- Requires Node.js ≥ 18.18 (for WebAssembly and `fetch` support)
+- ESM-only package — use `import`, not `require`
+- No additional polyfills needed
+
+**Browser considerations:**
+
+- Works in any browser with WebAssembly support (all modern browsers)
+- The WASM module is loaded asynchronously on first `connect()` call
+- Total bundle size includes the compiled Rust SDK (~2-4 MB gzipped)
+- gRPC calls use `grpc-web` over HTTPS, compatible with standard CORS
diff --git a/book/src/evo-sdk/overview.md b/book/src/evo-sdk/overview.md
new file mode 100644
index 00000000000..646e49ae187
--- /dev/null
+++ b/book/src/evo-sdk/overview.md
@@ -0,0 +1,74 @@
+# Evo SDK Overview
+
+The **Evo SDK** (`@dashevo/evo-sdk`) is the primary JavaScript/TypeScript SDK
+for building applications on Dash Platform. It provides a high-level,
+strongly-typed facade over the WebAssembly-based Rust SDK, working in both
+Node.js (≥ 18.18) and modern browsers.
+
+> **API reference**: For detailed per-method documentation with interactive
+> examples, see the [Evo SDK Docs](https://dashpay.github.io/evo-sdk-website/docs.html).
+
+## How it works
+
+```text
+┌──────────────────┐
+│ Your TypeScript │
+│ Application │
+└────────┬─────────┘
+ │ EvoSDK facades (identities, documents, tokens, …)
+┌────────▼─────────┐
+│ @dashevo/ │
+│ evo-sdk │ TypeScript wrapper layer
+└────────┬─────────┘
+ │ calls into compiled WASM module
+┌────────▼─────────┐
+│ @dashevo/ │
+│ wasm-sdk │ Rust SDK compiled to WebAssembly
+└────────┬─────────┘
+ │ gRPC over HTTPS
+┌────────▼─────────┐
+│ DAPI nodes │ Dash Platform's decentralized API
+└──────────────────┘
+```
+
+The Evo SDK does **not** use JSON-RPC or REST. Every request is a gRPC call to
+one of the Platform's DAPI nodes. Responses include cryptographic proofs that
+the SDK verifies against the platform state root, so you do not need to trust
+any single node.
+
+## Facades
+
+The SDK organises its API into domain-specific facades, each accessible as a
+property on the `EvoSDK` instance:
+
+| Facade | Description |
+|--------|-------------|
+| `sdk.identities` | Fetch, create, update, and top up identities |
+| `sdk.contracts` | Fetch, publish, and update data contracts |
+| `sdk.documents` | Query, create, replace, delete, and transfer documents |
+| `sdk.tokens` | Mint, burn, transfer, freeze tokens and query balances |
+| `sdk.dpns` | Register and resolve Dash Platform names |
+| `sdk.addresses` | Query balances, transfer credits, withdraw to L1 |
+| `sdk.epoch` | Query epoch information and evonode proposed blocks |
+| `sdk.protocol` | Protocol version upgrade state and voting |
+| `sdk.stateTransitions` | Broadcast and wait for state transitions |
+| `sdk.system` | System status, quorum info, and total credits |
+| `sdk.group` | Group membership, actions, and contested resources |
+| `sdk.voting` | Contested resource vote states and polls |
+
+A standalone `wallet` namespace is also exported for mnemonic generation, key
+derivation, address validation, and message signing — see the
+[Wallet Utilities](wallet-utilities.md) chapter.
+
+## What it covers
+
+The SDK supports the full set of Platform operations:
+
+- **Queries** (read-only): fetch identities, contracts, documents, token
+ balances, DPNS names, epoch info, vote states, and more. Every query can
+ return a cryptographic proof.
+- **State transitions** (writes): create identities, deploy contracts, manage
+ documents and tokens, register names, cast votes, and transfer credits.
+
+See the [API reference](https://dashpay.github.io/evo-sdk-website/docs.html) for the
+complete list of operations with interactive examples.
diff --git a/book/src/evo-sdk/state-transitions.md b/book/src/evo-sdk/state-transitions.md
new file mode 100644
index 00000000000..4526d7085db
--- /dev/null
+++ b/book/src/evo-sdk/state-transitions.md
@@ -0,0 +1,158 @@
+# State Transitions
+
+State transitions are the write operations of Dash Platform. Unlike queries
+(which are free and instant), state transitions modify on-chain state, cost
+credits, and must be signed with a private key.
+
+> **API reference**: For the full list of state transition methods with
+> parameters and examples, see the
+> [State Transitions section](https://dashpay.github.io/evo-sdk-website/docs.html)
+> of the Evo SDK Docs.
+
+## How state transitions work
+
+1. **Build** — The SDK constructs the transition (e.g., "create identity",
+ "register name") from the parameters you provide.
+2. **Sign** — You provide a private key (WIF format) and the SDK signs the
+ transition.
+3. **Broadcast** — The signed transition is sent to a DAPI node, which
+ propagates it to the Platform chain.
+4. **Wait** — The SDK waits for the transition to be included in a block and
+ returns the result.
+
+## Identity operations
+
+### Create an identity
+
+```typescript
+// Generate keys for the new identity
+const keyPair = await wallet.generateKeyPair('testnet');
+
+const identity = await sdk.identities.create({
+ privateKeyWif: fundingKeyWif, // key with Dash balance for the asset lock
+ identityPublicKeys: [{
+ type: 0, // ECDSA_SECP256K1
+ purpose: 0, // AUTHENTICATION
+ securityLevel: 0, // MASTER
+ publicKeyHex: keyPair.publicKeyHex,
+ }],
+});
+```
+
+### Top up an identity
+
+```typescript
+await sdk.identities.topUp({
+ identityId: 'BxPVr5...',
+ amount: 100000, // credits (1 credit = 1000 duffs)
+ privateKeyWif: fundingKeyWif,
+});
+```
+
+### Transfer credits between identities
+
+```typescript
+await sdk.identities.creditTransfer({
+ identityId: senderIdentityId,
+ recipientId: recipientIdentityId,
+ amount: 50000,
+ privateKeyWif: senderAuthKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(senderIdentityId),
+});
+```
+
+## Document operations
+
+### Create a document
+
+```typescript
+await sdk.documents.create({
+ contractId: 'GWRSAVFMjXx8HpQFaNJMqBV7MBgMK4br5UESsB4S31Ec',
+ documentType: 'domain',
+ document: {
+ label: 'my-username',
+ normalizedLabel: 'my-username',
+ normalizedParentDomainName: 'dash',
+ records: { identity: identityId },
+ subdomainRules: { allowSubdomains: false },
+ },
+ identityId,
+ privateKeyWif: authKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(identityId, dpnsContractId),
+});
+```
+
+### Replace, delete, transfer
+
+The `sdk.documents` facade also provides `replace()`, `delete()`,
+`transfer()`, `purchase()`, and `setPrice()` methods. See the
+[API reference](https://dashpay.github.io/evo-sdk-website/docs.html) for
+parameters.
+
+## Token operations
+
+```typescript
+// Mint tokens (requires minting authority)
+await sdk.tokens.mint({
+ tokenId: '...',
+ amount: 1000,
+ recipientId: '...',
+ identityId: minterIdentityId,
+ privateKeyWif: minterKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(minterIdentityId),
+});
+
+// Transfer tokens
+await sdk.tokens.transfer({
+ tokenId: '...',
+ amount: 100,
+ recipientId: '...',
+ identityId: senderIdentityId,
+ privateKeyWif: senderKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(senderIdentityId),
+});
+```
+
+## DPNS name registration
+
+```typescript
+await sdk.dpns.register({
+ name: 'alice',
+ identityId,
+ privateKeyWif: authKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(identityId, dpnsContractId),
+});
+```
+
+## Waiting for results
+
+The `sdk.stateTransitions` facade provides low-level control:
+
+```typescript
+// Broadcast a raw state transition and wait for confirmation
+const result = await sdk.stateTransitions.waitForResult(stateTransitionHash);
+```
+
+## Nonces
+
+Every state transition requires a **nonce** to prevent replay attacks. There
+are two types:
+
+- **Identity nonce** — incremented per identity for identity-level transitions
+ (top-ups, credit transfers, token operations)
+- **Contract nonce** — incremented per identity-contract pair for document and
+ contract transitions
+
+```typescript
+const identityNonce = await sdk.identities.nonce(identityId);
+const contractNonce = await sdk.identities.contractNonce(identityId, contractId);
+```
+
+Always fetch the nonce immediately before broadcasting. If another transition
+lands between your fetch and broadcast, the nonce will be stale and the
+transition will be rejected.
diff --git a/book/src/evo-sdk/trusted-mode.md b/book/src/evo-sdk/trusted-mode.md
new file mode 100644
index 00000000000..998d7517c60
--- /dev/null
+++ b/book/src/evo-sdk/trusted-mode.md
@@ -0,0 +1,66 @@
+# Trusted Mode and Proof Verification
+
+## The problem
+
+Every Platform query response includes a cryptographic proof — a signed hash
+from the current validator quorum that attests the response matches the
+platform state tree. To verify these proofs the SDK needs the **quorum public
+keys** for the active validator set.
+
+On a full node you can look up quorum keys directly from the Core chain. In a
+browser or lightweight environment you cannot. Trusted mode solves this.
+
+## How trusted mode works
+
+When you create an SDK with `trusted: true`, the `connect()` call does an extra
+step before returning: it fetches the current quorum public keys from a
+well-known HTTPS endpoint and caches them in memory.
+
+```typescript
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect(); // fetches quorum keys, then connects to DAPI
+```
+
+The trust model:
+
+- You trust the HTTPS endpoint (operated by Dash Core Group) to return correct
+ quorum public keys.
+- Once the keys are cached, every subsequent query response is verified against
+ them — you do **not** trust individual DAPI nodes for the data itself.
+
+This is a pragmatic trade-off: you trust one endpoint for the validator set,
+but verify all actual data cryptographically.
+
+## When to use trusted mode
+
+| Scenario | Trusted mode? | Why |
+|----------|:---:|-----|
+| Browser app | Yes | No access to Core chain |
+| Node.js script | Yes | Simplest setup |
+| Server with Core RPC | Optional | Can fetch quorum keys from your own node |
+| Local Docker setup | Yes | Use `EvoSDK.localTrusted()` |
+
+If you do not use trusted mode, the SDK still works but cannot verify proofs.
+Queries return data but you are trusting the responding DAPI node.
+
+## Proofs in responses
+
+By default, the SDK verifies proofs internally and returns just the data. To
+inspect the proof metadata yourself, use the `WithProof` variants:
+
+```typescript
+// Standard — proof verified internally, returns data only
+const identity = await sdk.identities.fetch(id);
+
+// With proof — returns both data and proof metadata
+const { data, proof, metadata } = await sdk.identities.fetchWithProof(id);
+console.log('Block height:', metadata.height);
+console.log('Core chain locked height:', metadata.coreChainLockedHeight);
+```
+
+Some methods also offer an `Unproved` variant that skips proof verification
+entirely, useful when you trust the node or want faster responses:
+
+```typescript
+const identity = await sdk.identities.fetchUnproved(id);
+```
diff --git a/book/src/evo-sdk/tutorials/basic-token.md b/book/src/evo-sdk/tutorials/basic-token.md
new file mode 100644
index 00000000000..13125dd4da9
--- /dev/null
+++ b/book/src/evo-sdk/tutorials/basic-token.md
@@ -0,0 +1,242 @@
+# Tutorial: Creating a Basic Token
+
+> **Environment:** This tutorial uses **Node.js** scripts to deploy a contract
+> and perform token operations. For browser-based applications, deploy the
+> contract from a Node.js script first, then use the contract ID in your
+> frontend code.
+
+Create a fungible token on Dash Platform with minting, transferring, and
+balance queries. This tutorial walks through the full lifecycle from contract
+deployment to token operations.
+
+## What you will learn
+
+- Defining a data contract with a token configuration
+- Minting tokens to an identity
+- Transferring tokens between identities
+- Querying balances and supply
+
+## Prerequisites
+
+```sh
+npm install @dashevo/evo-sdk
+```
+
+You need a funded testnet identity with enough credits to deploy a contract and
+perform token operations.
+
+## Step 1: Define the token contract
+
+A token is defined as part of a data contract. The contract schema includes a
+`tokens` section alongside the usual document schemas.
+
+```typescript
+import { EvoSDK, wallet } from '@dashevo/evo-sdk';
+
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+const identityId = 'YOUR_IDENTITY_ID';
+const privateKeyWif = 'YOUR_PRIVATE_KEY_WIF';
+const signingKeyIndex = 0;
+
+// Define a contract with a token
+const contractSchema = {
+ // Document types (optional — a token-only contract can have none)
+ tokenMetadata: {
+ type: 'object',
+ properties: {
+ tokenName: { type: 'string', maxLength: 64 },
+ description: { type: 'string', maxLength: 256 },
+ },
+ additionalProperties: false,
+ },
+};
+
+// Token configuration is passed separately when publishing
+const tokenConfig = {
+ // Position 0 = first token in this contract
+ conventions: {
+ localizations: {
+ en: {
+ shouldCapitalize: true,
+ singularForm: 'CoffeeCoin',
+ pluralForm: 'CoffeeCoins',
+ },
+ },
+ decimals: 2,
+ },
+ // The contract owner can mint manually
+ manualMinting: {
+ rules: {
+ // Allow the contract owner to mint
+ type: 'ownerOnly',
+ },
+ },
+ // The contract owner can burn their own tokens
+ manualBurning: {
+ rules: {
+ type: 'ownerOnly',
+ },
+ },
+ // Maximum supply (optional)
+ maxSupply: 1_000_000_00, // 1,000,000.00 with 2 decimals
+};
+```
+
+## Step 2: Publish the contract
+
+```typescript
+const contract = await sdk.contracts.publish({
+ identityId,
+ documentSchemas: contractSchema,
+ tokens: [tokenConfig],
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+const contractId = contract.getId().toString();
+console.log('Contract published:', contractId);
+
+// Calculate the token ID (derived from contract ID + position)
+const tokenId = await sdk.tokens.calculateId(contractId, 0);
+console.log('Token ID:', tokenId);
+```
+
+## Step 3: Mint tokens
+
+The contract owner can mint tokens to any identity:
+
+```typescript
+// Mint 10,000.00 CoffeeCoins to yourself
+await sdk.tokens.mint({
+ tokenId,
+ amount: 10_000_00, // 10,000.00 (2 decimal places)
+ recipientId: identityId, // mint to yourself
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+console.log('Minted 10,000 CoffeeCoins');
+```
+
+### Mint to another identity
+
+```typescript
+await sdk.tokens.mint({
+ tokenId,
+ amount: 500_00, // 500.00 CoffeeCoins
+ recipientId: 'RECIPIENT_IDENTITY_ID',
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+```
+
+## Step 4: Check balances
+
+```typescript
+// Check your own balance
+const myBalances = await sdk.tokens.identityBalances(identityId, [tokenId]);
+const myBalance = myBalances.get(tokenId) ?? 0n;
+console.log('My balance:', Number(myBalance) / 100, 'CoffeeCoins');
+
+// Check multiple identities at once
+const balances = await sdk.tokens.balances(
+ [identityId, 'OTHER_IDENTITY_ID'],
+ tokenId,
+);
+
+for (const [id, balance] of balances) {
+ console.log(`${id}: ${Number(balance) / 100} CoffeeCoins`);
+}
+```
+
+### Check total supply
+
+```typescript
+const supply = await sdk.tokens.totalSupply(tokenId);
+if (supply) {
+ console.log('Total supply:', Number(supply.totalSupply) / 100, 'CoffeeCoins');
+}
+```
+
+## Step 5: Transfer tokens
+
+```typescript
+await sdk.tokens.transfer({
+ tokenId,
+ amount: 25_00, // 25.00 CoffeeCoins
+ recipientId: 'RECIPIENT_IDENTITY_ID',
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+console.log('Transferred 25 CoffeeCoins');
+```
+
+## Step 6: Burn tokens
+
+Reduce the supply by burning tokens you own:
+
+```typescript
+await sdk.tokens.burn({
+ tokenId,
+ amount: 100_00, // 100.00 CoffeeCoins
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+console.log('Burned 100 CoffeeCoins');
+```
+
+## Full example
+
+Putting it all together as a complete script:
+
+```typescript
+import { EvoSDK } from '@dashevo/evo-sdk';
+
+async function main() {
+ const sdk = EvoSDK.testnetTrusted();
+ await sdk.connect();
+
+ const identityId = 'YOUR_IDENTITY_ID';
+ const privateKeyWif = 'YOUR_PRIVATE_KEY_WIF';
+ const tokenId = 'YOUR_TOKEN_ID'; // from step 2
+
+ // Check balance
+ const balances = await sdk.tokens.identityBalances(identityId, [tokenId]);
+ console.log('Balance:', balances.get(tokenId) ?? 0n);
+
+ // Transfer
+ await sdk.tokens.transfer({
+ tokenId,
+ amount: 10_00,
+ recipientId: 'FRIEND_IDENTITY_ID',
+ identityId,
+ privateKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(identityId),
+ });
+
+ console.log('Transfer complete!');
+}
+
+main().catch(console.error);
+```
+
+## Next steps
+
+- Add **freeze/unfreeze** capabilities for compliance scenarios
+- Set up a **direct purchase price** so anyone can buy tokens with credits
+- Create a **distribution schedule** for automatic token rewards
+- Use the `tokenMetadata` document type to store on-chain metadata
diff --git a/book/src/evo-sdk/tutorials/car-sales.md b/book/src/evo-sdk/tutorials/car-sales.md
new file mode 100644
index 00000000000..bcb1a96c919
--- /dev/null
+++ b/book/src/evo-sdk/tutorials/car-sales.md
@@ -0,0 +1,229 @@
+# Tutorial: Car Sales Management
+
+Build a decentralised car listing and sales application on Dash Platform. By
+the end you will have a data contract for vehicle listings, the ability to
+create/query/update listings, and a purchase flow using document transfers.
+
+> **How this works in practice:** Data contracts are deployed once using a
+> **Node.js script** with a developer identity. After deployment, your
+> **browser app** uses the published contract ID to create, query, and update
+> documents. Steps 1-2 below are run from Node.js; steps 3 onward can run
+> in either Node.js or the browser.
+
+## What you will learn
+
+- Designing a data contract with multiple document types
+- Publishing a contract to testnet from a Node.js deployment script
+- Creating, querying, and updating documents (Node.js or browser)
+- Using document pricing and purchase for a sales flow
+
+## Prerequisites
+
+```sh
+npm install @dashevo/evo-sdk
+```
+
+You need a funded testnet identity. See the
+[Getting Started](../getting-started.md) chapter for setup.
+
+## Step 1: Design the data contract
+
+A car sales contract needs two document types: **listings** (vehicles for sale)
+and **reviews** (buyer reviews of sellers).
+
+```typescript
+const carSalesSchema = {
+ listing: {
+ type: 'object',
+ properties: {
+ make: { type: 'string', maxLength: 64 },
+ model: { type: 'string', maxLength: 64 },
+ year: { type: 'integer', minimum: 1900, maximum: 2100 },
+ mileageKm: { type: 'integer', minimum: 0 },
+ priceUsd: { type: 'integer', minimum: 0 },
+ description: { type: 'string', maxLength: 1024 },
+ imageUrl: { type: 'string', maxLength: 512, format: 'uri' },
+ status: { type: 'string', enum: ['available', 'pending', 'sold'] },
+ },
+ required: ['make', 'model', 'year', 'priceUsd', 'status'],
+ additionalProperties: false,
+ },
+ review: {
+ type: 'object',
+ properties: {
+ sellerId: { type: 'string', maxLength: 44 },
+ listingId: { type: 'string', maxLength: 44 },
+ rating: { type: 'integer', minimum: 1, maximum: 5 },
+ comment: { type: 'string', maxLength: 512 },
+ },
+ required: ['sellerId', 'rating'],
+ additionalProperties: false,
+ },
+};
+```
+
+## Step 2: Connect and publish the contract
+
+```typescript
+import { EvoSDK, wallet } from '@dashevo/evo-sdk';
+
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+// Your identity credentials
+const identityId = 'YOUR_IDENTITY_ID';
+const privateKeyWif = 'YOUR_PRIVATE_KEY_WIF';
+const signingKeyIndex = 0;
+
+// Publish the data contract
+const contract = await sdk.contracts.publish({
+ identityId,
+ documentSchemas: carSalesSchema,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.nonce(identityId),
+});
+
+const contractId = contract.getId().toString();
+console.log('Contract published:', contractId);
+```
+
+Save the `contractId` — you will need it for all subsequent operations.
+
+## Step 3: Create a listing
+
+```typescript
+const nonce = await sdk.identities.contractNonce(identityId, contractId);
+
+await sdk.documents.create({
+ contractId,
+ documentType: 'listing',
+ document: {
+ make: 'Toyota',
+ model: 'Camry',
+ year: 2021,
+ mileageKm: 45000,
+ priceUsd: 22500,
+ description: 'Well-maintained, single owner, full service history.',
+ status: 'available',
+ },
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce,
+});
+
+console.log('Listing created!');
+```
+
+## Step 4: Query listings
+
+```typescript
+// Fetch all available listings
+const results = await sdk.documents.query({
+ contractId,
+ documentType: 'listing',
+ where: [['status', '==', 'available']],
+ orderBy: [['priceUsd', 'asc']],
+ limit: 20,
+});
+
+for (const [id, doc] of results) {
+ if (!doc) continue;
+ const data = doc.getData();
+ console.log(`${data.year} ${data.make} ${data.model} — $${data.priceUsd}`);
+ console.log(` ID: ${id}`);
+}
+```
+
+### Search by make
+
+```typescript
+const toyotas = await sdk.documents.query({
+ contractId,
+ documentType: 'listing',
+ where: [
+ ['make', '==', 'Toyota'],
+ ['status', '==', 'available'],
+ ],
+ limit: 10,
+});
+```
+
+## Step 5: Update a listing
+
+Mark a listing as sold:
+
+```typescript
+const listingId = 'THE_LISTING_DOCUMENT_ID';
+
+await sdk.documents.replace({
+ contractId,
+ documentType: 'listing',
+ documentId: listingId,
+ document: {
+ make: 'Toyota',
+ model: 'Camry',
+ year: 2021,
+ mileageKm: 45000,
+ priceUsd: 22500,
+ description: 'Well-maintained, single owner, full service history.',
+ status: 'sold',
+ },
+ identityId,
+ privateKeyWif,
+ signingKeyIndex,
+ nonce: await sdk.identities.contractNonce(identityId, contractId),
+});
+
+console.log('Listing marked as sold');
+```
+
+## Step 6: Leave a review
+
+```typescript
+await sdk.documents.create({
+ contractId,
+ documentType: 'review',
+ document: {
+ sellerId: 'SELLER_IDENTITY_ID',
+ listingId: 'THE_LISTING_DOCUMENT_ID',
+ rating: 5,
+ comment: 'Great seller, car was exactly as described!',
+ },
+ identityId: buyerIdentityId,
+ privateKeyWif: buyerKeyWif,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(buyerIdentityId, contractId),
+});
+```
+
+### Query reviews for a seller
+
+```typescript
+const reviews = await sdk.documents.query({
+ contractId,
+ documentType: 'review',
+ where: [['sellerId', '==', 'SELLER_IDENTITY_ID']],
+ orderBy: [['rating', 'desc']],
+ limit: 50,
+});
+
+let totalRating = 0;
+let count = 0;
+for (const [, doc] of reviews) {
+ if (!doc) continue;
+ totalRating += doc.getData().rating;
+ count++;
+}
+console.log(`Average rating: ${(totalRating / count).toFixed(1)} (${count} reviews)`);
+```
+
+## Next steps
+
+- Add **indexes** to the contract schema for efficient queries on `make`,
+ `year`, and `priceUsd`
+- Add a `location` field and query by region
+- Use **document pricing** (`sdk.documents.setPrice` / `sdk.documents.purchase`)
+ to let buyers pay for premium listing details
+- Integrate with a frontend framework (React, Vue, etc.) for a full web app
diff --git a/book/src/evo-sdk/tutorials/card-game.md b/book/src/evo-sdk/tutorials/card-game.md
new file mode 100644
index 00000000000..89567f8eee7
--- /dev/null
+++ b/book/src/evo-sdk/tutorials/card-game.md
@@ -0,0 +1,401 @@
+# Tutorial: Card Game with Tokens
+
+Build a collectible card game on Dash Platform where cards are documents that
+can be traded, and an in-game currency token is used for purchases. This
+tutorial combines data contracts, documents, and tokens into a cohesive
+application.
+
+> **Environment:** Steps 1-2 (contract design and deployment) are run from a
+> **Node.js script** using a developer/operator identity. Steps 3 onward
+> (minting, trading, querying) can run in either Node.js or a **browser app**
+> using the published contract ID.
+
+## What you will learn
+
+- Designing a contract with both document types and tokens
+- Using documents as game items (cards) owned by identities
+- Token-based in-game economy (minting rewards, spending on packs)
+- Document transfers for card trading between players
+- Querying collections and leaderboards
+
+## Prerequisites
+
+```sh
+npm install @dashevo/evo-sdk
+```
+
+You need a funded testnet identity. This tutorial uses two identities to
+demonstrate trading.
+
+## Step 1: Design the game contract
+
+The contract defines three document types and one token:
+
+- **card** — A collectible card with rarity, power, and element
+- **deck** — A player's active deck configuration
+- **match** — Match result history
+- **GemToken** — In-game currency for buying card packs
+
+```typescript
+const gameSchema = {
+ card: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', maxLength: 64 },
+ element: { type: 'string', enum: ['fire', 'water', 'earth', 'air', 'shadow'] },
+ rarity: { type: 'string', enum: ['common', 'uncommon', 'rare', 'legendary'] },
+ power: { type: 'integer', minimum: 1, maximum: 100 },
+ defense: { type: 'integer', minimum: 1, maximum: 100 },
+ ability: { type: 'string', maxLength: 128 },
+ edition: { type: 'integer', minimum: 1 },
+ },
+ required: ['name', 'element', 'rarity', 'power', 'defense', 'edition'],
+ additionalProperties: false,
+ },
+ deck: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', maxLength: 64 },
+ cardIds: {
+ type: 'array',
+ items: { type: 'string', maxLength: 44 },
+ minItems: 5,
+ maxItems: 10,
+ },
+ },
+ required: ['name', 'cardIds'],
+ additionalProperties: false,
+ },
+ match: {
+ type: 'object',
+ properties: {
+ player1Id: { type: 'string', maxLength: 44 },
+ player2Id: { type: 'string', maxLength: 44 },
+ winnerId: { type: 'string', maxLength: 44 },
+ player1Score: { type: 'integer', minimum: 0 },
+ player2Score: { type: 'integer', minimum: 0 },
+ timestamp: { type: 'integer' },
+ },
+ required: ['player1Id', 'player2Id', 'winnerId', 'timestamp'],
+ additionalProperties: false,
+ },
+};
+
+const gemTokenConfig = {
+ conventions: {
+ localizations: {
+ en: {
+ shouldCapitalize: true,
+ singularForm: 'Gem',
+ pluralForm: 'Gems',
+ },
+ },
+ decimals: 0, // whole numbers only
+ },
+ manualMinting: {
+ rules: { type: 'ownerOnly' },
+ },
+ manualBurning: {
+ rules: { type: 'ownerOnly' },
+ },
+ maxSupply: 10_000_000, // 10 million Gems total
+};
+```
+
+## Step 2: Deploy the contract
+
+```typescript
+import { EvoSDK } from '@dashevo/evo-sdk';
+
+const sdk = EvoSDK.testnetTrusted();
+await sdk.connect();
+
+// Game operator identity
+const operatorId = 'OPERATOR_IDENTITY_ID';
+const operatorKey = 'OPERATOR_PRIVATE_KEY_WIF';
+
+const contract = await sdk.contracts.publish({
+ identityId: operatorId,
+ documentSchemas: gameSchema,
+ tokens: [gemTokenConfig],
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(operatorId),
+});
+
+const contractId = contract.getId().toString();
+const gemTokenId = await sdk.tokens.calculateId(contractId, 0);
+
+console.log('Game contract:', contractId);
+console.log('Gem token:', gemTokenId);
+```
+
+## Step 3: Mint starter Gems for a new player
+
+When a player joins, give them starter Gems:
+
+```typescript
+async function onboardPlayer(playerId: string) {
+ // Gift 100 Gems to the new player
+ await sdk.tokens.mint({
+ tokenId: gemTokenId,
+ amount: 100,
+ recipientId: playerId,
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(operatorId),
+ });
+
+ console.log(`Welcomed ${playerId} with 100 Gems`);
+}
+```
+
+## Step 4: Create a card pack (operator mints cards)
+
+The operator creates cards as documents. Each card is owned by the operator
+initially, then transferred to players when purchased.
+
+```typescript
+// Define a set of cards for a pack
+const starterPack = [
+ { name: 'Flame Sprite', element: 'fire', rarity: 'common', power: 15, defense: 10, edition: 1 },
+ { name: 'Tidal Guardian', element: 'water', rarity: 'common', power: 10, defense: 20, edition: 1 },
+ { name: 'Stone Golem', element: 'earth', rarity: 'uncommon', power: 25, defense: 30, edition: 1 },
+ { name: 'Wind Dancer', element: 'air', rarity: 'common', power: 20, defense: 12, edition: 1 },
+ { name: 'Shadow Wraith', element: 'shadow', rarity: 'rare', power: 40, defense: 15, edition: 1 },
+];
+
+async function createCards(cards: typeof starterPack) {
+ for (const card of cards) {
+ await sdk.documents.create({
+ contractId,
+ documentType: 'card',
+ document: card,
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(operatorId, contractId),
+ });
+ console.log(`Created: ${card.name} (${card.rarity})`);
+ }
+}
+
+await createCards(starterPack);
+```
+
+## Step 5: Player buys a card pack
+
+The purchase flow:
+1. Player spends Gems (transfer to operator)
+2. Operator transfers card documents to the player
+
+```typescript
+const PACK_PRICE = 50; // 50 Gems per pack
+
+async function buyPack(playerId: string, playerKey: string) {
+ // Player pays Gems to the operator
+ await sdk.tokens.transfer({
+ tokenId: gemTokenId,
+ amount: PACK_PRICE,
+ recipientId: operatorId,
+ identityId: playerId,
+ privateKeyWif: playerKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(playerId),
+ });
+ console.log(`Player paid ${PACK_PRICE} Gems`);
+
+ // Operator transfers cards to the player
+ // (In production, select random cards from available pool)
+ const availableCards = await sdk.documents.query({
+ contractId,
+ documentType: 'card',
+ where: [['$ownerId', '==', operatorId]],
+ limit: 5,
+ });
+
+ for (const [cardId, card] of availableCards) {
+ if (!card) continue;
+ await sdk.documents.transfer({
+ contractId,
+ documentType: 'card',
+ documentId: cardId,
+ recipientId: playerId,
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(operatorId, contractId),
+ });
+ console.log(`Transferred ${card.getData().name} to player`);
+ }
+}
+```
+
+## Step 6: Query a player's collection
+
+```typescript
+async function getCollection(playerId: string) {
+ const cards = await sdk.documents.query({
+ contractId,
+ documentType: 'card',
+ where: [['$ownerId', '==', playerId]],
+ orderBy: [['power', 'desc']],
+ limit: 100,
+ });
+
+ console.log(`\n${playerId}'s collection:`);
+ for (const [id, card] of cards) {
+ if (!card) continue;
+ const d = card.getData();
+ console.log(` [${d.rarity}] ${d.name} — ${d.element} — ATK:${d.power} DEF:${d.defense}`);
+ }
+
+ return cards;
+}
+```
+
+### Filter by rarity
+
+```typescript
+const legendaries = await sdk.documents.query({
+ contractId,
+ documentType: 'card',
+ where: [
+ ['$ownerId', '==', playerId],
+ ['rarity', '==', 'legendary'],
+ ],
+ limit: 50,
+});
+```
+
+## Step 7: Trade cards between players
+
+Player-to-player trading using document transfers:
+
+```typescript
+async function tradeCards(
+ fromId: string, fromKey: string, fromCardId: string,
+ toId: string, toKey: string, toCardId: string,
+) {
+ // Player A sends their card to Player B
+ await sdk.documents.transfer({
+ contractId,
+ documentType: 'card',
+ documentId: fromCardId,
+ recipientId: toId,
+ identityId: fromId,
+ privateKeyWif: fromKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(fromId, contractId),
+ });
+
+ // Player B sends their card to Player A
+ await sdk.documents.transfer({
+ contractId,
+ documentType: 'card',
+ documentId: toCardId,
+ recipientId: fromId,
+ identityId: toId,
+ privateKeyWif: toKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(toId, contractId),
+ });
+
+ console.log('Trade complete!');
+}
+```
+
+## Step 8: Record a match result
+
+```typescript
+async function recordMatch(
+ player1Id: string, player2Id: string,
+ winnerId: string,
+ p1Score: number, p2Score: number,
+) {
+ await sdk.documents.create({
+ contractId,
+ documentType: 'match',
+ document: {
+ player1Id,
+ player2Id,
+ winnerId,
+ player1Score: p1Score,
+ player2Score: p2Score,
+ timestamp: Date.now(),
+ },
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.contractNonce(operatorId, contractId),
+ });
+
+ // Reward the winner with Gems
+ await sdk.tokens.mint({
+ tokenId: gemTokenId,
+ amount: 10,
+ recipientId: winnerId,
+ identityId: operatorId,
+ privateKeyWif: operatorKey,
+ signingKeyIndex: 0,
+ nonce: await sdk.identities.nonce(operatorId),
+ });
+
+ console.log(`Match recorded. ${winnerId} wins and earns 10 Gems!`);
+}
+```
+
+## Step 9: Leaderboard
+
+Query match history to build a win count:
+
+```typescript
+async function getWinCounts() {
+ const matches = await sdk.documents.query({
+ contractId,
+ documentType: 'match',
+ orderBy: [['timestamp', 'desc']],
+ limit: 100,
+ });
+
+ const wins = new Map Loading... {error} ID: {identity.getId().toString()} Balance: {identity.getBalance().toString()} credits Public keys: {identity.getPublicKeys().length} Loading listings... {error} No listings found. Waiting for SDK connection...Fetch Identity
+
+
+ {isLoading && searchId && Available Cars
+
+
+ {[...results.entries()].map(([id, doc]) => {
+ if (!doc) return null;
+ const d = doc.getData();
+ return (
+
+ Dash Platform App
+