Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

DSM, Deterministic State Machine

Plug your app into DSM

Your app stays what it is: a game, a shop, a service, in the language you already use. You don't build on DSM the way you build on a blockchain, with contracts and a new language. You plug in. Your app gets its own DSM account, your users connect their DSM wallet to it, and from then on your app can ask the wallet for things. Your own account tells you, with evidence, what happened.

  • You write no Kotlin, Swift or Rust. The wallet exists, on Android and iOS.
  • You hold no user's keys, passwords or card numbers.
  • You learn a dozen words: offer, connections, request, result; send, owns, accept, trade, match, login; issue, give.

This guide walks the whole path, a page at a time. Every command on these pages is in a box with a copy button; paste it into a terminal as it is. The screenshots are from real runs.

  1. Set up your machine: Node.js, and DSM's tools from a release: no Rust, no clone.
  2. The wallet on Android and iOS: what your players run. Optional; a wallet in the terminal does for testing.
  3. Your app's own DSM account: one command, and your app has an account on the beta network, funded.
  4. Make an offer, connect a wallet: a code, a scan, an approval.
  5. Ask the wallet for things: a payment, a proof of what it owns, a sign-in, an item.
  6. Write it into your app: the same calls in TypeScript and in Python.

Then, when you want them: shops, matches and contacts, running your app's account in production, and a storage set of your own.

Where this takes you

By the fifth page your app has shown a code, a wallet has connected to it, and your app has asked it for things:

The starter's connect code: a QR, a link, and the code as text
Your app shows a code.
Android: My Game asks to connect, with what it asks for and the caps
The player sees what it asks for, and approves.
Android: a sign-in waiting for the player
A sign-in, approved on the phone.
The starter's requests: two payments received and a sign-in signed
And your own account established the facts: two payments received, one sign-in signed.

In your app's code, the heart of it is this. The whole program, which ran as shown, is on Write it into your app.

import { ask, grant, localAccount } from "dsm-app-client";

const app = await localAccount("app");
const offer = await app.offer({ name: "My Game", grants: [grant.send("ERA", "0.10", "1.00")] });
console.log(offer.code); // show it as a QR code

// Once a wallet has connected to it (the whole program waits for one):
const player = (await app.connections()).find((c) => c.connected);
const paid = await app.wait(await app.request(player, ask.send("ERA", "0.05", "100 gems")));
if (paid.fact === "received") {
  // Your own account took up the transfer: hand over the gems.
}

Each page ends with all of its commands as one script, and Write it into your app has the whole app as one program, in JavaScript and in Python, to copy and run.

Apps already plugged into DSM, a game, a warehouse system and a prototype AI with a body, are on Apps plugged into DSM, with screenshots of each.

What DSM is, for an app builder

  • Each party keeps its own state. Your app has an account, each player has a wallet.
  • There is no global ledger to wait on, no validators and no fee market. Storage nodes keep an index of where everyone's state is, and decide nothing.
  • You plug in. You don't deploy storage nodes or run any part of the DSM network: your app runs its own DSM account beside your existing server.

Your app is an account

Your web app stays a web app. Beside it runs one process, the app host, and that process is your DSM account: one real identity, running the same code a phone runs. Your server talks to it on the same machine. Wallets reach it over TLS, through what it calls its relay.

 your app's server ──HTTP+protobuf (loopback, secret)──► dsm-app-host ──► storage nodes
                                                          (your app's      (the network's
                                                           DSM account)     storage set)
                                                               ▲
 player's DSM wallet (phone) ──HTTPS, pinned in the code──────┘  (the relay)

Important

Trust your account, not the relay. The relay carries messages between wallets and your account, and proves nothing. A wallet's answer to a request, its outcome, is what the wallet says happened. Your app acts only on a fact: what your own account established from the evidence itself, a transfer it accepted, a proof of holdings it verified, a signature it checked. Outcome and fact shows the two side by side.

Connect, in one picture

offer→code→scan→grant→requests→answers

Your app makes an offer: its name, and what it wants to be allowed to do. The offer becomes a code. A wallet scans it, shows the player what is asked, and the player approves a grant. From then on your app sends requests, and the wallet answers: at once when a request is inside the grant, and after the player says so when it is not.

A grant is the player's consent, written down: which kinds of requests your app may make without asking again, and the caps on each, per request and in all. It is not a validity rule. Every request still runs through the same routes the player would use by hand, so a grant never lets your app do something the player could not do themselves.

What your app can never do

  • Hold the player's keys. They never leave the phone.
  • Spend past a cap. The wallet refuses, and asks the player.
  • Treat anything the relay says as proof. The only facts are the ones your own account established.

What you can replace

You run todayWith DSMWhy it's better
Passwords, password resets, an authenticator app, SMS codes, security keysask.login: the player approves "Sign in to your app" on their device, and the wallet's identity signs it. Your account verifies the signature and gives you the fact signed.No password or code to store, leak, reuse or intercept. The player reads the words on their own device and signs them there, and the signature names this connection and this request, so it signs the player in to your app and nowhere else. It does not stop a player approving a look-alike app's own requests: the wallet shows which app is asking, and the player decides. Post-quantum signatures. No hardware key to buy, ship or replace.
A payment processor for in-app purchasesask.send: the wallet sends your account ERA, or a coin you issued. Fact: received.A transfer your account accepted is final; there is no chargeback. Small amounts cost what a storage write costs, not a percentage plus a fixed fee. Inside the grant's caps the player is not asked again.
An inventory database as the source of truth for who owns whatissue an object, give it, and later ask.owns: the wallet proves what it owns, and your account verifies the proof. Fact: owns.Items are the player's: they can send them to each other in the wallet, and your app still sees who holds what. Your database keeps game state; ownership lives with the player.
An escrow service, or trusting yourself with both players' stakesA match: both players stake from their own wallets, your app decides, the winner collects.Your app never holds the stakes. Each wallet locks its own, under terms it built itself; your key only decides.
A marketplace backend with its own order book and settlementA vault of your own that sells your coin for ERA (or any pair) at a fee you set, and ask.trade.Prices come from the vault's reserves, trades settle on the players' own devices, and your account earns the fee.
Account recovery flows, "forgot password"None. The identity is the player's wallet, which they back up themselves.Your app has nothing to recover.

What DSM does not replace: your app's own logic and state (DSM holds identities, objects and facts, not your game), and government identity. If you need KYC, keep your own record of it keyed by the player's DSM identity, which a sign-in proves. DSM itself holds no personal data, so KYC attaches to the identity, not to a password.

Who pays for what

Storage on DSM is paid for by the account that writes: a one-time entry payment, then credits consumed by each write. Nobody is billed per month or per user, and reading is free. That leaves you three ways to set things up:

  1. Players pay their own way. Each player's wallet is their own account, on the public set. In the beta, the faucet funds every new wallet. Your app pays only for its own account's writes. This is what this guide sets up: your app's account on the beta network, funded by the same faucet.
  2. Your app pays for its players. give a player ERA when they connect; their wallet spends it on its own writes. What you give is theirs, and works anywhere on DSM, so this is a gift, not a subscription.
  3. Your app runs the storage, and players pay nothing. Run your own storage set and point your players' wallets at it. A node may enforce the entry payment and credits, or not; on your own set, that is your call. You pay the hosting. Accounts on your set are still their owners', and a wallet on your set reaches only your set. A storage set of your own shows it on one machine.

What this replaces, in cost: a hardware key per user and its replacements, SMS codes per message, a processor's percentage on every payment, an escrow or marketplace provider. What you pay instead is your account's storage writes, in ERA, and hosting if you choose the third setup.

What you need

  • macOS or Linux. On Windows, use WSL2.
  • Outbound HTTPS. Nothing of the network runs on your machine: your app's account joins the beta network, the hosted storage set every wallet ships configured for.
  • No PostgreSQL and no storage node. You need those only if you later run a storage set of your own.

Next: Set up your machine.

Set up your machine

Plugging an app in needs Node.js 20 or later, Python 3 if you use the Python client, and curl. No Rust, no protobuf compiler, and no copy of the DSM repo: DSM's tools come prebuilt from a release, for Linux (x86-64 and ARM64) and macOS (Apple silicon and Intel). On Windows, use WSL2 with Ubuntu and follow the Ubuntu lines. PostgreSQL is not on the list either: you need it only if you later run storage nodes of your own.

1. Install the prerequisites

macOS, with Homebrew. macOS already has Python 3 with venv, and curl.

brew install node

Ubuntu or Debian. Ubuntu's own nodejs package is older than the Node.js 20 the tools need, so Node comes from NodeSource:

sudo apt update
sudo apt install -y curl ca-certificates python3-venv
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

These four lines are the ones the newcomer check (make first-run) runs on a clean Ubuntu before it follows this path to a payment received.

2. Install DSM's tools

Download the release's install script, then run it:

curl -fsSLO https://github.com/deterministicstatemachine/dsm/releases/download/v0.1.0-beta.4/install.sh
sh install.sh

It downloads the release's SHA256SUMS, then the app host built for your machine and the developer archive, and checks each against SHA256SUMS before it unpacks anything. A file that is missing from it or does not match stops the install, and nothing is changed. Then it installs into ~/.local:

  • dsm-app-host: your app's own DSM account.
  • dsm-app: your app's account from the terminal.
  • dsm-dev-wallet: a player's wallet in the terminal, to test with no phone.
  • dsm-connect-starter: the starter app, a page to build your app from.
  • The TypeScript and Python clients, as packages you install into your app, under ~/.local/share/dsm/clients.

What it printed on a Mac:

DSM 0.1.0-beta.4 for macos-arm64, from …
  verified  dsm-app-host-0.1.0-beta.4-macos-arm64.tar.gz  sha256 bfaebcc172e33f0a519bd4e50d42968bb7d67cee2a73eed78712a62c9af4f3eb
  verified  dsm-developer-0.1.0-beta.4.tar.gz  sha256 7ecc867880f1c6feeb82d7b1f48d7c840a58bb6bcfb6593e26360a3afe2cf40e

installed:
  ~/.local/bin/dsm-app-host        dsm-app-host 0.1.0-beta.4
  ~/.local/bin/dsm-app
  ~/.local/bin/dsm-dev-wallet
  ~/.local/bin/dsm-connect-starter
  the clients, to install into your app:
    ~/.local/share/dsm/clients/dsm-app-client-0.1.0-beta.4.tgz
    ~/.local/share/dsm/clients/dsm_app_client-0.1.0b4-py3-none-any.whl
