Docs

Quick start, object model, node, transport, and running the network.


Quick start

Node.js 20+ required.

Run a node

git clone https://github.com/intervalplace/aon.git
cd aon
npm install

Create a .env file:

AON_PORT=8787
AON_P2P_PORT=9000
AON_PEER_KEY_PATH=./data/peer.key
AON_BOOTSTRAP=/ip4/151.240.121.186/tcp/9000/p2p/12D3KooWAy5oxGuRFZD1SV1vCcD7tK6aRAUS1VYqmG7XwLAXNP5p

Start and verify:

npm run dev

curl http://localhost:8787/v1/health
# {"ok":true,"service":"aon-node-v0"}

curl http://localhost:8787/v1/p2p/info
# {"ok":true,"p2p":{"started":true,"peerId":"12D3KooW...","peers":[...]}}

Submit your first object

curl -X POST http://localhost:8787/v1/objects \
  -H "Content-Type: application/json" \
  -d '{
    "objectType":    "authorization",
    "schemaVersion": "1",
    "namespace":     "aon:evm-spot",
    "createdAt":     1234567890000,
    "references":    [],
    "payload":       { "example": true }
  }'
# {"ok":true,"objectHash":"0x...","object":{...}}

curl http://localhost:8787/v1/objects/0x...

Run an executor

npm install @intervalplace/aon-sdk @intervalplace/aon-namespace-csd-usdc
import { runExecutor, registerNamespace } from "@intervalplace/aon-sdk";
import { csdUsdcNamespace } from "@intervalplace/aon-namespace-csd-usdc";

registerNamespace(csdUsdcNamespace);

await runExecutor({
  nodeUrl:        "http://localhost:8787",
  namespace:      "aon:csd-usdc",
  mode:           "simulate",
  pollIntervalMs: 5000,
  onExecuted: (graph, result) => console.log("executed", result),
  onError:    (err, context)  => console.error(context, err),
});

Define a new namespace

import type { NamespaceDriver, AonObject } from "@intervalplace/aon-sdk";

export const myNamespace: NamespaceDriver = {
  namespace: "aon:my-namespace",

  evaluate(objects: AonObject[]) {
    // find authorizations and proofs — return executable graphs
    return [];
  },

  async execute(graph, args) {
    const mode = args?.mode ?? "simulate";
    if (mode === "simulate") {
      return { executed: true, mode, result: "simulated" };
    }
    // on-chain execution logic here
  },
};

registerNamespace(myNamespace);

await runExecutor({
  nodeUrl:   "http://localhost:8787",
  namespace: "aon:my-namespace",
  mode:      "contract",
});

The node stores and propagates your objects without knowing what they mean. No node or SDK changes required.

Object model

Every object shares a common envelope. Identity is derived from content: the objectHash is a keccak256 of the canonical serialization of all other fields. Objects are immutable. If any field changes, the result is a new object with a different hash.

{
  "objectType":    string,
  "schemaVersion": "1",
  "namespace":     "aon:example",
  "createdAt":     1781080000000,
  "references":    ["0x..."],
  "payload":       { ... },
  "objectHash":    "0x..."
}

The node always recomputes objectHash from content on receipt. Any submitted hash is ignored.

Primitive object types

The protocol defines five primitive object types. Everything else is namespace-specific; the node stores and propagates all object types without interpretation.

Authorization

A user-signed permission for a bounded state transition. Specifies who grants permission, what action is permitted, execution constraints, and validity window (validAfter and validBefore). Signature verification is handled by the SDK and namespace adapters, not the node.

Condition

A requirement that must be satisfied before execution may occur. The namespace determines what constitutes valid proof for a given condition.

Proof

Evidence that a condition has been met. Proofs do not create the facts they describe; those originate within external systems. The node propagates proofs; namespaces validate them.

Receipt

A record that execution completed. References the objects that comprised the executable graph. Built and submitted by executors via the SDK.

Revocation

