A full TypeScript walkthrough. In Node you verify receipts and talk to the cloud control plane; capturing and gating your own agent with decorators is the job of the Python SDK. Everything below runs in-process — no subprocess, and no re-implementing the crypto in JavaScript.
Install
Add the SDK to a Node project (Node 18 or later). @hesohq/sdk pulls in the native verifier @hesohq/core as a dependency, so verification works with no extra setup.
pnpm add @hesohq/sdkConfigure the cloud client
Call configure once at startup, before any cloud call, with your API key and endpoint. This is needed only for the cloud client — pushReceipt, openApproval, and friends. Local verification (gate, assertGate) needs no configuration and no network.
import { configure } from "@hesohq/sdk" configure(process.env.HESO_API_KEY!, "https://api.your-endpoint")
Read the API key from process.env. See Authentication for the header format, the environment variables, and how the SDK and CLI find your credentials.
Verify a receipt
gate verifies receipt bytes locally and returns a structured result. Pass a JSON string or a Buffer; the second argument is the minimum trust level to accept, defaulting to "L0".
import { gate } from "@hesohq/sdk"import { readFileSync } from "node:fs" const receiptJson = readFileSync("receipt.json", "utf8") const r = gate(receiptJson)console.log(r.allowed) // trueconsole.log(r.verdict) // "valid"console.log(r.trustLevel) // "L0"
It returns a GateResult, which is small on purpose:
- allowedboolean
truewhen the receipt verifies and its trust level meets the minimum you asked for.- trustLevelTrustLevel | null
- The trust level re-derived from which signatures pass —
"L0"(operator-signed) or"L1"(operator plus human co-signature).nullwhen verification fails before a level can be established. - verdictstring
- The verification outcome, e.g.
"valid". On failure it names the gate that failed, such as"hash_mismatch"or"invalid_signature". See Verdicts for the full list.
Trust is never trusted from the wire — it is re-derived on every verify from the signatures that actually pass. A receipt that claims "L1" but carries only an operator signature fails with the trust_mismatch verdict. Read Offline verification for the full seven-gate order.
Gate by trust level
When you want to halt on a bad receipt rather than branch on a boolean, use assertGate. It verifies and checks the minimum trust level, and throws if either fails. Asking for "L1" means the action must be valid and human co-signed before your code continues.
import { assertGate } from "@hesohq/sdk" // Throws unless the receipt verifies AND a human co-signed it (L1).assertGate(receiptJson, "L1") // Past this line, the action was authorized and human-approved.applyTransfer()
To branch on what policy decided rather than on cryptographic validity, use isDecisionAllowed. It takes a parsed ActionReceipt and the set of decision paths you accept, and returns a boolean.
import { isDecisionAllowed } from "@hesohq/sdk"import type { ActionReceipt } from "@hesohq/sdk" const receipt: ActionReceipt = JSON.parse(receiptJson) // Accept actions that policy let through or redacted; reject blocks.if (isDecisionAllowed(receipt, ["allow", "redact"])) { proceed()}
The four decision paths it can match are:
allow— policy let the action through.block— policy stopped the action.redact— policy stripped sensitive fields, then let it through.require_approval— policy routed the action to a human.
See Policy & decisions for how those paths are chosen and Trust levels for the L0/L1 distinction.
Wrap a client
wrap returns a Proxy around a client object. After each method call it looks for a __heso_receipt field on the result and gates that receipt against your minTrust. One thing to be clear about: it only gates clients that attach a receipt to their response. A method that returns no __heso_receipt is passed through unguarded.
import { wrap, pushReceipt } from "@hesohq/sdk" const guarded = wrap(agentClient, { minTrust: "L1", onReceipt: async (method, receiptJson) => { await pushReceipt(JSON.parse(receiptJson)) }, onGateFail: (method, verdict) => { console.error(`${method} failed gating: ${verdict}`) return false // re-throw the gate failure },}) // Every result that carries a __heso_receipt field is gated automatically.const out = await guarded.transfer({ amountUsd: 4200 })
The WrapOptions hooks let you set the bar, persist receipts, and decide what happens on a failed gate:
- minTrustTrustLevel
- The minimum trust level every attached receipt must meet, e.g.
"L1"to require a human co-signature. - onReceipt(method, receiptJson) => void | Promise<void>
- Called with the method name and the receipt JSON for each gated result — a natural place to
pushReceiptinto the cloud outbox. - onGateFail(method, verdict) => boolean | Promise<boolean>
- Called when a receipt fails its gate. Return
trueto swallow the failure, orfalseto re-throw it.
Push to the cloud
pushReceipt sends one receipt to the outbox. The server re-verifies it before accepting — the same byte-identical check you ran locally — so a tampered or under-signed receipt is rejected at the control plane too.
import { pushReceipt } from "@hesohq/sdk"import type { ActionReceipt } from "@hesohq/sdk" const receipt: ActionReceipt = JSON.parse(receiptJson) const result = await pushReceipt(receipt)if (!result.accepted) { throw new Error(result.rejectionReason ?? "receipt rejected")}console.log(result.receiptId)
It resolves to an OutboxPushResult:
- receiptIdstring
- The id the control plane assigned the stored receipt.
- acceptedboolean
- Whether the server accepted the receipt after re-verifying it.
- rejectionReasonstring
- Present only when
acceptedisfalse— why the server turned the receipt away.
To send many at once, pushReceipts takes an array and returns one result per receipt. When an action needs a person in the loop, open an approval with openApproval and block on waitForApproval. It polls until the approver decides or the call times out (defaults: poll every 2000 ms, time out after 300000 ms). The full surface — the cloud client, the gating helpers, and the wire types — lives in the TypeScript SDK reference.
Accepting a receipt proves the operator authorized the action under a known policy, and at L1 that a person approved it with a device-held key. It records what was authorized, not whether the action succeeded downstream. The cloud holds no signing key; it only re-checks the signatures already on the receipt.