note: ~/.local/bin is not on your PATH; add it (and the same line to your shell's profile):
  export PATH="$HOME/.local/bin:$PATH"

… is where it downloaded from, and the checksums are that build's; yours name the release's. To check the script itself before you run it, read it: it is short, and it is scripts/install.sh in the repo.

3. Put the tools on your PATH

If the install said ~/.local/bin is not on your PATH, add it to this terminal, and the same line to your shell's profile (~/.zshrc on macOS, ~/.bashrc on Ubuntu):

export PATH="$HOME/.local/bin:$PATH"

Then check:

dsm-app-host --version
dsm-app-host 0.1.0-beta.4

4. A folder for your app

Your app's account lives in the folder you start it from, so start from your app's own folder. To follow this guide, make one:

mkdir my-app && cd my-app

Every command from here on runs in this folder.

Building from source instead

To work on DSM itself, build the wallet (next page), or run storage nodes of your own (A storage set of your own), clone the repo and build it. That needs a C toolchain, Git, protobuf's compiler and Rust as well; a JDK 17 is for the Android wallet.

git clone https://github.com/deterministicstatemachine/dsm.git
cd dsm
make doctor

QUICKSTART.md lists the prerequisites. make doctor reads every tool the build needs and says what it found; it changes nothing on your machine. From the repo, the make targets run the same programs as the installed commands, built from your checkout:

InstalledFrom the repo
dsm-app up, dsm-app downmake app-dev, make app-dev-down
dsm-app <command>make app ARGS="<command>"
dsm-dev-wallet upmake dev-wallet
dsm-dev-wallet <command>make wallet ARGS="<command>"
DSM_WALLET_NAME=wallet2 dsm-dev-wallet upmake dev-wallet2
DSM_WALLET_NAME=wallet2 dsm-dev-wallet <command>make wallet2 ARGS="<command>"
dsm-connect-startermake starter
the clients' packagesclients/typescript, clients/python

The repo's accounts live in its own .dsm-dev folder, so run those from the repo's root.

Next: The wallet on Android and iOS, or skip straight to your app's own DSM account.

The wallet on Android and iOS

This page is optional. Everything in this guide can be tested with a wallet in the terminal, which the connect page starts with one command. Build the real wallet when you want to see what your players see.

As installed, the wallet is on the beta network: five storage nodes, named in a file inside the app with each one's certificate, so it trusts no node it was not built to trust. Your app's account will be on the same network.

Android

You need Android Studio with the Android NDK 27.0.12077973, and ANDROID_NDK_HOME set to it. With that in place, set the build up once:

make setup

Then build the wallet and install it on every phone connected over adb:

make install

make android builds the APK without installing it.

iOS

On a Mac with Xcode, and cbindgen:

cargo install cbindgen

Build the Rust SDK for iOS, the wallet's screens, and the app for the Simulator, then install it on the Simulator that is running and open it:

make ios-libs
make ios-frontend
make ios-simulator
make ios-simulator-run

The iOS app runs the same Rust SDK and the same screens as the Android one.

For a real iPhone or iPad, run make ios-device IOS_TEAM= followed by your Apple team id: Xcode signs it with automatic provisioning. A free Apple ID gives a profile that lasts a week; a developer account, a year.

The first run

The first run is the wallet's setup. You read the beta terms and tick each one, and you pick a look.

Before you start: the beta terms, each with a tick box
The beta terms. You continue once every point is ticked.
Choose your look: Modern or DGen, dark mode, simple mode
Choose your look: Modern or DGen.
Welcome to DSM, with the Create my wallet button
Create my wallet.

Then the wallet makes its identity on your phone: a recovery phrase of 24 words, which you write down and check. The phrase is not shown here. After the check, the wallet puts itself on the network: it publishes its directory entry to the storage nodes, and then it is ready.

Almost ready: putting your wallet on the DSM network
Putting the wallet on the network.
Device ready: your device key is enrolled and your identity is published
Device ready.
Protect your wallet: set up a PIN or a button combo
A PIN or a button combo, now or later.

Its first money is one tap: Get your welcome ERA, which the network's faucet gives a new account.

The home screen at 0.00 ERA with the Get your welcome ERA button
A new wallet, at 0.00 ERA.
The home screen at 100.00 ERA on iOS
100.00 ERA, on iOS.
The home screen at 100.00 ERA on Android
The same wallet on Android.

Each wallet now holds ERA from the faucet, so there is something to pay your app with. Nothing of the network runs on your machine.

Next: Your app's own DSM account.

Your app's own DSM account

Your app gets a DSM account. Not an API key, not a merchant id: an account like a wallet's, with its own identity, running the same code a phone runs.

your web app→loopback ingress
+ per-launch secret
→dsm-app-host
your account
→storage nodes

Your web app stays what it is: Node, Python, whatever it is today. Beside it runs one process, the app host, and that process is your account. Your app talks to it over a local socket on the same machine, loopback only, with a secret the host writes for each launch. Wallets reach the host over TLS, through its relay.

1. Start it

In your app's folder:

dsm-app up

This runs dsm-app-host in the background with a data directory, a directory for the ingress secret, and the beta network's client config, which the host carries: the same five storage nodes the wallet ships with. It keeps them under .dsm-dev in this folder.

The first launch creates the account. It writes a recovery mnemonic once to the data directory, readable by you alone, and makes the account the way a phone does. The host waits until the network holds the account's directory entry, and only then opens its ingress. Then the account is funded from the network's faucet.

What it prints, from a run on the beta network (13 seconds, on a Mac):

[dsm-app] app account running
  storage set:    the beta network (carried in dsm-app-host)
  ingress:        http://127.0.0.1:8787   (your server calls this)
  ingress secret: …/.dsm-dev/app/ingress/ingress-token
  relay:          https://127.0.0.1:8443   (wallets reach this)
  recovery:       …/.dsm-dev/app/data/RECOVERY_MNEMONIC
  log:            …/.dsm-dev/app/host.log
funded: claimed 100.00 ERA (economic position 1)

… is your app's folder. Line by line:

  • storage set: the beta network, hosted.
  • ingress, on loopback port 8787: your server calls this.
  • ingress secret: in its own directory, so your app can read the secret and nothing else of the account.
  • relay, on port 8443: wallets reach this.
  • recovery: the recovery mnemonic.
  • funded: 100 ERA from the faucet.

Warning

Back up the recovery mnemonic like a wallet's, because that is what it is. Whoever can read the data directory controls the account.

dsm-app up returns once the account is running; the host keeps running in the background. dsm-app down stops it, and dsm-app status says whether it runs. Run dsm-app up again whenever you like: it keeps the same account, and claims nothing more from the faucet once the account holds ERA (it says already holds and the amount).

2. Look at it

dsm-app talks to the account through the same ingress your code will. id prints the account's identity:

dsm-app id
P1TTQ65K20YCW6FCHVJ8A8V0VQ55NN3DZK6KCBFMCC3M98KN3X2G

owned prints what it holds: a hundred ERA from the faucet, and nothing of dBTC yet.

dsm-app owned
ERA                100.00   currency NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0
dBTC           0.00000000   currency 0EJ2PSRS2YQR8BR77P3WZ92SV12VJT7XP6NWP0SH5P8MWD81C8H0

Each line is an object: its symbol, the amount in its own units, its kind, and its id.

activity is the account's own record of every call made through the ingress:

dsm-app activity
account P1TTQ65K20YCW6FCHVJ8A8V0VQ55NN3DZK6KCBFMCC3M98KN3X2G
1  ROUTE  faucet.claim  claim ERA from the network's faucet  -> claimed 100.00 ERA (economic position 1)

Your app can read the activity too, from any point. That is how it learns what happened without trusting anyone's say-so. Your identity and ids will differ from these; everything else reads the same.

3. The ingress

Three routes make the ingress:

RouteWhat it does
POST /ingressMakes a call, and records it in the account's activity.
POST /ingress/quietMakes a call that is not recorded: what your app polls with.
GET /activity/{after}Reads what was recorded after that point.

Every call carries the launch's secret as Authorization: Bearer <secret>, must name the ingress as its Host, and may come from no other web origin. A web page in a browser on the same machine can neither read the secret nor send it. The clients wrap all of this; you will not write these requests by hand.

4. The wire

On the wire, each call is one protobuf message, the route's name and its arguments, answered with one message back. The schema is one file, proto/dsm_app.proto, and the TypeScript and Python clients are generated from it. For a client in another language, that file is all you need: send an IngressRequest (a RouterInvokeOp or RouterQueryOp: the route's name and an ArgPack holding its request message) to POST /ingress, and an IngressResponse comes back. clients/typescript/src/ingress.ts is the whole exchange, in one short file.

5. The first call

Here is the first call in TypeScript and in Python: open the local account, and ask for its connections. First the client, into your app's folder. TypeScript, from the package the install put in ~/.local/share/dsm/clients:

npm init -y
npm install ~/.local/share/dsm/clients/dsm-app-client-0.1.0-beta.4.tgz

npm init -y makes a package.json if your folder has none. The client's types are inside the package, so nothing is compiled. Then:

import { localAccount } from "dsm-app-client";

const app = await localAccount("app");
console.log(await app.connections());
from dsm_app_client import local_app

app = local_app()
print(app.connections())

Each prints an empty list: nothing is connected yet. localAccount("app") and local_app() find the account dsm-app up started by looking for the .dsm-dev folder above the folder you run in; from anywhere else, name it with DSM_DEV_DIR. Write it into your app runs whole programs.

The Python client goes into a virtual environment, from its package beside the TypeScript one:

python3 -m venv .venv && . .venv/bin/activate
pip install ~/.local/share/dsm/clients/dsm_app_client-0.1.0b4-py3-none-any.whl
python3 ~/.local/share/dsm/examples/account_status.py

account_status.py prints your account at a glance: its identity, what it owns, every connected wallet with what it may do, and the latest activity. Its types are inside the package too, so pip installs it with nothing to compile; it fetches one dependency, protobuf.

The whole page, as one script

dsm-app up
dsm-app id
dsm-app owned
dsm-app activity

Next: Make an offer, connect a wallet.

Make an offer, connect a wallet

Your app shows a code, and a wallet connects to it. By the end of this page a wallet is connected to your account, and your app knows exactly what the wallet has let it do.

What an offer is

An offer is what your app asks for, before a wallet agrees to anything. It carries your app's name and a list of grants: things the wallet will let your app do without asking the player again, each with a cap. Send, up to so much per request and so much in all. Owns, to prove what it holds. Accept, to take objects your app issues. The wallet shows all of it to the player before they approve. Nothing in a grant is hidden, and nothing outside it runs without the player.

In code, an offer is one call:

const offer = await app.offer({
  name: "My Game",
  grants: [grant.send("ERA", "0.10", "1.00"), grant.owns(["ERA"]), grant.accept()],
});
console.log(offer.code);

Amounts are in the object's own units: "0.10" is a tenth of an ERA. This page makes offers two ways: on the starter's page, and from the command line.

1. The starter: your app as a page