A signed declaration that an authorization is no longer valid. References the target authorization by hash. Executors and namespace adapters check revocation status before acting on a graph.

Namespace-specific objects

Namespaces may define additional object types beyond the five primitives. A reserve (committed settlement state) is one example, used by aon-namespace-csd-usdc to represent locked USDC before proof arrives. In aon-namespace-evm-spot, order and fill are distinct object types that carry signed orders and fill parameters respectively. These are namespace-specific conventions. The protocol does not require them.


Authorization graphs

Objects reference other objects through the references field. An authorization graph becomes executable when all required components are present and no receipt has yet consumed the graph. Graph shape is determined by the namespace, not the protocol. The minimum viable graph is an authorization and a receipt. Everything between them depends on what the namespace requires.

The canonical shape for aon:csd-usdc:

Authorization → Reserve → Proof → Receipt

The shape for aon:evm-spot, which requires no reserve:

Maker Authorization + Taker Authorization + Maker Order + Taker Order + Fill → Receipt

The node

A node has exactly five responsibilities: accept and structurally validate objects, store them content-addressed, maintain the inbound reference index, propagate objects via the configured transports, and serve the HTTP API.

A node does not interpret payload content, validate signatures, evaluate graph executability, perform execution, or maintain application state. Those responsibilities belong to the SDK and namespace adapters.

HTTP API

MethodPathDescription
POST/v1/objectsSubmit an object. Hash is always recomputed from content.
GET/v1/objectsList objects. Filter by objectType, namespace, references.
GET/v1/objects/:hashFetch a single object by hash.
GET/v1/objects/:hash/referencesInbound references to an object.
GET/v1/graphs/:hashFull object graph rooted at hash (nodes + edges). Query param: maxObjects (default 1000).
GET/v1/graph/walk/:hashBFS inbound graph walk. Params: maxDepth, maxObjects.
GET/v1/p2p/infoNode identity and peer list.
GET/v1/p2p/pubsubPubsub topic and subscriber info.
POST/v1/p2p/dialConnect to a peer by address.
POST/v1/p2p/request-objectRequest a specific object from a specific peer.
POST/v1/p2p/gossip/:objectHashBroadcast a stored object to all peers.
POST/v1/p2p/exchangeExchange peer lists with a peer.
GET/v1/healthHealth check.

All responses carry an ok boolean. Errors carry a string error.code. Semantic queries (executable graphs, open authorizations, receipt lookups) are handled by the SDK using objects fetched from the node, not by the node itself.


Transport

The node has no knowledge of TCP, libp2p, radio, or any physical medium. It depends only on the AonTransport interface. Any number of transport implementations run simultaneously via MultiTransport; objects arriving on any transport are cross-propagated to all others automatically.

TransportMediumEnabled when
libp2pTCP/IPAlways
WebSocketWebSocket (browsers and lightweight clients)Default on. Disable with AON_WS=false
LoRaLong-range radioAON_LORA_PORT is set
BluetoothBluetooth Classic or BLEAON_BT=true
ReticulumLoRa, I2P, serial, TCP via RNSAON_RNS=true

Each transport is an independent failure domain. If internet connectivity is lost, LoRa and Bluetooth continue propagating objects within their physical range. The network survives as long as any two nodes can reach each other through any transport.

To add a new transport, implement the AonTransport interface and add it to buildTransports() in src/server.ts. No other node code changes.

Address formats

TransportFormatExample
libp2pmultiaddr/ip4/1.2.3.4/tcp/9000/p2p/12D3KooW...
WebSocketws:// or wss://ws://1.2.3.4:8788
LoRalora:{peerId}lora:AABBCCDDEEAA
Bluetoothbt:// or ble://bt://AA:BB:CC:DD:EE:FF
Reticulumrns://{hash}rns://a1b2c3d4...

Architecture

The system is composed of three layers, each in its own package:

An executor imports the SDK and any namespace packages it wants to support, registers them, and connects to a node via HTTP. The node has no knowledge of namespaces or executors.


