Skip to content

Developers

Use your own model

Register your own decision server, and a model you set up takes its answers from it. Helm signs each request, then seals every answer, scores it on your outcomes and can calibrate it.

How it works

  • On the API page in the app, under Endpoints, register your server’s address. Helm shows its signing secret once: keep it on your server, as you would a key. The page lists each endpoint with its id.
  • In Start now, choose Answered by your own model and pick the endpoint. Test sends it one signed sample request and shows the answer it read, or why it was refused; a test saves and charges nothing.
  • Every call of that model, on POST /v1/models/{model}/forecasts or POST /v1/systemone, sends your server one request per record. Helm seals each answer before it returns it, as it seals its own.
  • Helm scores the answers on the outcomes you record, and from enough of them calibrates them. It does not compute per-field reasons for a model your server answers, and each answer says so in its warnings.

Where Helm sends it

  • An https address on port 443 or 8443, under a public host name: no IP address, and no user name or password in it. Helm checks it again on every request.
  • Helm never follows a redirect: it would send the request and its signature to a host you did not register.

The request

One POST per record to the address you registered, with content-type: application/json, a Helm-Signature and a Helm-Request-Id:

POST https://models.example.com/v1/systemone
Content-Type: application/json
Helm-Signature: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIs…
Helm-Request-Id: dreq_…

{"model": "late_orders",
 "state": {"ordered_on": "2026-09-01", "region": "China",
           "value": 18250.0, "expedite": true},
 "questions": {"late_orders": {
   "type": "noul",
   "instructions": "Will this order arrive late?",
   "criteria": {"true": "Arrives after the promised date",
                "false": "On time"}}}}
  • model is the model’s short name, and questions asks its one question under that name: noul for yes or no, choice for one of several answers. instructions is the question as written, and criteria describes each answer: true and false for yes or no, each answer’s id for a choice.
  • state holds the record’s facts by name: a category as its listed value, a number as a number, a date as YYYY-MM-DD, yes or no as true or false, and text after Helm removes contact details and account numbers (and, in Compliance mode, the listed protected terms). The as-of date is there under the model’s own name for it. A fact the record left out is not sent, and neither is its outcome key.
  • Helm-Request-Id names the request: it is the signature’s sub.

Checking the signature

  • Helm-Signature is an HS256 JWS, a JWT signed with your endpoint’s secret: the whole hmsec_… string is the key. Its kid is the secret’s first 14 characters, as the API page shows it.
  • Claims: iss (helm-express), aud (your endpoint’s id), sub (the Helm-Request-Id), jti (fresh on every request), iat, exp (five minutes after iat), url (the address it was sent to) and bh, the hex SHA-256 of the raw body. Hash the bytes as they arrived, before any JSON parse.
  • Refuse a request whose signature does not check, whose aud is not your endpoint’s id, whose bh is not its body’s hash, or whose exp has passed. To refuse a replay too, keep each jti until its exp and refuse one you have seen.

The code below needs nothing installed. Call it with the header, the raw body, your secret and your endpoint’s id; it returns the claims, or raises (throws) when the request is not Helm’s.

In Python:

import base64
import hashlib
import hmac
import json
import time


def _b64url(part):
    return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))


def verify_signature(header, body, secret, endpoint_id, url=None, now=None, leeway=60):
    """Check a Helm-Signature header against the raw request body: the bytes, before
    any JSON parse. Returns the claims; raises ValueError if the request is not Helm's."""
    if isinstance(body, str):
        body = body.encode("utf-8")
    now = time.time() if now is None else now
    key = secret.encode("utf-8")
    bh = hashlib.sha256(body).hexdigest()
    for jws in header.split(","):
        parts = jws.strip().split(".")
        if len(parts) != 3:
            continue
        try:
            head = json.loads(_b64url(parts[0]))
            claims = json.loads(_b64url(parts[1]))
            given = _b64url(parts[2])
            signing = (parts[0] + "." + parts[1]).encode("ascii")
        except ValueError:
            continue
        signed = hmac.new(key, signing, hashlib.sha256).digest()
        if not isinstance(head, dict) or head.get("alg") != "HS256":
            continue
        if not hmac.compare_digest(signed, given):
            continue
        if not isinstance(claims, dict) or claims.get("iss") != "helm-express":
            raise ValueError("not issued by Helm")
        if claims.get("aud") != endpoint_id:
            raise ValueError("signed for another endpoint")
        exp, iat = claims.get("exp"), claims.get("iat")
        if not isinstance(exp, int) or exp + leeway < now:
            raise ValueError("expired")
        if not isinstance(iat, int) or iat - leeway > now:
            raise ValueError("issued in the future")
        if claims.get("bh") != bh:
            raise ValueError("signed over another body")
        if url is not None and claims.get("url") != url:
            raise ValueError("signed for another URL")
        return claims
    raise ValueError("no valid Helm-Signature")