The starter is a web app plugged into DSM, and the place to build your app from pieces. It runs in the foreground, so give it a terminal of its own, in your app's folder:

dsm-connect-starter

Open http://127.0.0.1:3000.

The Connect Starter page: its account's identity, what the page is, and a switch between TypeScript and Python
The top of the page: your account's identity, and the switch that shows every piece's code in TypeScript or in Python.

The page is a builder, with three kinds of pieces, in the order an app uses them:

  1. Objects your app issues: a coin, points, an item (supply 1 is a one-of-a-kind), a ticket; and a shop, a vault that sells one object for another.
  2. What the wallet lets your app do: the offer. Tick the grants (send, owns, accept, contacts, match, trade), set each cap, and press Show the connect code.
  3. Ask a connected wallet: send, prove what it owns, sign in, sign a statement, accept or receive an object, price or make a trade through your shop, stake on a match, decide it, collect.

Under every piece is the exact code it is, with a Copy button. The page never makes that code up: it asks the starter's server, which runs one mapping from piece to client call both to show the code and to make the call. Set the pieces up the way your app needs them, press the button to see it happen, then paste the code into your own app.

The offer piece: the app's name, the grants with their caps, the Show the connect code button, and the offer's code under it
The offer: tick the grants, set the caps. The code under it changes with them.
The connect code as a QR code, a link to open it in the wallet on the same device, and the code as text
Show the connect code: a QR for a phone, a link for a wallet on the same device, and the code as text.

To connect the terminal wallet below to the page's code instead of the command line's, copy the code under the QR and run dsm-dev-wallet connect followed by it.

2. A wallet in the terminal

To test without a phone, start a player's wallet in the terminal:

dsm-dev-wallet up

It is a second account, made the same way dsm-app up made yours, on the same network, and funded from the faucet:

[dsm-dev-wallet] wallet account running
  storage set:    the beta network (carried in dsm-app-host)
  ingress:        http://127.0.0.1:8788   (your server calls this)
  ingress secret: …/.dsm-dev/wallet/ingress/ingress-token
  relay:          https://127.0.0.1:8444   (wallets reach this)
  recovery:       …/.dsm-dev/wallet/data/RECOVERY_MNEMONIC
  log:            …/.dsm-dev/wallet/host.log
funded: claimed 100.00 ERA (economic position 1)

dsm-dev-wallet down stops it.

3. Make the offer from the command line

CODE=$(dsm-app offer 'My Game' ERA 0.10 1.00)

The words after offer are your app's name, the object a wallet may send it, the cap per request and the cap in all. The command line's offer asks for more than sends: to be shown what the wallet owns, to hand it objects your account issues, to see its contacts, and to referee matches it stakes ERA in (1.00 a match, 2.00 in all).

The code is now in $CODE:

echo $CODE
dsm:connect/v1:18B6GX3ME1SKMBSF64S3EBHG5RR2WC9T70T38CRJ41QXQJEH1V239MWMG2Q6RPQ5SJ4RD3XP78QK4KMD5G86Z5SCMB2MY6H0TPTDHBBYF15C78D78Z8ACYG4C5HQ0A6TAVSEY614C1D27CXRNWGG

It is long because it carries the relay's address and its certificate pin: the wallet fetches the offer itself, over TLS pinned to that certificate.

4. Connect the terminal wallet

dsm-dev-wallet connect "$CODE"

The wallet fetches the offer, checks it, and prints what My Game asks for. The terminal wallet is your test wallet, so it approves on its own, and prints the connection: its id, the app, CONNECTED, and the grants it now holds.

My Game (account 0RHS9YDRSPR9PG9WESBX7PMGAVMEJSSVSC11QSD4NG4JPKY3XQ80) asks to:
  - Send this app: up to 0.10 ERA a time, 1.00 ERA in all
  - Prove what you own of ERA and of objects this app issued
  - Receive objects this app issues
  - See which of your contacts are here: their DSM IDs, not their names
  - Lock stakes for matches: up to 1.00 ERA a time, 2.00 ERA in all
  - Up to 64 requests that spend nothing; after them, each waits for you
approved:
connection RKBQ40RXFBYWZCJ150X3XG32V7EQYPN1SDGV96VGGTCAQCJMCSTG  My Game  CONNECTED
  grants: Send this app: up to 0.10 ERA a time, 1.00 ERA in all
  grants: Prove what you own of ERA and of objects this app issued
  grants: Receive objects this app issues
  grants: See which of your contacts are here: their DSM IDs, not their names
  grants: Lock stakes for matches: up to 1.00 ERA a time, 2.00 ERA in all
  grants: 0 of 64 requests that spend nothing used

A player on a phone reads the same list and decides.

5. See it from your app's side

dsm-app connections
connection RKBQ40RXFBYWZCJ150X3XG32V7EQYPN1SDGV96VGGTCAQCJMCSTG  wallet HA62NJZ06ZY8C79HKM39AGTC3CMX9348RJH30GZTHA00BCSP0KV0  CONNECTED

One line per wallet: the connection's id, the wallet's identity, and whether it is connected. The connection's id is what every request on the next page names. The starter lists the same, with what each connection may do:

The starter's Connected wallets: each connection, the wallet's identity, its grants, and whether it is connected
Connected wallets on the starter, after the next pages' steps too, so it lists several: the ones a wallet disconnected stay listed, with what they allowed.

6. On a phone

On Android, the wallet's Apps screen has Connect an app. Scan the starter's QR with Scan code, or open the code as a link on the same phone and press Read. The wallet shows the same list the terminal printed: who asks, what for, and the caps. The player approves.

Android: the Apps screen with a connect code and the Read button
A code, handed over as a link.
Android: My Game asks to connect, with the list of grants and their caps
Who asks, and what for.
Android: the end of the list, with Cancel and Approve
The player approves, or cancels.

The iOS Simulator has no camera, so there the code opens as a link. From the repo (building from source), this opens a code in the wallet on every phone on your machine at once, the adb devices and the booted iOS Simulators (debug builds of the wallet):

make phones-connect CODE="$CODE"
iOS: the Apps screen with a connect code and the Read button
The code, opened as a link.
iOS: My Game asks to connect, with the list of grants
The same list on iOS.
iOS: Cancel and Approve
Approve.
iOS: Connected to My Game, with a Disconnect button
Connected. The player can disconnect at any time.

The red box says the code came from outside the wallet. Any app or web page on a phone can open such a link, so the wallet asks the player to approve only an offer they opened themselves, from the app they mean to connect.

These phones were recorded before the command line's offer asked to see contacts, so their lists are one line shorter than the one above.

What each side holds now

  • The wallet holds a grant it can read and end at any time.
  • Your account holds a connection to the wallet.
  • No keys moved, and nothing was taken on the relay's word.

A wallet connects to an app once. To connect it again, it disconnects first (the next page ends with that), or a second wallet connects: DSM_WALLET_NAME=wallet2 dsm-dev-wallet up, then DSM_WALLET_NAME=wallet2 dsm-dev-wallet connect $CODE.

The whole page, as one script

With dsm-app up running, this makes the terminal wallet, the offer and the connection:

dsm-dev-wallet up
CODE=$(dsm-app offer 'My Game' ERA 0.10 1.00)
echo $CODE
dsm-dev-wallet connect "$CODE"
dsm-app connections

Next: Ask the wallet for things.

Ask the wallet for things

Your app asks a connected wallet for things. Inside the grant, it just happens. Outside it, the player decides. In every case, your own account checks the result.

Outcome and fact

Every request has two answers, and only one is evidence:

Where it comes fromUse it for
outcomethe wallet's word: carried_out, awaiting_approval, declined, failedwhat to show the player while you wait
factwhat your own account established: received, owns, signed, staked, collecteddeciding anything: delivering the item, logging the player in, crediting the sale

received means your account has taken up a transfer the wallet sent. owns means a proof of what the wallet owns verified. signed means a signature checked. Code against the fact: the outcome tells you what the wallet said, and the fact tells you what happened.

The commands below use the terminal wallet from the last page. Each request gets a number on its connection, and result reads what became of it.

Note

On the beta network a request takes from a few seconds to about half a minute to settle: the wallet and your account each write to the storage nodes over the internet. Until it settles, result says so:

answered: no  outcome: unanswered
fact: none  no transfer this account accepted answers it yet; an answer saying sent grants nothing

Run it again a little later. The wallet also answers a connection's requests in order, so a request that waits for the player holds up the ones after it.

1. Find the connection

S=$(dsm-app connections | awk -v w="$(dsm-dev-wallet id)" '$4==w && $NF=="CONNECTED"{print $2}')

This puts the terminal wallet's connection id in $S: the line of connections whose wallet is your terminal wallet, and is connected.

2. A send inside the grant

Ask the wallet to send your account five hundredths of an ERA:

dsm-app send $S ERA 0.05
request 1

Once it settles:

dsm-app result $S 1
answered: yes  outcome: carried_out
fact: received  the transfer is accepted onto this account's relationship with the wallet

No prompt on the phone: 0.05 is inside the cap the player approved.

3. A send over the cap

Now 0.20 ERA, twice the cap per request:

dsm-app send $S ERA 0.20
request 2

The wallet does not carry it out. It waits for the player, and says why:

dsm-dev-wallet pending
RKBQ40RXFBYWZCJ150X3XG32V7EQYPN1SDGV96VGGTCAQCJMCSTG 2  My Game: Send 0.20 ERA for dsm-app send
  why it waits: 0.20 ERA is above the 0.10 ERA the grant allows per request

If it says nothing waiting, the request has not reached the wallet yet: give it a moment. The player approves it, here in the terminal, or on the phone:

dsm-dev-wallet approve $S 2
CARRIED_OUT: My Game: Send 0.20 ERA for dsm-app send, carried out.

Approving too early fails with request 2 is not waiting; list pending again first. About ten seconds later:

dsm-app result $S 2
answered: yes  outcome: carried_out
fact: received  the transfer is accepted onto this account's relationship with the wallet

The same fact, one more step. On a phone, the request waits under Waiting, with the reason:

Android: My Game asks for 0.20 ERA, outside what you approved, with Decline and Approve
Outside the grant: the player decides.
Android: My Game's request for 0.20 ERA, carried out
Approved and carried out.
iOS: the same request over the cap, waiting
The same on iOS.
iOS: My Game's request for 0.20 ERA, carried out
Carried out.

4. Prove what it owns

owns asks the wallet to prove what it holds of an object. The wallet builds a proof at its current position, and your account verifies it, not the wallet's say-so:

dsm-app owns $S ERA
request 3
dsm-app result $S 3
answered: yes  outcome: carried_out
fact: owns  proven at the wallet's position 3, whose next cell is still open
  owns 9975 of NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0

The amount is in the object's smallest units, and the long id is ERA's: 9975 is 99.75 ERA after the two sends.

