Every stream's vouchers are signed by a scoped session key — a signer valid for that one stream and nothing else. Streams you open over the wire keep their scoped key with the core (stream.sessionKeyAddress names it); the blast radius of any leak is one stream's remaining cap, bounded by its rate envelope, revocable in one call. The SessionKey class in the SDK is the standalone signer for direct settlement flows against the published contracts — same confinement, enforced structurally by the settlement layer.
import { Dynamo, usd, units, verifyAllowanceCredential } from "@dynamoprotocol/sdk";
const dynamo = await Dynamo.open({
coreUrl: process.env.DYNAMO_CORE_URL!,
token: process.env.OWNER_API_TOKEN!,
});
const alice = "0x00000000000000000000000000000000000000A1";
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) });
// The scoped signer behind this stream's vouchers, held by the core (Q6).
if (!/^0x[0-9a-fA-F]{40}$/.test(stream.sessionKeyAddress)) {
throw new Error("every stream has a scoped session key");
}
// The offline allowance credential: a signed JWS (ES256K) any gateway can
// verify in-process against a pinned engine key — no network call in the
// request path, ever.
const { token, payload } = await stream.credential({ ttlSeconds: 300n });
if (token.split(".").length !== 3) throw new Error("expected a compact JWS");
if (payload.streamId !== stream.id) throw new Error("credential must name the stream");
if (payload.serviceNode.toLowerCase() !== alice.toLowerCase()) {
throw new Error("credential must be aimed at the payee");
}
// Verify it EXACTLY the way a seller or gateway does: offline, in-process,
// against the pinned engine signer your budget reported at open.
const verdict = await verifyAllowanceCredential(token, {
expectedSigner: budget.engineSigner as `0x${string}`,
nowSeconds: BigInt(Math.floor(Date.now() / 1000)),
});
if (!verdict.valid) throw new Error(`a live credential must verify: ${verdict.reason}`);
// A WRONG pinned signer is refused — fail closed, no network involved.
const wrong = await verifyAllowanceCredential(token, {
expectedSigner: "0x00000000000000000000000000000000000000eE",
nowSeconds: BigInt(Math.floor(Date.now() / 1000)),
});
if (wrong.valid) throw new Error("a foreign signer must never verify");
await stream.revoke();
await budget.close();
const session = await budget.aggregate();
if (!session.reconciled) throw new Error("engine and settlement layer disagree");
| Artifact | Who holds it | What it proves |
|---|---|---|
| Session key | The core (wire streams) or a direct-settlement worker | Authority to SIGN vouchers for one stream — spending within that stream's cap and envelope |
| Allowance credential (JWS) | Presented to a seller/gateway per request | That a live, capped, unexpired allowance aimed at THIS service node exists — verifiable offline against a pinned key |
Gateways and the MCP adapter verify credentials in-process, offline (request-path purity): an absent, unverifiable, expired, exhausted, or revoked-at-last-sync credential fails closed with HTTP 402 or a tool error. There is no fail-open path, and no blocking network call in the request path.
A stolen session key can attempt exactly two things, and both are bounded: a voucher for a foreign stream is refused — the key's scope does not match — and burning its own stream runs into the cap (bounded loss, then 402). Revoking the stream ends the incident; nothing stamped after the revocation instant can settle.