In TypeScript, on Node 18 or later:

import { createHash, createHmac, timingSafeEqual } from "node:crypto";

export type HelmClaims = {
  iss: string; aud: string; sub: string; jti: string;
  iat: number; exp: number; bh: string; url: string;
};

/** Check a Helm-Signature header against the raw request body (the bytes, before any JSON
 *  parse). Returns the claims; throws when the request is not Helm's. */
export function verifySignature(
  header: string,
  body: string | Uint8Array,
  secret: string,
  endpointId: string,
  opts: { url?: string; now?: number; leeway?: number } = {},
): HelmClaims {
  const now = opts.now ?? Date.now() / 1000;
  const leeway = opts.leeway ?? 60;
  const bh = createHash("sha256").update(body).digest("hex");
  const refuse = (why: string): never => {
    throw new Error(why);
  };
  for (const jws of header.split(",")) {
    const parts = jws.trim().split(".");
    if (parts.length !== 3) continue;
    const [h, p, s] = parts as [string, string, string];
    let head: { alg?: unknown } | null;
    let claims: Partial<HelmClaims> | null;
    try {
      head = JSON.parse(Buffer.from(h, "base64url").toString("utf8"));
      claims = JSON.parse(Buffer.from(p, "base64url").toString("utf8"));
    } catch {
      continue;
    }
    const signed = createHmac("sha256", secret).update(`${h}.${p}`).digest();
    const given = Buffer.from(s, "base64url");
    if (head?.alg !== "HS256" || given.length !== signed.length) continue;
    if (!timingSafeEqual(given, signed)) continue;
    if (claims?.iss !== "helm-express") refuse("not issued by Helm");
    const c = claims as HelmClaims;
    if (c.aud !== endpointId) refuse("signed for another endpoint");
    if (typeof c.exp !== "number" || c.exp + leeway < now) refuse("expired");
    if (typeof c.iat !== "number" || c.iat - leeway > now) refuse("issued in the future");
    if (c.bh !== bh) refuse("signed over another body");
    if (opts.url !== undefined && c.url !== opts.url) refuse("signed for another URL");
    return c;
  }
  throw new Error("no valid Helm-Signature");
}

Any JWT library does the same if it pins the algorithm to HS256, the issuer to helm-express and the audience to your endpoint’s id; then compare bh yourself.

Your reply

Answer 200 with JSON: one answer, to the one question, under the model’s short name.

{"answers": {"late_orders": {"type": "noul", "noul": 0.72}}}

# a choice: a chance for every answer id; choice, if sent, is the likeliest
{"answers": {"carrier": {"type": "choice", "choice": "sea",
  "probabilities": {"air": 0.2, "sea": 0.7, "road": 0.1}}}}
  • Yes or no: noul is the chance of true, from 0 to 1.
  • A choice: probabilities gives every answer id a chance from 0 to 1, together summing to 1 within 0.02. choice, if you send it, must be the likeliest of them; confidence is kept as well.
  • Helm keeps type, noul, choice, confidence and probabilities of the answer, and nothing else of the reply.
  • A redirect, a status other than 2xx, a reply over 256 KiB, one that is not JSON, or one that does not answer exactly the question asked fails the whole call. Helm does not retry a request.

Limits

One request20 s, connecting included. Past it, the call fails as timed_out.
One call60 s for every record of the call, waiting included. Past it, the call fails as deadline, or endpoint_busy while a record still waits its turn.
At once, per call4 requests.
At once, per endpoint8 requests in flight from each of Helm’s engine machines, across every call. A request waits its turn inside its call’s 60 s.
A reply256 KiB at most.
Tests10 a minute per workspace. Past it, 429 rate_limited with Retry-After; nothing is sent.

Helm runs several engine machines, each with its own count, so more than 8 requests can reach your server at once. Limit what your server takes as well: when it is full, answer 429 or 503, and the call fails with nothing sealed or charged.

Price

Each forecast is billed per input token at $0.10 per million: its facts and the question, counted as Helm counts its own. Output is free, a test is free, and a call that fails is not charged.

When a call fails

  • 502 endpoint_bad_reply: Your server answered, and not with an answer to every record in time: an error status, a redirect, a reply too large, not JSON or not the answer asked, a request past 20 s, or the call past 60 s. reason says which, index names the record, and status is the status your server answered.
  • 503 endpoint_unavailable: Helm could not ask your server: no connection could be made (its name, its address, TLS), Helm’s requests to it were all in flight for other calls (endpoint_busy), the endpoint was revoked, or answers from your own model are not on for the workspace. A wait carries Retry-After.

In both, nothing is sealed and nothing is charged, and Helm’s own model is never asked in its place.

Next