5. Sign in

login is a sign-in. Your app makes a nonce and asks the wallet to sign "Sign in to", your app's name, and the nonce. It always waits for the player, because only they can sign:

dsm-app login $S
request 4: waits for the player to approve it on the device

Pending shows the exact words to be signed:

dsm-dev-wallet pending
RKBQ40RXFBYWZCJ150X3XG32V7EQYPN1SDGV96VGGTCAQCJMCSTG 4  My Game: Sign this: "Sign in to dsm-app (3a6b4e28-b931-4116-b53a-b1681d5aa7b3)"
  why it waits: only you can sign this, so it always waits for you

The player approves:

dsm-dev-wallet approve $S 4
CARRIED_OUT: My Game: Sign this: "Sign in to dsm-app (3a6b4e28-b931-4116-b53a-b1681d5aa7b3)", carried out.
dsm-app result $S 4
answered: yes  outcome: carried_out
fact: signed  the wallet signed these words after they were approved on its device: proof of its key, not of who pressed
  signed: Sign in to dsm-app (3a6b4e28-b931-4116-b53a-b1681d5aa7b3)

Your app compares the signed words with what it asked. That is a password, an authenticator code and a security key in one request, with nothing for you to store but the nonce.

Android: My Game asks to sign Sign in to Connect Starter, with Decline and Approve
A sign-in from the starter, on Android.
iOS: the same sign-in request
And on iOS. Only the player can sign, so it always waits.

6. Issue an object and give it

Your app issues an object: SHIELD, a one-of-a-kind item, supply 1. Your account holds it:

dsm-app issue SHIELD shield 1
issued SHIELD (unique, supply 1): C46M962T804B5XND8P2ASG4QK2VCB7SCGE5RNWRA4ST4112EK400

A wallet receives only what it has taken up itself, so your app asks it to accept the object:

dsm-app accept $S SHIELD
request 5

Give it a few seconds, then give it the shield:

dsm-app give $(dsm-dev-wallet id) SHIELD 1
sent

The transfer is final once sent; the wallet takes it up within seconds. Until then what it owns shows the shield at 0:

dsm-dev-wallet owned
ERA                 99.75   currency NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0
SHIELD                  1   unique   C46M962T804B5XND8P2ASG4QK2VCB7SCGE5RNWRA4ST4112EK400
dBTC           0.00000000   currency 0EJ2PSRS2YQR8BR77P3WZ92SV12VJT7XP6NWP0SH5P8MWD81C8H0

The item is the player's now. They can send it on in the wallet, and your app still sees who holds it with owns.

SHIELD's supply is 1, so it can be given once. Run this page again on the same account and issue answers with the same SHIELD, but give fails: the account no longer holds it (balance underflow on debit (insufficient funds)).

7. Disconnect

dsm-dev-wallet disconnect $S
connection RKBQ40RXFBYWZCJ150X3XG32V7EQYPN1SDGV96VGGTCAQCJMCSTG  My Game  DISCONNECTED
  grants: Send this app: up to 0.10 ERA a time, 1.00 ERA in all
  grants: Prove what you own of ERA and of objects this app issued
  grants: Receive objects this app issues
  grants: See which of your contacts are here: their DSM IDs, not their names
  grants: Lock stakes for matches: up to 1.00 ERA a time, 2.00 ERA in all
  grants: 2 of 64 requests that spend nothing used
  used:   0.05 of 2.00 ERA

The grant ends, and the connection shows DISCONNECTED on your side too. used counts what went through the grant: the 0.05. The 0.20 the player approved by hand is not in it. A wallet that wants to connect again does it with a new offer.

The same, on the starter's page

Everything above is a piece on the starter's page. Pick the wallet, pick what to ask, and the code under it is the call; Ask makes it:

Ask a connected wallet: the wallet's connection, send your app an object, ERA, 0.05, the Ask button, and the TypeScript code for the request
Asking for 0.05 ERA. The code is the whole call, with the connection filled in.
Ask a connected wallet: sign in, with the TypeScript code that makes a nonce, asks, waits and checks the signed words
A sign-in: the code keeps the nonce and checks the signed words against it.

The page keeps a table of what it asked. Here: a send inside the grant, one over it (the player approved it in the terminal), and a sign-in, each with the wallet's answer and the fact your account established:

Requests made: 1 send 0.05 ERA received, 2 send 0.20 ERA received after waiting for approval, 3 sign in signed
The facts are the evidence: received, received, signed.

And the account's activity, read from the ingress: every call your app made, and every time the wallet fetched its requests or answered one through the relay:

Account activity: offers, requests, the wallet fetching its requests and answering them through the relay
The relay's part is kept and labelled for what it is: a signed notification that establishes nothing.

What to keep from this page

  • Log the facts, not the outcomes.
  • Show players what you asked for and what they approved.
  • Trust nothing the relay says. Your account already checked.

The whole page, as one script

With the terminal wallet connected from the last page, this runs every step above, waiting for each request to settle before reading it, and approving as the player:

S=$(dsm-app connections | awk -v w="$(dsm-dev-wallet id)" '$4==w && $NF=="CONNECTED"{print $2}')
dsm-app send $S ERA 0.05
until dsm-app result $S 1 | grep -q "^fact: received"; do sleep 3; done
dsm-app result $S 1
dsm-app send $S ERA 0.20
until dsm-dev-wallet pending | grep -q "why it waits"; do sleep 3; done
dsm-dev-wallet pending
dsm-dev-wallet approve $S 2
until dsm-app result $S 2 | grep -q "^fact: received"; do sleep 3; done
dsm-app result $S 2
dsm-app owns $S ERA
until dsm-app result $S 3 | grep -q "^fact: owns"; do sleep 3; done
dsm-app result $S 3
dsm-app login $S
until dsm-dev-wallet pending | grep -q "Sign this"; do sleep 3; done
dsm-dev-wallet pending
dsm-dev-wallet approve $S 4
until dsm-app result $S 4 | grep -q "^fact: signed"; do sleep 3; done
dsm-app result $S 4
dsm-app issue SHIELD shield 1
dsm-app accept $S SHIELD
until dsm-app result $S 5 | grep -q "^answered: yes"; do sleep 3; done
dsm-app give $(dsm-dev-wallet id) SHIELD 1
until dsm-dev-wallet owned | grep -Eq "^SHIELD +1 "; do sleep 3; done
dsm-dev-wallet owned
dsm-dev-wallet disconnect $S

Next: Write it into your app.

Write it into your app

Everything the command line did, your app does with a client: JavaScript or TypeScript on Node.js 20 or later, or Python 3.10 or later. Both are generated from the same schema and use the same words: offer, connections, request, result, wait; issue, give; owned, owns. Requests are built with ask.*, and what an offer asks for with grant.*.

Your app needs a server side, even a small one, because the account's secret must never reach a browser page. The starter is one server file, and a page.

This page has the whole of an app, twice: the same program in JavaScript and in Python. Each makes an offer, waits for a wallet to connect to it, takes a payment, gives the player 100 gems of its own coin, and signs the player in. Copy either one whole. Both ran, as written, against an account on the beta network, and their output is below them.

The whole program, in JavaScript

One file, my-game.mjs. Save it in your app's folder, where the TypeScript client is installed (the account page):

// SPDX-License-Identifier: MIT OR Apache-2.0
//
// My Game, plugged into DSM, in one file: a connect code, a wallet that
// connects to it, a payment, gems handed over, and a sign-in. It talks to the
// account `make app-dev` runs.
//
//   npm install
//   node my-game.mjs

import { ask, grant, localAccount } from "dsm-app-client";

const app = await localAccount("app");

// The wallets connected before this run, so the one that connects now stands out.
const before = new Set((await app.connections()).map((c) => c.id));

// 1. The offer: what My Game may do without asking the player again.
const offer = await app.offer({
  name: "My Game",
  grants: [grant.send("ERA", "0.10", "1.00"), grant.owns(["ERA"]), grant.accept()],
});
console.log(`connect code: ${offer.code}`);
console.log("waiting for a wallet to connect to it...");

// 2. A wallet connects: scanned on a phone, or `make dev-wallet-connect CODE=...`.
let player;
for (;;) {
  player = (await app.connections()).find((c) => c.connected && !before.has(c.id));
  if (player !== undefined) break;
  await new Promise((resolve) => setTimeout(resolve, 1000));
}
console.log(`wallet ${player.peer} connected`);

// 3. 0.05 ERA, inside the grant, so the player is not asked again.
const paid = await app.wait(await app.request(player, ask.send("ERA", "0.05", "100 gems")));
console.log(`payment: outcome ${paid.outcome}, fact ${paid.fact}`);
if (paid.fact !== "received") process.exit(1);

// 4. Your own coin. Issuing it again with the same terms answers with the
//    same coin, so this runs again and again. The wallet accepts it, then
//    your account gives.
const gems = await app.issue({ symbol: "GEMS", name: "Gems", supply: "1000000" });
await app.wait(await app.request(player, ask.accept(gems)), { until: "answered" });
await app.give(player, gems, "100");
console.log(`gave 100 ${gems.symbol}`);

// 5. A sign-in. It always waits for the player to approve it on the device.
console.log("sign-in asked: approve it on the device");
const login = await app.wait(await app.request(player, ask.login("My Game")));
console.log(`sign-in: outcome ${login.outcome}, fact ${login.fact}`);
if (login.fact === "signed") console.log(`signed: ${login.signed.statement}`);

It is plain JavaScript, so node runs it with no build step, and it is valid TypeScript too. With dsm-app up running:

node my-game.mjs

(From the repo, it is in guide/examples/my-game, whose package.json depends on the client in the clone: npm install, then the same node my-game.mjs.)

It prints a connect code and waits. In a second terminal, connect a wallet to it: dsm-dev-wallet connect followed by the code, or scan it on a phone. (A wallet connects to an app once: if the terminal wallet is still connected from the earlier pages, dsm-dev-wallet disconnect $S first, or connect a second one with DSM_WALLET_NAME=wallet2 dsm-dev-wallet up, then DSM_WALLET_NAME=wallet2 dsm-dev-wallet connect <the code>.) When the program asks for the sign-in, approve it as on the last page: dsm-dev-wallet pending shows the connection and the request's number, and dsm-dev-wallet approve <connection> <number> approves it.

What it printed:

connect code: dsm:connect/v1:18B6GX3ME1SKMBSF64S3EBHG5RR2WC9T70T38CRJ41QXQJEH1V239MWMG2Q6RPQ5SJ4RD3XP78QK4KMD5G86Z5SCMB2MY6H0B03M69RKFDWBCDNTWZR7KEK2HJ0Y9JYVD945TPAJW7719A4HNYWG
waiting for a wallet to connect to it...
wallet EKJMVVEGD8KFQNP2DP8XJ0QFNSZTV5V7MHZKQZ0N7097WPG8ZPV0 connected
payment: outcome carried_out, fact received
gave 100 GEMS
sign-in asked: approve it on the device
sign-in: outcome carried_out, fact signed
signed: Sign in to My Game (a45c78b0-3774-466d-aee8-98cbdf2b9b49)

