Streams

A Stream is a capped, revocable, per-second allowance of a budget, aimed at exactly one service node. Streams are where enforcement lives: the cap bounds total spend, the rate envelope bounds burn speed, and revocation is instant and structural. All of it is enforced by your instance; the client cannot talk it out of a refusal.

Fan-out, delegation, revocation

import { Dynamo, HaltError, usd, units } from "@dynamoprotocol/sdk";

const dynamo = await Dynamo.open({
  coreUrl: process.env.DYNAMO_CORE_URL!,
  token: process.env.OWNER_API_TOKEN!,
});
const alice = "0x00000000000000000000000000000000000000A1";
const bob = "0x00000000000000000000000000000000000000B2";
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));

const budget = await dynamo.openBudget({ funding: "none", cap: usd("15") });

// Fan out: two top-level allowances with independent caps and envelopes.
const research = await budget.stream({ to: alice, rate: units(1_000_000n), cap: units(5_000_000n) });
const writing = await budget.stream({ to: bob, rate: units(1_000_000n), cap: units(6_000_000n) });

// Recursive delegation: a child allowance under research's cap. The engine
// enforces subtree arithmetic structurally — children can never outspend
// their parent's cap, at any depth.
const summarizer = await research.delegate({ to: bob, rate: units(500_000n), cap: units(1_000_000n) });

await sleep(1_300);
await research.tick(units(600_000n));
await summarizer.tick(units(400_000n));
await writing.tick(units(500_000n));

// Free pre-check material — no cost to ask before doing paid work (rule:
// free pre-checks precede paid actions).
const room = await writing.headroom();
if (room.remainingCapUnits <= 0n) throw new Error("expected headroom");

// Revoke research: the WHOLE subtree (research + summarizer) is dead from
// this instant. Anything stamped later — or in this same wall-clock
// second — is structurally unsettleable (see "Settle before you revoke").
await research.revoke();
let halted = false;
try {
  await summarizer.tick(units(1n));
} catch (err) {
  halted = err instanceof HaltError;
}
if (!halted) throw new Error("a revoked subtree must fail closed");

await sleep(1_100);
await writing.tick(units(300_000n));
// Graceful shutdown: settle the last signed voucher BEFORE revoking, so a
// same-second revocation can strand nothing ("Settle before you revoke").
await writing.checkpoint({ force: true });
await writing.revoke();

await budget.close();
const session = await budget.aggregate();
if (!session.reconciled) throw new Error("engine and settlement layer disagree");

Settle before you revoke

Timestamps at the money layer are whole unix seconds (voucher fields are on-chain uint64 types). Settlement for a revoked stream includes only vouchers signed strictly before the revocation second — the same rule the on-chain contract enforces. At second granularity, a voucher stamped in the revocation second cannot be proven to precede the revocation instant, so the money layer refuses it: fail closed, in the payer's favor.

The consequence: revoking a stream in the same wall-clock second as its final tick leaves those delivered units billed but permanently unsettled — the service node bears them. That bias is intentional for what instant revocation is for: an emergency cut, where under-paying the final second is the acceptable outcome. The research revocation in the sample above is exactly that case.

For a graceful shutdown, force a checkpoint before you revoke — as the sample does before revoking writing: await stream.checkpoint({ force: true }) drives the stream's last signed voucher to settlement immediately and returns the settlement record (or null when nothing is unsettled), and the revoke() that follows strands nothing. The method requires an instance serving owner-api 1.1.0 or later (client 2.1.0; older cores answer with a not-found error — fail closed, never silently dropped).

Fallback for older clients or cores (owner-api 1.0.0 / client 2.0.0, where checkpoint() does not exist): let a full wall-clock second elapse between the final tick and revoke(); the settlement run at budget.close() then includes the last signed voucher. Prefer checkpoint() — the elapsed-second form works but encodes a timing assumption the code cannot verify.

Two related facts worth knowing when you audit a session:

settlement layer itself reports. It does not assert that every billed unit settled — units clipped by a same-second revocation leave both sides in agreement at the lower figure.

session.totals.billedAmount with session.totals.settledAmount after close: equality means every signed voucher settled.

Fail closed, bill nothing

Every refusal path throws HaltError and bills zero units:

a revoked stream) cannot meter.

whole, never partially billed.

(rate × elapsed) is refused; burn speed is bounded even inside the cap.

whose burn accelerates abnormally, at the money layer, mid-loop.

A failed action never costs anything — no unit is billed for refused work, on any rail.

Safe retries: idempotent ticks

Ticks are idempotent per requestRef. Re-sending the same reference — a retry after a timeout, a crashed worker replaying its journal — bills once and returns duplicate: true:

import { Dynamo, usd, units } from "@dynamoprotocol/sdk";

const dynamo = await Dynamo.open({
  coreUrl: process.env.DYNAMO_CORE_URL!,
  token: process.env.OWNER_API_TOKEN!,
});
const alice = "0x00000000000000000000000000000000000000A1";
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));

const budget = await dynamo.openBudget({ funding: "none", cap: usd("5") });
const stream = await budget.stream({ to: alice, rate: units(1_000_000n), cap: units(2_000_000n) });
await sleep(1_100);

const first = await stream.tick(units(700_000n), "job-1");
if (first.duplicate) throw new Error("first billing must not be a duplicate");

// The retry: same requestRef, no double billing.
const retry = await stream.tick(units(700_000n), "job-1");
if (!retry.duplicate) throw new Error("a replayed requestRef must be a duplicate");
if (retry.cumulativeUnits !== first.cumulativeUnits) {
  throw new Error("a duplicate must not change the billed total");
}

// A billed requestRef also yields a SIGNED consume receipt — the
// request-bound proof a seller can verify offline (A.2b).
const receipt = await stream.receipt("job-1");
if (receipt.payload.requestRef !== "job-1") throw new Error("receipt must bind the requestRef");

await stream.revoke();
await budget.close();
const session = await budget.aggregate();
if (!session.reconciled) throw new Error("engine and settlement layer disagree");

For delivery-gated billing — bill only if the work delivers — meter AFTER delivery with the delivery's own reference as requestRef: a crashed or failed delivery bills nothing (rule: no cost for failed actions), and a replayed success bills once.