The Tenor TypeScript SDK provides two classes: TenorEvaluator for local evaluation via WASM, and TenorClient for remote evaluation and execution via the HTTP API.
Installation
npm install @tenor/sdkThe package includes the WASM evaluator binary. No additional native dependencies are required. Works in Node.js 18+, Deno, Bun, and modern browsers.
TenorEvaluator — Local WASM Evaluation
TenorEvaluator loads a compiled contract (.tenor.wasm) and evaluates it locally in your process. No network calls, no API key, no platform dependency. The evaluator is a pure function: same inputs always produce same outputs.
Loading a Contract
import { TenorEvaluator } from "@tenor/sdk";
// Load from file path (Node.js)
const evaluator = await TenorEvaluator.load("./contracts/escrow.tenor.wasm");
// Load from URL (browser)
const evaluator = await TenorEvaluator.load(
"https://cdn.example.com/contracts/escrow.tenor.wasm"
);
// Load from buffer (any runtime)
const wasmBytes = await fs.readFile("./contracts/escrow.tenor.wasm");
const evaluator = await TenorEvaluator.fromBytes(wasmBytes);The compiled WASM artifact is produced by tenor compile:
tenor compile escrow.tenor --output escrow.tenor.wasmEvaluating a Contract
const result = evaluator.evaluate({
facts: {
payment_received: true,
payment_amount: "5000.00",
customer_tier: "premium",
},
entities: {
Order: { order_001: "pending" },
Payment: { pay_001: "cleared" },
},
});
console.log(result.verdicts);
// {
// payment_ok: { value: true, type: "Bool", stratum: 0 },
// eligible_for_release: { value: true, type: "Bool", stratum: 1 }
// }
console.log(result.evaluationTimeMs);
// 0.4Computing the Action Space
const actions = evaluator.computeActionSpace({
facts: {
payment_received: true,
payment_amount: "5000.00",
customer_tier: "premium",
},
entities: {
Order: { order_001: "pending" },
Payment: { pay_001: "cleared" },
},
});
console.log(actions);
// {
// escrow_agent: [
// {
// operation: "release_funds",
// preconditionMet: true,
// entitiesAffected: [
// { entity: "Order", instance: "order_001", from: "pending", to: "released" }
// ]
// }
// ],
// buyer: [
// {
// operation: "dispute_transaction",
// preconditionMet: true,
// entitiesAffected: [
// { entity: "Order", instance: "order_001", from: "pending", to: "disputed" }
// ]
// }
// ],
// seller: []
// }Action Space for a Single Persona
const buyerActions = evaluator.computeActionSpace({
facts: { payment_received: true, payment_amount: "5000.00" },
entities: { Order: { order_001: "pending" } },
persona: "buyer",
});
console.log(buyerActions);
// [
// {
// operation: "dispute_transaction",
// preconditionMet: true,
// entitiesAffected: [...]
// }
// ]Querying Contract Metadata
const meta = evaluator.metadata();
console.log(meta.entities);
// [
// { name: "Order", states: ["pending", "approved", "disputed", "released"], initial: "pending" },
// { name: "Payment", states: ["held", "released", "refunded"], initial: "held" }
// ]
console.log(meta.personas);
// ["escrow_agent", "buyer", "seller"]
console.log(meta.facts);
// [
// { name: "payment_received", type: "Bool" },
// { name: "payment_amount", type: "Money" },
// { name: "customer_tier", type: "Text" }
// ]
console.log(meta.contractHash);
// "sha256:9f4a2b..."Error Handling
try {
evaluator.evaluate({
facts: {
payment_received: "yes", // Wrong type: expected Bool, got Text
},
entities: {},
});
} catch (err) {
if (err instanceof TenorTypeError) {
console.error(err.fact); // "payment_received"
console.error(err.expectedType); // "Bool"
console.error(err.receivedType); // "Text"
console.error(err.message);
// "Fact 'payment_received' expected type Bool, got Text"
}
}
try {
evaluator.evaluate({
facts: {
nonexistent_fact: true, // Undeclared fact
},
entities: {},
});
} catch (err) {
if (err instanceof TenorUndeclaredFactError) {
console.error(err.fact); // "nonexistent_fact"
console.error(err.message);
// "Fact 'nonexistent_fact' is not declared in this contract"
}
}TenorClient — Remote HTTP API
TenorClient calls the Tenor platform at api.tenor.run. It supports evaluation, action space queries, flow execution, and simulation. State is managed by the platform.
Constructor
import { TenorClient } from "@tenor/sdk";
const client = new TenorClient({
org: "acme",
apiKey: "tk_live_9f4a2b3c4d5e6f7a8b9c0d1e2f3a4b5c",
// Optional overrides:
baseUrl: "https://api.tenor.run", // default
timeout: 10000, // ms, default 10000
retries: 3, // automatic retry on 429/5xx, default 3
});Evaluate
const result = await client.evaluate("escrow", {
facts: {
payment_received: true,
payment_amount: "5000.00",
},
});
console.log(result.verdicts);
// {
// payment_ok: { value: true, type: "Bool", stratum: 0, rule: "check_payment" },
// eligible_for_release: { value: true, type: "Bool", stratum: 1, rule: "check_release_eligibility" }
// }
console.log(result.actionSpace);
// { escrow_agent: [...], buyer: [...], seller: [] }
console.log(result.requestId);
// "req_20260215_abc123"Action Space
// All personas
const actions = await client.actions("escrow");
// Single persona
const agentActions = await client.actions("escrow", {
persona: "escrow_agent",
});
console.log(agentActions);
// [
// { operation: "release_funds", preconditionMet: true, entitiesAffected: [...] }
// ]Execute a Flow
const execution = await client.executeFlow("escrow", "release_flow", {
persona: "escrow_agent",
facts: {
release_approved: true,
release_reason: "Goods received and inspected",
},
entityBindings: {
Order: "order_001",
Payment: "pay_001",
},
idempotencyKey: "idem_20260215_001", // optional, prevents duplicate execution
});
console.log(execution.status);
// "completed"
console.log(execution.stepsExecuted);
// [
// {
// step: "verify_approval",
// operation: "verify_release",
// result: "success",
// entityTransitions: [{ entity: "Order", instance: "order_001", from: "pending", to: "approved" }]
// },
// {
// step: "release_payment",
// operation: "release_funds",
// result: "success",
// entityTransitions: [{ entity: "Payment", instance: "pay_001", from: "held", to: "released" }]
// }
// ]
console.log(execution.provenanceChainId);
// "chain_20260215_001"Execute a Single Step
const stepResult = await client.executeFlowStep(
"escrow",
"release_flow",
"verify_approval",
{
persona: "escrow_agent",
facts: { release_approved: true },
entityBindings: { Order: "order_001" },
}
);
console.log(stepResult.status);
// "in_progress"
console.log(stepResult.nextStep);
// { step: "release_payment", requiredPersona: "escrow_agent" }Simulate
const simulation = await client.simulate("escrow", {
flow: "release_flow",
persona: "escrow_agent",
facts: { release_approved: true },
entityBindings: { Order: "order_001", Payment: "pay_001" },
});
console.log(simulation.status);
// "would_succeed"
console.log(simulation.projectedSteps);
// [
// { step: "verify_approval", wouldSucceed: true, projectedTransitions: [...] },
// { step: "release_payment", wouldSucceed: true, projectedTransitions: [...] }
// ]
console.log(simulation.projectedFinalStates);
// { Order: { order_001: "approved" }, Payment: { pay_001: "released" } }Error Handling
import {
TenorApiError,
TenorUnauthorizedError,
TenorForbiddenError,
TenorPreconditionError,
TenorConflictError,
TenorRateLimitError,
} from "@tenor/sdk";
try {
await client.executeFlow("escrow", "release_flow", {
persona: "buyer", // Not authorized for release_funds
facts: { release_approved: true },
entityBindings: { Order: "order_001" },
});
} catch (err) {
if (err instanceof TenorForbiddenError) {
console.error(err.persona); // "buyer"
console.error(err.operation); // "release_funds"
console.error(err.allowedPersonas); // ["escrow_agent"]
}
if (err instanceof TenorPreconditionError) {
console.error(err.operation); // "verify_release"
console.error(err.precondition); // "verdict_present(release_approval_ok)"
console.error(err.errorMessage); // "Release conditions not met."
}
if (err instanceof TenorConflictError) {
console.error(err.entity); // "Order"
console.error(err.instance); // "order_001"
console.error(err.expectedState); // "pending"
console.error(err.actualState); // "disputed"
}
if (err instanceof TenorRateLimitError) {
console.error(err.retryAfterMs); // 1200
// SDK retries automatically up to `retries` times
}
}Generated Types
Use tenor generate typescript to produce typed bindings from your contract:
tenor generate typescript escrow.tenor --output ./src/generated/escrow.tsThis produces typed interfaces you can use with both the evaluator and the client:
// src/generated/escrow.ts (generated — do not edit)
export interface EscrowFacts {
payment_received: boolean;
payment_amount: string; // Money type is string in TS
customer_tier: string;
release_approved?: boolean;
dispute_reason?: string;
}
export type OrderState =
| "pending"
| "approved"
| "disputed"
| "released"
| "cancelled";
export type PaymentState = "held" | "released" | "refunded";
export type EscrowPersona = "escrow_agent" | "buyer" | "seller";
export type EscrowOperation =
| "release_funds"
| "dispute_transaction"
| "cancel_order"
| "verify_release"
| "escalate_dispute"
| "refund_payment";
export interface EscrowVerdicts {
payment_ok?: { value: boolean; stratum: 0 };
eligible_for_release?: { value: boolean; stratum: 1 };
release_approval_ok?: { value: boolean; stratum: 1 };
}
export interface EscrowEntities {
Order: Record<string, OrderState>;
Payment: Record<string, PaymentState>;
}Using the generated types:
import { TenorEvaluator } from "@tenor/sdk";
import type { EscrowFacts, EscrowEntities, EscrowVerdicts } from "./generated/escrow";
const evaluator = await TenorEvaluator.load("./escrow.tenor.wasm");
const facts: EscrowFacts = {
payment_received: true,
payment_amount: "5000.00",
customer_tier: "premium",
};
const entities: EscrowEntities = {
Order: { order_001: "pending" },
Payment: { pay_001: "cleared" },
// Compile error: unknown entity type
};
const result = evaluator.evaluate({ facts, entities });
const verdicts = result.verdicts as EscrowVerdicts;
if (verdicts.payment_ok?.value) {
console.log("Payment verified at stratum", verdicts.payment_ok.stratum);
}Full Working Example: Express.js Middleware
A complete example using the TypeScript SDK to build an Express middleware that checks contract permissions before allowing API actions:
import express from "express";
import { TenorClient, TenorForbiddenError, TenorPreconditionError } from "@tenor/sdk";
import type { EscrowFacts, EscrowPersona } from "./generated/escrow";
const app = express();
app.use(express.json());
const tenor = new TenorClient({
org: "acme",
apiKey: process.env.TENOR_API_KEY!,
});
// Middleware: check what the current user can do
async function loadActionSpace(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
const persona = req.headers["x-persona"] as EscrowPersona;
if (!persona) {
res.status(400).json({ error: "Missing X-Persona header" });
return;
}
try {
const actions = await tenor.actions("escrow", { persona });
req.availableActions = actions;
next();
} catch (err) {
if (err instanceof TenorForbiddenError) {
res.status(403).json({ error: "Unknown persona" });
return;
}
next(err);
}
}
// Release endpoint: execute the release flow
app.post("/orders/:orderId/release", loadActionSpace, async (req, res) => {
const canRelease = req.availableActions.some(
(a) => a.operation === "release_funds" && a.preconditionMet
);
if (!canRelease) {
res.status(403).json({
error: "Release not available",
availableActions: req.availableActions.map((a) => a.operation),
});
return;
}
try {
const result = await tenor.executeFlow("escrow", "release_flow", {
persona: req.headers["x-persona"] as EscrowPersona,
facts: { release_approved: true, release_reason: req.body.reason },
entityBindings: { Order: req.params.orderId },
});
res.json({
status: result.status,
provenanceChainId: result.provenanceChainId,
finalStates: result.finalEntityStates,
});
} catch (err) {
if (err instanceof TenorPreconditionError) {
res.status(422).json({ error: err.errorMessage });
return;
}
throw err;
}
});
app.listen(3000, () => {
console.log("Escrow service listening on :3000");
});Full Working Example: Browser Action Space Preview
Using the WASM evaluator in a React component to show available actions:
import { useState, useEffect } from "react";
import { TenorEvaluator } from "@tenor/sdk";
import type { EscrowPersona } from "./generated/escrow";
interface ActionPreviewProps {
persona: EscrowPersona;
orderId: string;
orderState: string;
facts: Record<string, unknown>;
}
export function ActionPreview({ persona, orderId, orderState, facts }: ActionPreviewProps) {
const [actions, setActions] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function evaluate() {
const evaluator = await TenorEvaluator.load("/contracts/escrow.tenor.wasm");
const result = evaluator.computeActionSpace({
facts,
entities: { Order: { [orderId]: orderState } },
persona,
});
if (!cancelled) {
setActions(result);
setLoading(false);
}
}
evaluate();
return () => { cancelled = true; };
}, [persona, orderId, orderState, facts]);
if (loading) return <div>Evaluating contract...</div>;
return (
<div>
<h3>Available Actions for {persona}</h3>
{actions.length === 0 ? (
<p>No actions available in current state.</p>
) : (
<ul>
{actions.map((action) => (
<li key={action.operation}>
<strong>{action.operation}</strong>
{action.preconditionMet ? " (ready)" : " (precondition not met)"}
</li>
))}
</ul>
)}
</div>
);
}