The wallet then owned the 100 GEMS:

ERA                 98.95   currency NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0
GEMS                  100   currency 4K53KPP5HCKG2NK0CRM8H888R3YYT7ACQ00BB7S6C5VVEQFNR670
dBTC           0.00000000   currency 0EJ2PSRS2YQR8BR77P3WZ92SV12VJT7XP6NWP0SH5P8MWD81C8H0

The same program, in Python

my_game.py. Save it in your app's folder too:

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
"""My Game, plugged into DSM, in one file: a connect code, a wallet that
connects to it, a payment, gems handed over, and a sign-in. It talks to the
account `make app-dev` runs.

    python3 guide/examples/my_game.py
"""

import sys
import time

from dsm_app_client import ask, grant, local_app


def main() -> int:
    app = local_app()

    # The wallets connected before this run, so the one that connects now stands out.
    before = {c.id for c in app.connections()}

    # 1. The offer: what My Game may do without asking the player again.
    offer = app.offer("My Game", [grant.send("ERA", "0.10", "1.00"), grant.owns(["ERA"]), grant.accept()])
    print(f"connect code: {offer.code}", flush=True)
    print("waiting for a wallet to connect to it...", flush=True)

    # 2. A wallet connects: scanned on a phone, or `make dev-wallet-connect CODE=...`.
    while True:
        new = [c for c in app.connections() if c.connected and c.id not in before]
        if new:
            player = new[0]
            break
        time.sleep(1)
    print(f"wallet {player.peer} connected", flush=True)

    # 3. 0.05 ERA, inside the grant, so the player is not asked again.
    paid = app.wait(app.request(player, ask.send("ERA", "0.05", "100 gems")))
    print(f"payment: outcome {paid.outcome}, fact {paid.fact}", flush=True)
    if paid.fact != "received":
        return 1

    # 4. Your own coin. Issuing it again with the same terms answers with the
    #    same coin, so this runs again and again. The wallet accepts it, then
    #    your account gives.
    gems = app.issue("GEMS", "Gems", "1000000")
    app.wait(app.request(player, ask.accept(gems)), until="answered")
    app.give(player, gems, "100")
    print(f"gave 100 {gems.symbol}", flush=True)

    # 5. A sign-in. It always waits for the player to approve it on the device.
    print("sign-in asked: approve it on the device", flush=True)
    login = app.wait(app.request(player, ask.login("My Game")))
    print(f"sign-in: outcome {login.outcome}, fact {login.fact}", flush=True)
    if login.fact == "signed" and login.signed is not None:
        print(f"signed: {login.signed[0]}", flush=True)
    return 0


if __name__ == "__main__":
    sys.exit(main())

With the Python client in the virtual environment from the account page:

. .venv/bin/activate
python3 my_game.py

Connect a wallet to the code it prints, and approve the sign-in, as above. What it printed:

connect code: dsm:connect/v1:18B6GX3ME1SKMBSF64S3EBHG5RR2WC9T70T38CRJ41QXQJEH1V239MWMG2Q6RPQ5SJ4RD3XP78QK4KMD5G86Z5SCMB2MY6H0V8KS5P8628JH2QR3BJ8Y97KTTFCQPFZ9NYH3AQN6PQZZP8XKEANG
waiting for a wallet to connect to it...
wallet HA62NJZ06ZY8C79HKM39AGTC3CMX9348RJH30GZTHA00BCSP0KV0 connected
payment: outcome carried_out, fact received
gave 100 GEMS
sign-in asked: approve it on the device
sign-in: outcome carried_out, fact signed
signed: Sign in to My Game (ef80f587-b7e4-4041-9088-d2b554da1071)

This run came after the JavaScript one, on the same account: issuing GEMS a second time answered with the same coin, and the account gave from what it still held.

Your account at a glance

The install put two more Python programs in ~/.local/share/dsm/examples: first_payment.py, a connect code and one payment received (the newcomer check, make first-run, runs it on a clean Ubuntu), and account_status.py:

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
"""Your application's account at a glance: its identity, what it owns, every
connected wallet, and the latest activity. Runs against `dsm-app up` (from the repo, `make app-dev`)."""

from dsm_app_client import local_app


def main() -> None:
    app = local_app()
    print(f"account {app.id()}")
    for o in app.owned():
        print(f"  {o.object.symbol:<8} {o.display:>16}   {o.object.kind}")
    connections = app.connections()
    print(f"{len(connections)} connection(s) with wallets")
    for c in connections:
        state = "connected" if c.connected else "disconnected"
        print(f"  connection {c.id}  wallet {c.peer}  {state}")
        for line in c.grants:
            print(f"    may: {line}")
    for e in app.ingress.activity(0).entries[-5:]:
        print(f"  {e.seq}  {e.name}  {e.summary}")


if __name__ == "__main__":
    main()
python3 ~/.local/share/dsm/examples/account_status.py

On the account from these pages, it began:

account 0RHS9YDRSPR9PG9WESBX7PMGAVMEJSSVSC11QSD4NG4JPKY3XQ80
  ERA                 60.60   currency
  GEMS               999800   currency
  GOLD            999000.00   currency
  SHIELD                  0   unique
  dBTC           0.00000000   currency
6 connection(s) with wallets
  connection 46E4R16XC06YC0YBK8H4P5EWCQ2J22Q4B1A4N16E4BA3J6WWN0Q0  wallet HA62NJZ06ZY8C79HKM39AGTC3CMX9348RJH30GZTHA00BCSP0KV0  disconnected
    may: Send this app: up to 0.10 ERA a time, 1.00 ERA in all
    may: Prove what you own of ERA and of objects this app issued
    may: Receive objects this app issues
    may: Lock stakes for matches: up to 1.00 ERA a time, 2.00 ERA in all

and went on with every other connection, and the latest activity.

Every piece, in either language

On the starter's page, the switch at the top shows every piece's code in Python instead of TypeScript. Here are the offer and the sign-in:

The starter's offer piece with its code in Python
The offer, in Python.
The starter's sign-in piece with its code in Python: a nonce, the request, the wait, and the check of the signed words
The sign-in, in Python: the code keeps the nonce and checks the signed words against login_statement.

In your own app

JavaScript or TypeScript. npm install the client's package (~/.local/share/dsm/clients/dsm-app-client-0.1.0-beta.4.tgz) into your app, as the account page did; its types are inside it, so nothing is compiled. localAccount("app") finds the account dsm-app up runs by looking for the .dsm-dev folder above the folder you run in. From anywhere else, name it: DSM_DEV_DIR=/path/to/your/app/.dsm-dev. On a server of your own, open the account from the ingress's address and the secret file the host writes: new AppAccount(await Ingress.fromSecretFile(url, secretFile)).

Python. pip install the client's wheel (~/.local/share/dsm/clients/dsm_app_client-0.1.0b4-py3-none-any.whl) into your app's virtual environment. local_app() finds the account the same way, and DSM_DEV_DIR names it from anywhere else. On a server of your own: AppAccount(Ingress.from_secret_file(url, secret_file)).

From a clone of the repo, the clients are clients/typescript (depend on it by path) and clients/python (pip install -e clients/python); both carry their generated types, so neither needs protoc.

Any other language

