Start here
Quickstart
Build the node and the CLI from the Rust workspace, start a node, read its chain, then make a key, register its identity, send a transfer and register a name, all from the terminal.
What this page does
You build two binaries from the workspace, start a node on your own machine, and use
uninet-cli against it: first to read what the node computed, then to act with a key of your
own. Everything below is reproducible from source alone, and every block of output on this
page was produced by running the command above it. Hashes are abbreviated to fit.
You are not connecting to a network someone else operates. No public network is hosted yet; the last section says what that leaves out.
uninet-cli is, before anything else, a UNP tunnel client: a node's /api/* routes are not
served over plain HTTP. The external router binds only /unp/* (the three bootstrap routes
the CLI uses, /unp/resolve, /unp/handshake and /unp, plus the relay, onion and
domain-gateway routes), and its fallback answers every other path with
400 UNP_TUNNEL_REQUIRED. Reaching an API route therefore means completing a bootstrap, a
key exchange and a sealed request, which is what the CLI does on every invocation.
Build
The workspace builds both binaries. The node's package is uninet-node-bin and its binary is
uninet-node; the CLI's package and binary are both uninet-cli.
cargo build --release -p uninet-node-bin -p uninet-cli
The commands below are written as uninet-node and uninet-cli; both are in
./target/release/.
Start a node
uninet-node --port 3001 --data-dir ./data
--port defaults to 3001, --data-dir to ./data, and --log-level to info. A node
started this way runs single-node: transactions apply locally with no gossip, because
--enable-p2p is off by default. The chain state trie is disk-backed under
{data_dir}/state, block history under {data_dir}/blocks, both encrypted at rest, so a
restart restores the same chain and does not re-run genesis.
What happens before your first command
uninet-cli uninet-node :3001
│ │
│ GET /unp/resolve?addr=unp://self.node/ │
│ ────────────────────────────────────────────▶ │
│ ◀──────── Ed25519 pubkey, X25519 pubkey, │
│ nonce, timestamp, signature │
│ verify the signature over all of them │
│ │
│ POST /unp/handshake (ephemeral X25519) │
│ ────────────────────────────────────────────▶ │
│ ◀──────── session id, server nonce, │
│ signature over the exchange │
│ key = BLAKE3-derive-key( │
│ "uninet unp session v1", │
│ ecdh ‖ client_nonce ‖ server_nonce) │
│ │
│ POST /unp session_id ‖ AES-256-GCM(request)│
│ ────────────────────────────────────────────▶ │
│ ◀──────── counter ‖ sealed signed envelope │
│ verify envelope against the pinned key │
The GCM nonce is a per-direction counter, so requests go one at a time. A response envelope whose Ed25519 signature does not verify against the key pinned at resolve is discarded and never printed.
--node accepts http:// only. An https:// URL is refused with an explanation:
confidentiality comes from the tunnel, not from TLS on the bootstrap listener.
Read the chain
$ uninet-cli status
block_time_ms: 2000
chain_id: 5a69…463f
child_chains: {}
height: 0
max_block_size: 4194304
max_txs_per_block: 1000
max_validators: 1
multi_node: false
node_id: 3e83…1c41
state_root: 68f4…b558
tip_applied_at_ms: -
tip_hash: 5c7b…2ed7
validator_count: 1
A node you just started is at height 0, and it stays there until something writes: in
single-node mode the node builds and applies one block per transaction, so blocks are never
produced on a timer and never empty after genesis. A field the node did not send prints as
-, never as a plausible zero.
chain_id is not per-install: the primary chain is built from the default configuration, so
it is blake3("default-chain") on every node started without a --config that overrides it.
multi_node is false and max_validators is 1 on a standalone node.
$ uninet-cli block latest
hash: 5c7b…2ed7
height: 0
parent_hash: 0000…0000
proposer: 3e83…1c41
state_root: 7270…e3d8
timestamp: 1790864535
tx_count: 0
That is the genesis block: height 0, an all-zero parent hash, no transactions, and this node
as proposer because it is the only entry in the validator set. Its state_root is the genesis
root, which is not the one status prints: the node writes further elements into the trie on
first boot, after genesis has been committed.
$ uninet-cli chains
CHAIN ID NAME TYPE HEIGHT VALIDATORS CHILDREN ACTIVE
----------------- ------- -------- ------ ---------- -------- ------
cc1fb8752157c833… Nexus Nexus 0 1 3 true
5a69aa3761cd33cf… default Personal 0 1 0 true
/api/chains always returns exactly two entries, the Nexus root and this node's own chain,
however many chains the hierarchy holds. raw reaches anything else: uninet-cli raw GET /api/overview prints a route no subcommand covers, with the HTTP status above the body.
Make a key
$ export UNINET_PASSWORD='correct horse battery staple'
$ uninet-cli key new Alice
key file uninet-key.json
username alice
public key 8919…7817
identity ae68…b400 (also its wallet address)
Keep the file and the password: nothing else recovers this key.
The commands on this page were run with the password in UNINET_PASSWORD, which is how a
script supplies it, and the signing ones with --yes. Without them the CLI asks for the
password (twice for a new key) and asks before it signs.
No node was asked anything. The key is Ed25519, the identity id is the BLAKE3 hash of the public key, and that id is also the address of the identity's wallet. The username is stored as a node stores it (trimmed, lower case).
The key file holds the key wrapped under the password:
$ cat uninet-key.json
{
"created_at": "2026-10-01T14:22:18.000Z",
"encrypted": true,
"kdf": "pbkdf2-sha256-310000/hkdf-sha256:uninet-wrap-v1",
"network": "uninet",
"public_key": "8919…7817",
"username": "alice",
"version": 3,
"wrapped_private_key": "v3:1892…84c3:7a4f…f8cc"
}
This is the client application's recovery file, byte for byte: import it there and you are
the same identity, and uninet-cli key import takes a file exported from the application.
Register the identity
$ uninet-cli --yes identity register
registered alice
identity ae68…b400
transaction c7c3…7451
The request carries the public key and a signature made with the key, over
SHA-256(username ‖ public_key ‖ profile_data_hash). The node verifies it before it writes,
because the identity's id is derived from that key and an unverified signature would let
anyone pre-register someone else's. The node also receives the wrapped key and a login proof
derived from the password, so the client application can sign in with the username and
password alone. The node cannot open the wrapped key: the proof it stores and the key that
opens the file are two separate HKDF branches of the stretched password.
$ uninet-cli wallet
WALLET NAME BALANCE
--------- -------------- -------
ae68…b400 alice's Wallet 0.0
Fund the wallet
The wallet is empty. On a node you run yourself you are also its operator, and the node's own
wallet is funded at genesis. A node's operator routes take a token signed with the node's
key, which uninet-node --operator-token prints for whoever can read the node's data
directory. It is good for ten minutes, on that node only.
$ uninet-cli --token "$(uninet-node --data-dir ./data --operator-token)" \
raw POST /api/tx/transfer --body '{"to":"ae68…b400","amount":"1200"}'
HTTP 200
block_hash: ebe2…ad65
block_height: 2
gas_used: 21000
receipt: {"error":null,"events":[],"gas_used":21000,"status":"Success","tx_hash":[157,69,136,187,…],"tx_index":0}
state_root: 1a9f…3fd2
tx_hash: 9d45…deae
$ uninet-cli wallet
WALLET NAME BALANCE
--------- -------------- -------
ae68…b400 alice's Wallet 1200.0
That transfer was signed by the node, with its key. Everything from here is signed by yours.
Send a transfer
$ uninet-cli --yes transfer 2222…2222 7
transaction {"Transfer":{"amount":7000000000000,"from":"ae68…b400","to":"2222…2222","token_id":"a089…fa41"}}
chain 5a69…463f
nonce 1
signer ae68…b400 (8919…7817)
applied daa2…76ca
The node never holds your key. It builds the transaction and answers with the exact bytes to
sign: the transaction as JSON, the chain id and your nonce. The CLI prints what those bytes
say, checks that the recipient and the amount in them are the ones you typed (amounts are in
base units, 10^12 to a whole token), asks unless --yes is given, signs, and sends the
signature. It computes the transaction's hash itself and waits for the receipt.
$ uninet-cli receipt daa2…76ca
error: -
events: []
gas_used: 21000
status: Success
tx_hash: [218,162,5,158,…]
tx_index: 0
A receipt is serialized straight from the node's receipt struct, which is why tx_hash comes
back as a byte array and not the hex string you asked with.
A transfer of more than the wallet holds is the chain's refusal, not a transport failure:
$ uninet-cli --yes transfer 2222…2222 999999
…
error: node returned an error for /api/transactions/submit-signed: insufficient balance: need 999,999.0000010905, have 1,192.9999989098
and the command exits 3.
Register a name
$ uninet-cli --yes dns register Hello.UNP
transaction {"RegisterDomain":{"domain":"hello.unp","nft_id":"1e31…b784"}}
chain 5a69…463f
nonce 2
price_une 50
fee_une 0.0000010727
signer ae68…b400 (8919…7817)
applied c980…4749
$ uninet-cli wallet
WALLET NAME BALANCE
--------- -------------- ---------------
ae68…b400 alice's Wallet 1142.9999978423
A .unp name is held for twelve months for 50 UNE, in lower case however it was typed. The
wallet is down by the 7 sent, the 50 for the name, and two fees.
The other signing commands follow the same steps: stake, unbond and withdraw for an
operator's stake, and tx sign <path> for any other transaction a node prepares. They are in
the CLI reference.
Exit codes
| Code | Meaning |
|---|---|
0 | The node answered and the answer is printed. |
1 | The node could not be reached, its identity or a response failed to verify, the key file did not open, or an argument failed local validation. |
2 | Usage error from clap: unknown subcommand or bad flag. |
3 | The node answered with a refusal: an unknown block, an unknown receipt, a transaction the chain refused, a 4xx. |
Three is deliberately not two. Several handlers return HTTP 200 with an error field, so
the CLI checks the envelope as well as the status, and --json still exits non-zero on an
error body. A wrong password exits 1 and sends nothing:
$ UNINET_PASSWORD=wrong uninet-cli wallet
error: wrong password, or a damaged key file
What this quickstart does not address
- A network. One node is one validator: it signs its own blocks, and nothing here shows agreement between machines. The repository's devnet and testnet scripts build a multi-node network, and a hosted testnet of five validators runs: a node joins it as in running a node, and the faucet funds a wallet on it.
- Key custody. The key is one file under one password. Whoever has both has the key, and losing either loses it. There is no hardware wallet, no seed phrase and no second factor.
- Bootstrap trust. The resolve response carries the node's public key and a signature made
with it. Verifying that proves the responder holds the private key for the key it just
sent; it does not prove that key belongs to the node you meant to reach. For an out-of-band
source of truth, pass
--network <file>(the validators a network file lists) or--node-key <hex>(one key): a node presenting another key is refused. - What the node builds. The CLI checks a transfer's recipient and amount, and that a
dedicated command got the transaction it stands for. With
tx signany transaction may come back, and the reader is the check. - Network position. The CLI opens a direct tunnel to the origin in
--node. It is one hop, sealed but not onion-routed, and anyone watching your link sees which node you queried. The node computes every answer and can log every question. - Files and sites. Uploading files and deploying a site are done from the client application. The terminal signs a change to a file the node already holds.
How it composes
The transport this page uses is the direct case of the addressing layer described in private networking: the same address form, without the relay path. The node on the other end is the process described in running a node, which is what you started above.
The key you made is an identity: an id derived from a key you hold, and a signature the node checks before it writes. What that identity is then permitted to do is authority, carried with a request and not inferred from having reached the node. The name you registered is described under domains, and the assumptions behind all of it are collected in the threat model.