Skip to content

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

bash
npm install @tenor/sdk

The 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

typescript
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:

bash
tenor compile escrow.tenor --output escrow.tenor.wasm

Evaluating a Contract

typescript
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.4

Computing the Action Space

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
// 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

typescript
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

typescript
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

typescript
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

typescript
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:

bash
tenor generate typescript escrow.tenor --output ./src/generated/escrow.ts

This produces typed interfaces you can use with both the evaluator and the client:

typescript
// 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:

typescript
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:

typescript
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:

typescript
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>
  );
}