Generate types from proto/dsm_app.proto, and send an IngressRequest (a RouterInvokeOp or RouterQueryOp: the route's name and an ArgPack holding its request message) to POST /ingress. An IngressResponse comes back: either an error, or 0x03 followed by an Envelope. clients/typescript/src/ingress.ts is the whole exchange, in one short file. The routes are in the reference.

Objects

Everything an account can hold is an object: ERA, a sword, a ticket, a coin you issued. A currency is an object counted in units; an item is an object of which exactly one exists. You name an object by its symbol ("ERA", "GEMS") or by its id, and amounts in its own units ("0.10", "100"); your account converts them, in both directions, so no client carries the unit rule. issue makes a new object, and your account then holds its whole supply. Issued again with the same terms, it answers with the same object. give hands some to a wallet that has accepted it.

Next, when you want it: Shops, matches and contacts.

Shops, matches and contacts

Three things your app can do beyond payments and sign-ins: run a shop of its own, where wallets buy your coin; referee matches, where two wallets stake and your key decides; and read a wallet's contacts.

This page carries on from the last ones. Your app's account and the terminal wallet are running, and the wallet disconnected at the end of Ask the wallet for things. The output below is from one run on the beta network.

A shop of your own

your coin
reserve
→vault
your account's
→ERA
reserve

A shop is a vault your account opens between two objects, funded from what it holds: so much of your coin on one side, so much ERA on the other. Wallets trade against it through your account, and every trade moves the reserves. There is no order book, and no counterparty but the vault. The fee on each trade is yours to set; the command line's vaults charge 0.30%.

On the starter's page, issuing a coin and opening a shop are the first pieces, each with its code:

The starter's first pieces: what the account owns, Issue an object with its fields and code, and Open a shop with its fields and code
What your account owns, Issue and Open, and the code under each.

1. Issue a coin, open a vault

Your app issues GOLD, a coin with two decimal places and a million in supply. Your account holds all of it:

dsm-app issue GOLD 'Starter gold' 1000000 2
issued GOLD (currency, supply 1000000.00): 89S6HVVW03FRJZBNTR8M62ACZ56J0A09QJQ0ZCFDA3EHRB5KER1G

Then it opens a vault: a thousand GOLD on one side, ten ERA on the other.

dsm-app vault GOLD 1000 ERA 10
vault 41ab586aaf3258f93bda09966b67eaa235dc0df3aa131eb56ffb2529a7e52eee: 1000.00 GOLD, 10.00 ERA

vaults lists it, with its id and its reserves:

dsm-app vaults
vault 41ab586aaf3258f93bda09966b67eaa235dc0df3aa131eb56ffb2529a7e52eee  GOLD/ERA  1000.00 GOLD, 10.00 ERA  active

2. Connect with a trade grant

When offer names a coin after the caps, it also asks to trade: to sell the wallet that coin for ERA through your vault, up to 1.00 ERA a trade and 5.00 in all.

CODE=$(dsm-app offer 'My Game' ERA 0.10 1.00 GOLD)
dsm-dev-wallet connect "$CODE"
My Game (account 0RHS9YDRSPR9PG9WESBX7PMGAVMEJSSVSC11QSD4NG4JPKY3XQ80) asks to:
  - Send this app: up to 0.10 ERA a time, 1.00 ERA in all
  - Prove what you own of ERA and of objects this app issued
  - Receive objects this app issues
  - See which of your contacts are here: their DSM IDs, not their names
  - Lock stakes for matches: up to 1.00 ERA a time, 2.00 ERA in all
  - Trade between ERA and 89S6HVVW: up to 1.00 ERA a time, 5.00 ERA in all
  - Up to 64 requests that spend nothing; after them, each waits for you
approved:
connection 97MSN2P9S07FVAFFBPNGW2GRPBAJQM3113MJG03V60MTAP4HBWW0  My Game  CONNECTED
  grants: Send this app: up to 0.10 ERA a time, 1.00 ERA in all
  grants: Prove what you own of ERA and of objects this app issued
  grants: Receive objects this app issues
  grants: See which of your contacts are here: their DSM IDs, not their names
  grants: Lock stakes for matches: up to 1.00 ERA a time, 2.00 ERA in all
  grants: Trade between ERA and 89S6HVVW: up to 1.00 ERA a time, 5.00 ERA in all
  grants: 0 of 64 requests that spend nothing used

The wallet names your coin by the start of its id, 89S6HVVW. The new connection's id goes in $S again:

S=$(dsm-app connections | awk -v w="$(dsm-dev-wallet id)" '$4==w && $NF=="CONNECTED"{print $2}')

3. Price

price asks what half an ERA would get in GOLD through your vaults:

dsm-app price $S ERA GOLD 0.50
request 1
dsm-app result $S 1
answered: yes  outcome: carried_out
fact: none  a price is information and carries no authority
  price: 50 in, 4748 out (smallest units, 1 hop)

Half an ERA would get 47.48 GOLD. A price is information, never a fact: it can move before the trade.

4. Trade

dsm-app trade $S ERA GOLD 0.50 95

trade makes three requests in a row, and prints each step. The wallet accepts GOLD first, because a wallet receives only what it has taken up itself. Then your account prices the trade. Then it asks the wallet to trade half an ERA for no less than 95% of that price:

priced: 0.50 ERA gets 4748 of GOLD (smallest units); asking for at least 4510
request 4

About ten seconds later:

dsm-app result $S 4
answered: yes  outcome: carried_out
fact: none  a trade shows through this account's own vault (sofi.vaults) or a later proof of what it owns; the answer is a notification

The wallet's answer is a notification. The evidence of a trade is in your own vault, and in what the wallet owns:

dsm-dev-wallet owned
ERA                 99.25   currency NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0
GOLD                47.48   currency 89S6HVVW03FRJZBNTR8M62ACZ56J0A09QJQ0ZCFDA3EHRB5KER1G
SHIELD                  1   unique   C46M962T804B5XND8P2ASG4QK2VCB7SCGE5RNWRA4ST4112EK400
dBTC           0.00000000   currency 0EJ2PSRS2YQR8BR77P3WZ92SV12VJT7XP6NWP0SH5P8MWD81C8H0
dsm-app vaults
vault 41ab586aaf3258f93bda09966b67eaa235dc0df3aa131eb56ffb2529a7e52eee  GOLD/ERA  952.52 GOLD, 10.50 ERA  active

The wallet bought 47.48 GOLD for 0.50 ERA, and the vault's reserves moved: a little more ERA, a little less GOLD.

A match your app referees

side A
stakes
→your key
decides A, B or void
→side B
stakes against A

A match is two stakes that share one verdict. Side A stakes; side B stakes against side A's stake, so the two are tied. Your account's key decides: A, B, or void. The winner's wallet collects. Each wallet builds its stake's terms itself, from the match you describe: your request names no branch, no signer and no recipient, so your app cannot send the money anywhere but where the rules say.

5. A second player

A match needs two wallets. Start a second one, funded from the faucet like the first, and connect it:

DSM_WALLET_NAME=wallet2 dsm-dev-wallet up
CODE2=$(dsm-app offer 'My Game' ERA 0.10 1.00 GOLD)
DSM_WALLET_NAME=wallet2 dsm-dev-wallet connect $CODE2

It prints the same list as the first wallet, and its own connection. Its id goes in $S2:

S2=$(dsm-app connections | awk -v w="$(DSM_WALLET_NAME=wallet2 dsm-dev-wallet id)" '$4==w && $NF=="CONNECTED"{print $2}')

6. Both stake

Side A stakes one ERA on "match 1", against the second wallet:

dsm-app stake $S $S2 'match 1' ERA 1.00 A
request 5

On the first wallet's connection this is request 5: the price and the trade's three came first.

dsm-app result $S 5
answered: yes  outcome: carried_out
fact: staked  the wallet's vault MEJC8VVA holds the stake under the match's terms, Active on its verdict cell
  stake a3a4c46f6a3bc93ea5a37fde3ace814ac482a4eb33da21ce420defa953680930
  verdict cell 4348b438633afdcbdeddb108e1b2be177d35e1886189863f9e939e6d42130970  amount 100

The fact is staked, and the result prints the stake's id. Keep it:

A=$(dsm-app result $S 5 | awk '/^  stake /{print $2; exit}')

Side B stakes the same, passing side A's stake as the one it stakes against, so both share the verdict:

dsm-app stake $S2 $S 'match 1' ERA 1.00 B $A
request 1
dsm-app result $S2 1
answered: yes  outcome: carried_out
fact: staked  the wallet's vault JR65R02N holds the stake under the match's terms, Active on its verdict cell
  stake 960c5c0055233b430a8e5a92a8a0244c022ecd2b3b36f0755b6d592c05dc28c5
  verdict cell 4348b438633afdcbdeddb108e1b2be177d35e1886189863f9e939e6d42130970  amount 100

The two stakes name the same verdict cell. Keep the second id too:

B=$(dsm-app result $S2 1 | awk '/^  stake /{print $2; exit}')

Each wallet's cap, one ERA a match and two in all, is what its player agreed to.

7. Decide and collect

Your app decides for A:

dsm-app decide $A A
decided: a-wins  state 4

Then it asks side A's wallet to collect both stakes:

dsm-app collect $S $A $B
request 6

Until the wallet has collected, the result says so:

answered: no  outcome: unanswered
fact: none  vault MEJC8VVA is not released yet; an answer saying collected grants nothing

About fifteen seconds later:

dsm-app result $S 6
answered: yes  outcome: carried_out
fact: collected  each vault is Retired and its cell's final verdict (a-wins, a-wins) pays the wallet
  stake a3a4c46f6a3bc93ea5a37fde3ace814ac482a4eb33da21ce420defa953680930
  stake 960c5c0055233b430a8e5a92a8a0244c022ecd2b3b36f0755b6d592c05dc28c5
  verdict cell 4348b438633afdcbdeddb108e1b2be177d35e1886189863f9e939e6d42130970  amount 0
dsm-dev-wallet owned
ERA                100.25   currency NNG176RZ6ACTWCDPRNYHXZK2DCZ72SPA9Q6XWGRGQ9JGKZYTESG0
GOLD                47.48   currency 89S6HVVW03FRJZBNTR8M62ACZ56J0A09QJQ0ZCFDA3EHRB5KER1G
SHIELD                  1   unique   C46M962T804B5XND8P2ASG4QK2VCB7SCGE5RNWRA4ST4112EK400
dBTC           0.00000000   currency 0EJ2PSRS2YQR8BR77P3WZ92SV12VJT7XP6NWP0SH5P8MWD81C8H0

The winner's wallet owns 100.25 ERA: its own stake back, and the other's. Each stake sat in a vault of its own wallet, under the match's terms. The match cost your account nothing, and your key only chose the branch.

Contacts

contacts asks the wallet who its contacts are. The player granted it when connecting, and your app gets their DSM identities and nothing more: no names, no numbers, no photos.

dsm-app contacts $S
request 7
dsm-app result $S 7
answered: yes  outcome: carried_out
fact: none  contacts are the wallet's word: who its owner keeps, never proof of anything

Duels

A duel is a match that a program decides instead of your key, from both players' signed moves, with no referee. The client's ask.duel helpers carry the setup, the readies, the signed moves and the settlement; the setup and the moves are built with the program's own library (for the program in this repo, crates/wildstate-duel-wasm). This guide does not walk through one.

The whole page, as one script

With dsm-app up running and the terminal wallet disconnected, this runs every step above, waiting for each request to settle before reading it. Each run opens another vault from your account's ERA.

dsm-app issue GOLD 'Starter gold' 1000000 2
dsm-app vault GOLD 1000 ERA 10
dsm-app vaults
CODE=$(dsm-app offer 'My Game' ERA 0.10 1.00 GOLD)
dsm-dev-wallet connect "$CODE"
S=$(dsm-app connections | awk -v w="$(dsm-dev-wallet id)" '$4==w && $NF=="CONNECTED"{print $2}')
dsm-app price $S ERA GOLD 0.50
until dsm-app result $S 1 | grep -q "^answered: yes"; do sleep 3; done
dsm-app result $S 1
dsm-app trade $S ERA GOLD 0.50 95
until dsm-app result $S 4 | grep -q "^answered: yes"; do sleep 3; done
dsm-app result $S 4
dsm-dev-wallet owned
dsm-app vaults
DSM_WALLET_NAME=wallet2 dsm-dev-wallet up
CODE2=$(dsm-app offer 'My Game' ERA 0.10 1.00 GOLD)
DSM_WALLET_NAME=wallet2 dsm-dev-wallet connect $CODE2
S2=$(dsm-app connections | awk -v w="$(DSM_WALLET_NAME=wallet2 dsm-dev-wallet id)" '$4==w && $NF=="CONNECTED"{print $2}')
dsm-app stake $S $S2 'match 1' ERA 1.00 A
until dsm-app result $S 5 | grep -q "^fact: staked"; do sleep 3; done
dsm-app result $S 5
A=$(dsm-app result $S 5 | awk '/^  stake /{print $2; exit}')
dsm-app stake $S2 $S 'match 1' ERA 1.00 B $A
until dsm-app result $S2 1 | grep -q "^fact: staked"; do sleep 3; done
dsm-app result $S2 1
B=$(dsm-app result $S2 1 | awk '/^  stake /{print $2; exit}')
dsm-app decide $A A
dsm-app collect $S $A $B
until dsm-app result $S 6 | grep -q "^fact: collected"; do sleep 3; done
dsm-app result $S 6
dsm-dev-wallet owned
dsm-app contacts $S
until dsm-app result $S 7 | grep -q "^answered: yes"; do sleep 3; done
dsm-app result $S 7

Next: Run your app's account in production.

Run your app's account in production

dsm-app up runs your app's account for you while you develop. In production you run the same program, dsm-app-host, yourself, beside your server.

Get it

The install from Set up your machine put it in ~/.local/bin. On a server, install it where your service manager runs it from; DSM_PREFIX names the place (/usr/local/bin/dsm-app-host here):

curl -fsSLO https://github.com/deterministicstatemachine/dsm/releases/download/v0.1.0-beta.4/install.sh
sudo DSM_PREFIX=/usr/local sh install.sh

From the repo instead, build it:

cargo build --release --locked -p dsm-app-host

The program is then target/release/dsm-app-host.

Run it

The command, with your own paths and names in place of myapp's:

dsm-app-host \
  --data-dir /var/lib/myapp/dsm \
  --ingress-secret-dir /run/myapp/dsm-ingress \
  --env-config /etc/myapp/dsm_env_config.toml \
  --game-bind 127.0.0.1:8787 \
  --relay-bind 0.0.0.0:8443 \
  --relay-endpoint https://dsm.myapp.example:8443 \
  --relay-name dsm.myapp.example
FlagWhat it is
--data-dirThe account: its state and RECOVERY_MNEMONIC. The first start creates the account and writes the mnemonic there (mode 0600).
--ingress-secret-dirWhere each launch writes a new ingress secret, ingress-token. It must be apart from the data directory: your app gets the secret and nothing of the account.
--env-configThe network's storage set, each member with its own certificate authority. The wallet's own is dsm_client/frontend/public/dsm_env_config.toml. For the beta network, --network beta instead: the host carries that file and uses it, as dsm-app up does.
--game-bindThe ingress your server calls. Loopback only; anything else is refused.
--relay-bind, --relay-endpoint, --relay-nameWhere wallets reach your account. The host makes its own TLS certificate for these names, and every connect code pins it, so you need no certificate authority.

The first lines it logs name the relay and its certificate pin, the ingress, and where this launch's secret is. Your server opens the account from the ingress's address and that secret file: new AppAccount(await Ingress.fromSecretFile(url, secretFile)) in TypeScript, AppAccount(Ingress.from_secret_file(url, secret_file)) in Python.

What protects it, and what does not

  • The ingress listens on loopback only. Every call needs the launch's secret as Authorization: Bearer <secret>, must name the ingress as its Host, and may come from no other web origin. A web page on the same machine can neither read the secret nor send it.
  • Wallets reach the relay over TLS pinned by the connect code. The relay carries messages; nothing it carries is evidence.
  • A grant caps what your app can take from a wallet, per request and in total. Anything outside it waits for the player, on their device.

Caution

Your account is a hot wallet. RECOVERY_MNEMONIC is a file in the data directory, and the host reads it on every start. Whoever can read it, or holds root on the machine, controls the account. Anything holding the ingress secret can move everything the account holds; the host has no spending policy of its own. Keep on it only what it needs to work with, sweep the rest to an account you keep elsewhere, back up the mnemonic offline, and lock the machine down like any server that holds money.

Next, if you want your own network: A storage set of your own.

A storage set of your own

Everything in this guide runs on the beta network, which is already there. If you would rather depend on no hosted node, run the network's storage set yourself, on your machine: the network's five pinned members, dsm-node-1 to dsm-node-5, as five local nodes. Each is the production storage node program, on its own PostgreSQL database, serving TLS from a certificate authority of the set's own. Nothing then reaches outside your machine.

1. PostgreSQL

The set keeps each node's data on a PostgreSQL server on this machine, reached as your own login.

macOS. Homebrew's server already has a role named after your login, and takes it on localhost:

brew install postgresql@17 && brew services start postgresql@17

Linux. The server takes your login over its socket once it has a role of that name:

sudo apt install -y postgresql && sudo service postgresql start
sudo -u postgres createuser -s "$(id -un)"

PostgreSQL must run with fsync=on and full_page_writes=on, which are the defaults.

2. Your app's account on your set

An account exists only on the set it was made on. If you made accounts on the beta network with the earlier pages, forget them first:

make dev-reset

Then start the set and your app's account on it:

make app-dev-local

make dev-wallet, make dev-wallet2 and the rest follow your account to the set it is on.

3. The nodes

make app-dev-local starts the nodes. These manage them:

make nodes-status
make nodes-down
make nodes-up

nodes-status checks every node's health over verified TLS. nodes-down stops the nodes, and nodes-up prepares the set the first time and starts every node, on ports 8080 to 8084. make nodes-reset stops them, drops their databases, and forgets the set and the accounts made on it.

Everything the set needs lives under .dsm-dev/net/ in the repo, including the client config wallets use, dsm_env_config.toml, which trusts the set's dev-ca.pem.

4. The phones

Point the wallet on your phones at your set: every adb device (through adb reverse) and every booted iOS Simulator. Debug builds only:

make phones-local

A wallet that already made its identity on another set keeps it there, so clear the app's data first. On Android, adb shell pm clear com.dsm.wallet, then grant its Bluetooth permissions again; on iOS, delete the app and run make ios-simulator-run. The same holds after make nodes-reset: it makes a new set with a new certificate authority, so clear the wallets and run make phones-local again, or they refuse every node (invalid peer certificate).

Back to the beta network

make dev-reset

This forgets the accounts on this machine, and your local set if they were on it. The next make app-dev makes new ones on the beta network.

Next: Grants, commands and routes.

Apps plugged into DSM

These apps are plugged into DSM today. Each kept its own server and its own logic, added a DSM account of its own, and lets its users connect their DSM wallets to it. Their code is public, in Irrefutable Labs' repositories.

AppWhat it isWhat DSM does in it
WildstateA creature-collecting game for AndroidThe coin and every creature are objects in the player's own wallet; the shop, the market and staked matches run through it
OpenBoxes for DSMThe open-source warehouse and inventory systemCustody: a stock change stands only when DSM recorded it; staff sign in with their wallet; a carrier takes custody on a phone
CBFpy for DSMA prototype AI with a bodyEvery step it takes is a DSM transition, decided by rules, and anyone can audit the chain from public keys

Wildstate

Wildstate is a creature-collecting game for Android: walk the camp, catch creatures in the tall grass, battle trainers, buy capsules, trade at the market, and fight other players in the arena. Walking, encounters, battles and dialogue run on the game's own server and never touch DSM. What a player owns lives in their DSM wallet.

Wildstate's connect screen: OPEN DSM WALLET, a QR code, and the list of what the game asks the wallet for
The game shows its code and lists what it asks the wallet for.
The camp after connecting: the bar shows 4 capsules and 70 WILD
The camp. The 70 WILD on the bar is what the wallet proved it owns.
A wild Mossling at 12 of 40 HP, caught: Mossling joined your collection
A Mossling caught. The game's account issued it as an object, one of one, and gave it to the wallet.
Bramble's trading post: a capture capsule for 3 WILD, and your wallet sent 3 WILD
A capsule, granted once the wallet's 3 WILD arrived.
The market: the game's 5 vaults, what the wallet proved it owns, and a trade done
A trade at the market: 1.00 ERA for 49 WILD, through the game's vaults.

In the game:

  • WILD, the game's coin, is an object the game's account issued, with a fixed supply. The game pays players in it: a starting amount when they connect, and a bounty for each trainer beaten.
  • Every creature is an object of which exactly one exists, issued by the game's account at the capture. It names the game as its issuer, its species and its capture number; its HP, XP and charges are game data kept against it.
  • The shop is paid through the wallet. A capsule is granted only when the wallet's 3 WILD has arrived on the game's own account; a message saying "paid" grants nothing.
  • Coins and the roster come from proofs of what the wallet owns, which the game's account checks itself.
  • The market is a WILD/ERA vault the game's account funds, and players trade with it from inside the game.
  • The arena's staked matches are decided by a pinned program, not by a referee. Each wallet locks its own stake, and the winner takes both. The program is wildstate-duel, in this repository.
  • Pressing DSM in the game shows one timeline: what ran on the game server, and every call the game's account made on DSM.
The DSM overlay over the camp: for request 20, what DSM established, with the account's position, economic root and device tree
Press DSM: for a payment (request #20), what the game's own account established, with its position, economic root and device tree.

The game connects the way Make an offer, connect a wallet does: it shows a code, and the player opens it in their wallet and approves what the game asks for. Its integration is described in docs/dsm-integration.md; the game is Irrefutable-Labs/wildstate-dsm.

These screenshots are from a run on 2026-11-11 with the game on DSM's current code, a new game account on the beta network, and DSM's terminal wallet as the player. The wallet connected, got its starting coin, bought capsules, was given the creature it caught, proved what it owns, and traded at the market. That update is pull request #23.

OpenBoxes for DSM

OpenBoxes is an open-source warehouse and inventory system, first written to track emergency medical supplies in Haiti after the 2010 earthquake and now run by healthcare facilities around the world. OpenBoxes for DSM keeps OpenBoxes' books, and gives DSM the authority over custody at a DSM warehouse.

OpenBoxes sign-in: a username, Ask my phone, and the code the wallet must show
No password. The person approves the sign-in in their wallet, which must show this browser's code.
The door screen: shipment 938PDP, 120 units of amoxicillin, with the carrier
At the door: 120 boxes with the carrier, who holds 120 custody units in their wallet.
Lakeside's receive screen: 130 counted, and the refusal: DSM moved 120
Lakeside counts 130 and is refused: DSM moved 120, and the books cannot take in what the account has no custody of. It received the 120.

In the app:

  • Each warehouse is a DSM account. A stock change there (a receipt, a transfer, a shipment) stands only when DSM has recorded the matching transition. Otherwise the write fails.
  • The books follow DSM. A reconciliation reads DSM's state, finds any gap, and corrects it with a new entry, never by editing history.
  • Sign-in is the wallet's. A person signs in by approving a request in their wallet. Once a wallet is bound, passwords stop.
  • Seats are held in vaults, assigned and returned on DSM, and a revoked seat stays revoked.
  • The carrier leg. Handing a shipment to a carrier sends the organization's custody units to the carrier's wallet. At the destination's door the carrier hands them over on a phone screen, the destination takes them in, and receiving is admitted only up to what DSM moved.
Levels of access and seats: the warehouse manager level is an object with a cap of 5, and seat 1 is locked in a vault naming its holder
Seats. The warehouse manager level is an object with a cap of 5; seat #1 is locked in a vault naming its holder, and only the owner's wallet can revoke it. On the right, each DSM record the organization's account signed.
The authority record and custody on record: each entry signed by the organization's account; Lakeside proved it holds 120 of the shipment
The authority record, each entry signed by the organization's account and read back from the storage nodes. Below it, custody on record: Northgate asked Lakeside's account to prove what it holds of the shipment, and got 120, with no access to Lakeside's systems.
DSM and the books: a gap of 40 found by the reconciler and corrected by the owner, both findings signed
DSM and the books. A hand edit to the database added 40 boxes; the reconciler found the gap against DSM and signed it, and the owner corrected it forward with a new entry. Both findings stay.

It is in review as pull request #1 on Irrefutable-Labs/openboxes-dsm. On 2026-11-11 its whole demo ran on DSM's current code, on the beta network, with terminal wallets standing in for the phones: all 13 steps passed, from the owner's claim and the sign-ins to the seat revoked, the carrier's hand-over, the count refused, the proof of 120 and the gap corrected. Its 253 unit tests and 26 integration tests pass. These screenshots are from that run.

CBFpy for DSM

CBFpy is Stanford's library for control barrier functions: the mathematics that keeps a robot's controls inside a safe set. Irrefutable Labs' fork adds a prototype on DSM: an AI with a body proposes what to do next, and deterministic rules decide which proposal may become a real step on a DSM chain. No AI judges anything.

The Ethical Autonomy Console after a run: what the AI knows, the audit, the chain, and every candidate it put forward with the reason each was refused
The console after a run: what the AI knows, the audit, the chain, and every candidate it put forward, each refused one with its reason.

How DSM carries it:

  • Every proposal is committed. Each candidate the AI puts forward is committed with its outcome under one fork root, refused ones included, so the chain holds everything it proposed, not only what it did.
  • A step is a DSM transition. The enforcer, the only program holding the device key, checks a candidate from what the step before it already knew, commits the chosen one, stores it, and only then carries out the act and signs a receipt for it.
  • What the senses report is committed too. A look's report is its own signed link and must answer exactly the look its parent asked for. Anything the AI could reach that it has not seen recently counts as occupied.
  • Anyone can audit it. ethical-audit checks the stored chain link by link, each from its parent alone, replays it through DSM, checks every act's receipt, and checks that the world holds exactly what committed acts wrote. It needs public keys only.

The rules it enforces are set out in its README: it never aims at harm to any animal bigger than a baby mouse as an adult, or to any honeybee; it never knowingly causes harm; what it has not seen counts as present; it drives no faster than it could stop on road it has seen; a conflict ends in a no, with the reason; and if its senses fail their check, every motor act stops.

What it knows: the AI's body beside a wall, cells it has seen, cells it never saw, and a dotted circle behind the wall
What it knows: its body beside a wall, and the cells behind it it never saw, which count as occupied. With show what was really there on, the dotted circle is who the simulated world put behind the wall. It never broke through.
The audit: charter, chain, receipts, considered forks and world, each checked
The audit, from public material alone: the charter, the chain, every receipt, every replaced fork, and the world.
Candidates the AI put forward and the rules refused, each with its purpose, its claims and the reason it was refused
Candidates it put forward that the rules refused, each with what it declared and the reason.

These come from a run of the prototype as its README describes it, on its main branch on 2026-11-11: the recorded run of the real model, replayed on the wall world, then the audit. The replay ended:

sequence 20
committed steps: 12
every committed step verified from its parent

and the audit printed:

10 superseded forks verified from their parents, holding 30 more candidates, 30 of them refused or unmeasured
every step's act has a receipt signed by the device
the world holds exactly the 12 files committed actions wrote, and nothing else
charter in force: version 1

It enforces the rules and the facts it is given, in a simulated world. It does not understand ethics, and an act that passes is not made good by passing. Its report says what is and is not shown; the code is in Irrefutable-Labs/cbfpy-dsm.

Grants, commands and routes

Grants

What an offer asks the player to let your app do without asking again:

grant.Lets your appAsked with
send(object, perRequest, total)be sent an object, within caps per request and in totalask.send
owns(objects)be shown a proof of what the wallet owns of these objects, and of any you issuedask.owns
accept()hand the player objects your account issuedask.accept, then give
contacts()read the DSM identities of the wallet's contacts, and nothing moreask.contacts
trade(a, b, caps)trade between two objects through your vaults, within capsask.price, ask.trade; a wallet receives only an object it added, so ask.accept your coin first
match(caps)take stakes in matches your app referees, within capsask.match.stake, decide, ask.match.collect
duel(programs, caps)take stakes in matches a pinned program decidesask.duel.*

ask.login and ask.sign are never in a grant: each waits for the player on the device. Requests that move nothing run without asking, up to the offer's freeRequests (64 unless the offer says otherwise); past that, each waits for the player. A payment inside the grant's caps keeps running past that count until one fails; from then on, every request waits for the player.

The wallet answers a connection's requests in order: a request that waits for the player holds up every later one on that connection. On the beta network a request settles in a few seconds to about half a minute; wait (in the clients) polls until it has.

Outcomes and facts

Outcome, the wallet's word
carried_outthe wallet did it
awaiting_approvalit waits for the player
declinedthe player said no
failedit could not be done
Fact, your account's evidence
receivedyour account took up a transfer the wallet sent
ownsa proof of what the wallet owns verified
signeda signature over the asked words checked
stakedthe wallet's stake is held under the match's terms
collectedthe match's verdict paid the wallet
nonenothing to establish: a price, a trade's notification, contacts

The app's command line

dsm-app <command> drives your app's account; dsm-app help lists the commands. From the repo, make app ARGS="<command>" runs the same. Amounts are in the object's own units.

CommandWhat it does
upstart your app's account on the beta network (a new one the first time, in .dsm-dev in this folder) and fund it; it keeps running in the background
downstop it
statuswhether it is running, and where
idthis account's identity
fundERA from the faucet, unless the account holds some
ownedwhat it owns
issue <SYMBOL> <name> <supply> [decimals]issue a new object; this account holds the supply
give <wallet> <object> <amount>give a connected wallet some of an object
offer <name> <object> <per-request> <total> [coin]a connect code: be sent <object> within those caps, be shown what the wallet owns, hand it objects this account issues, read its contacts, referee matches it stakes <object> in (1.00 a match, 2.00 in all); with [coin], sell it <coin> for <object> through this account's vault (1.00 a trade, 5.00 in all)
connectionsevery connected wallet
send <connection> <object> <amount>ask a wallet to send this account some of an object
owns <connection> <object>ask a wallet to prove what it owns of an object
accept <connection> <object>ask a wallet to accept an object this account issued
login <connection>ask a wallet to sign in (the player approves on the device)
contacts <connection>ask a wallet who its contacts are
vault <a> <reserve-a> <b> <reserve-b>open a vault that trades between two objects, funded from what this account owns (the fee is 0.30%)
vaultsthis account's vaults and their reserves
price <connection> <from> <to> <amount>ask what <amount> of <from> would get in <to> through this account's vaults (information, never a fact)
trade <connection> <from> <to> <amount> [percent]the wallet accepts <to>, this account prices the trade, then asks for it at no less than [percent] (95) of the price
stake <connection> <opponent> <match> <object> <amount> <A|B> [against]ask a wallet to stake on a match this account referees, against the wallet of <opponent> (its connection); side B passes side A's stake id as [against]
decide <stake> <A|B|void>decide a match: who the stakes pay
collect <connection> <stake>...ask the winner's wallet to collect the stakes
result <connection> <seq>what became of a request
activity [after]what calls and wallets did to this account

The terminal wallets

dsm-dev-wallet <command> drives a player's wallet in the terminal: up, down, status, connect, connections, pending, approve, decline, log, disconnect, owned. With DSM_WALLET_NAME=wallet2 in front, it drives a second one, for matches. dsm-dev-wallet help lists them. From the repo, make wallet ARGS="..." and make wallet2 ARGS="..." run the same.

What the install puts on your machine

What it is
dsm-app-hostyour app's own DSM account; --help lists its flags, --network beta joins the beta network
dsm-app, dsm-dev-walletthe commands above
dsm-connect-starterthe starter app, on http://127.0.0.1:3000; run it in your app's folder
~/.local/share/dsm/clients/the TypeScript client's package (npm install it) and the Python client's wheel (pip install it)
~/.local/share/dsm/examples/first_payment.py and account_status.py

install.sh checks every download against the release's SHA256SUMS before it unpacks anything. DSM_PREFIX installs somewhere other than ~/.local.

The make targets, from the repo

TargetWhat it does
make doctorcheck the machine's prerequisites, changing nothing
make clientsbuild the TypeScript client (both clients' protobuf types are committed)
make client-typesregenerate both clients' protobuf types after a change to proto/dsm_app.proto
make app-devyour app's own DSM account on the beta network, funded
make app-dev-localthe same, on a storage set of your own
make app-dev-downstop your app's account
make starterthe starter app, on http://127.0.0.1:3000
make dev-wallet, make dev-wallet2a player's wallet in the terminal, and a second one for matches, funded
make dev-wallet-connect CODE=...connect the terminal wallet to an app
make dev-wallet-pendingrequests waiting for the terminal wallet's approval
make phones-connect CODE=...open a connect code in the wallet on every phone
make phones-localpoint the phones' wallets at your local set
make dev-downstop the terminal wallets, your app's account and the local nodes
make dev-resetforget the accounts made on this machine, and your local set if they were on it
make developer-paththe whole developer path end to end, as CI runs it
make first-runa newcomer's first run on a clean Ubuntu with no clone and no Rust: the release's files installed with install.sh, the docs alone, to a payment received

Routes

The routes your app's account uses:

RouteKindRequestAnswerClient
connect.app.offerinvokeConnectAppOfferRequestV1ConnectAppOfferV1offer
connect.app.sessionsquerynoneConnectSessionsV1connections
connect.app.requestinvokeConnectAppRequestIntentV1ConnectRequestRefV1request
connect.app.statusinvokeConnectRequestRefV1ConnectAppStatusV1result, wait
balance.listquerynoneBalancesListResponseowned, owns
wallet.amountqueryWalletAmountRequestWalletAmountResponseamounts, both ways
token.createinvokeTokenCreateRequestTokenCreateResponseissue
wallet.sendSmartinvokeOnlineTransferSmartRequestOnlineTransferResponsegive
sofi.createVault, sofi.vaults, sofi.closeinvokeSofiCreateVaultRequest, none, SofiCloseRequestSofiVaultCreatedResponse, SofiVaultsResponseopenVault, vaults, closeVault
escrow.adjudicateinvokeEscrowOutcomeRequestEscrowVerdictResponsedecide
faucet.claiminvokeFaucetClaimRequestFaucetClaimResponsefund

And a wallet's: connect.preview (query), connect.approve, connect.sync, connect.list (query), connect.pending (query), connect.respond, connect.log (query), connect.disconnect.

GET /activity/{after} returns AppHostActivityV1: your account's identity, and a record of each call and each wallet's visit to the relay. Every plain result keeps the wire's message as raw, and pb exports every generated message, for anything the plain fields leave out.

Offline

Your app's own calls are all online. Offline transfers are the wallet's feature, between two wallets: a bearer transfer carries any object whose policy lets it move, from one wallet's offline account to another's. Today that needs a hardware board on both sides, it is available on Android (the iOS app has no board link yet, and refuses), and a trade offline is two one-way transfers.

More