The SDK

The NamespaceDriver interface is the extension point for new namespaces. Any package that implements it can be registered with the executor and will work without any changes to the node or SDK:

type NamespaceDriver = {
  namespace:        string;
  evaluate:         (objects: AonObject[], opts?: any) => any[];
  reward?:          (graph: any) => any;
  verify?:          (graph: any) => any;
  execute?:         (graph: any, args?: { mode?: "off" | "simulate" | "contract" }) => Promise<any>;
  validateObject?:  (obj: AonObject) => { valid: boolean; reason?: string };
};

Namespace packages are registered before the executor loop starts. The executor then polls the node, evaluates graphs using the registered driver, and submits receipts back as plain objects:

import { runExecutor, registerNamespace } from "@intervalplace/aon-sdk";
import { csdUsdcNamespace } from "aon-namespace-csd-usdc";

registerNamespace(csdUsdcNamespace);

await runExecutor({
  nodeUrl:        "http://localhost:8787",
  namespace:      "aon:csd-usdc",
  mode:           "contract",
  pollIntervalMs: 5000,
});

Namespaces

A namespace identifies the environment within which objects are interpreted. The node transports objects; namespaces determine what those objects mean and what constitutes valid execution.

Namespaces are implemented as independent packages that implement the NamespaceDriver interface from the SDK. They are not part of the node and require no changes to the node to add. Anyone may publish a namespace package.

aon:csd-usdc / aon-namespace-csd-usdc

Cross-chain settlement between CSD (Compute Substrate) and USDC on EVM. A buyer signs an EIP-712 Authorization Object granting release of USDC conditioned on a confirmed CSD payment. The seller locks the buyer's USDC in the settlement contract, sends CSD, and submits a proof object carrying the full SPV proof: raw transaction bytes, merkle branch, block header, and a confirmation chain of subsequent headers.

An executor calls settleCsdUsdc on the settlement contract with the authorization, signature, and SPV proof. The contract verifies everything on-chain: recomputes the CSD txid from raw bytes, walks the merkle branch, hashes the block header, checks proof-of-work, and queries the CsdHeaderOracle to confirm the payment block is buried to sufficient depth. USDC is released to the seller and the executor fee is paid atomically in the same transaction.

The CsdHeaderOracle is a separate contract that maintains a chain of CSD block headers on EVM. Anyone may submit headers. The settlement contract uses it as a light client to verify confirmation depth without trusting any single party.

aon:evm-spot / aon-namespace-evm-spot

Non-custodial EVM spot trading. Maker and taker each sign a bounded session authorization. An executor matches them into a fill and settles atomically on-chain. Partial fills are supported. No reserve step; settlement is the lock.


Running a node

npm install
npm run dev

The object store is a SQLite database at data/aon.db by default (configurable via AON_DATA_DIR). WAL mode is enabled for concurrent reads. If a previous file-based store exists at data/index.json, it is migrated automatically on first startup. To join the network, configure at least one bootnode address. For LoRa, connect a compatible serial module and set AON_LORA_PORT.

# Minimal internet node
AON_PORT=8787
AON_P2P_PORT=9000
AON_PEER_KEY_PATH=./data/peer.key
AON_DATA_DIR=./data
AON_BOOTSTRAP=/ip4/151.240.121.186/tcp/9000/p2p/12D3KooWAy5oxGuRFZD1SV1vCcD7tK6aRAUS1VYqmG7XwLAXNP5p

# With LoRa radio
AON_LORA_PORT=/dev/ttyUSB0

# With Bluetooth
AON_BT=true

# With Reticulum (LoRa, I2P, serial via RNS)
AON_RNS=true

Bootnodes

The following are stable entry points for joining the network. Pass one or more as AON_BOOTSTRAP, comma-separated.

/ip4/151.240.121.186/tcp/9000/p2p/12D3KooWAy5oxGuRFZD1SV1vCcD7tK6aRAUS1VYqmG7XwLAXNP5p