Skip to main content
Get two nodes talking on your machine, and watch the routing model change in response to what happens.
NTL is in active development. Everything below is real and runs today, but the CLI and API are not yet stable.

Prerequisites

  • Node 18+ (for the npm install) or Rust 1.85+ (install)
  • A terminal

Install

The CLI is a Rust binary; npm is just the distribution channel, because it is the package manager almost everyone already has. The postinstall step downloads the binary for your platform and verifies its SHA-256 checksum. Prefer Rust, or on a platform without a prebuilt binary:

Initialize a Node

This creates, under ~/.ntl (override with --home or $NTL_HOME): Output:
The identity is a keypair derived from a seed in the store. It persists — a node that regenerated its identity on each start would lose every synapse it had formed and relearn its routing from scratch every restart. Pick a different deployment class with --class edge | full-node | high-traffic. The class sets learning rate, queue depth, refractory period, and fanout together, so the combination stays coherent.

Two Nodes, One Signal

NTL is peer-to-peer with no broker, so the smallest interesting network is two nodes. Use two terminals. Terminal 1 — the receiver:
Terminal 2 — the sender:
The sender prints:
And the receiver:
Three things in that exchange are worth pausing on. weight 0.450, not 0.500. The signal was attenuated by the synapse it crossed (0.5 × 0.9). Weight decays with distance, which is what stops a flood from travelling forever. --acknowledged. Without it the signal is best-effort: it may be absorbed silently and the sender never learns. With it, the sender is guaranteed an outcome — a receipt or a timeout, never silence. Use it for anything with a side effect. See delivery-semantics. weight +0.0322. That is the routing model updating. The receipt came back, resolved the journalled routing decision, and strengthened the synapse that carried it. Emit again and it rises again — bounded by the total weight budget and the per-peer influence cap.

Watch the Model

WEIGHT is learned, not configured. AFFINITY is learned per signal type, so a peer can become preferred for Query while staying unremarkable for Command.
Those last two lines are the model’s health check:
  • exploration near zero — the node has stopped trying alternatives, so it can no longer discover a better path. (With only one eligible synapse there is nothing to explore, and ntl status will not warn.)
  • pending near 100% — no receipts are arriving, so the weights reflect nothing. Usually it means peers are emitting best-effort traffic only.

See It Learn

The two-node exchange above moves a weight once. To see the model actually separate a good peer from a bad one:
The * marks an exploratory choice. Note that peer B keeps being tried even after it starts failing — that is deliberate, and it is what lets B recover if it starts working again. A router that stopped probing a failing path could never notice it had recovered. The demo advances a simulated clock by an hour per round, because a per-identity influence cap bounds how much one peer can move a weight inside one window. Learning is rate-limited on purpose; see learning-model §2.4.

Running a Long-Lived Node

--dev binds loopback only, so a development node is not reachable from the network by accident. For a real deployment use ntl start --listen 0.0.0.0:4433 --peer <addr>, or the ntl-node / ntl-edge binaries, which read $NTL_HOME, $NTL_LISTEN, and $NTL_PEERS.

What Is Not Built Yet

Being explicit, so you do not go looking:

Next

Your First Signal

Signal types, weights, scopes, and tags in depth

Storage Backends

SQLite, Postgres, graph, and KV backends

Learning Model

How weights are actually updated

Threat Model

What NTL defends against, and what it does not
Last modified on September 11, 2026