touchmαrkdocstouchmark.ai

Quickstart

From nothing to a running integration in about ten minutes: you'll open a session, emit an event, and set up the stream that delivers its valuation. Scoring runs out-of-band, so valuations arrive on that stream once your application is provisioned — your Touchmark contact configures that during onboarding (step 0). For the full surface see the API reference; for failure handling, Errors & recovery.

0. Get your key

During the private beta we set up each account by hand. Email whoever onboarded you (your Touchmark contact) and they'll get you a key — usually the same day. Save it as TOUCHMARK_API_KEY, then continue to step 1.

1. Install

npm install @touchmark/sdk

During the private beta we'll give you access to the package directly (the install command may differ, but the import — @touchmark/sdk — is always the same), and we'll configure your application with you: its event-type schemas and the evals that score it, so the platform knows how to validate and price what you emit.

2. Construct the client

One client per backend service; reuse it. The api_key authenticates you and identifies your application — there's nothing else for the client to configure.

import { Touchmark } from "@touchmark/sdk";

const tm = new Touchmark({ api_key: process.env.TOUCHMARK_API_KEY! });

3. Open a session

A session is one pricing context — typically per project, document, or conversation. Identify it with your own durable scope_id; Touchmark returns a session_id for it.

const scope_id = "project-42"; // your id for this scope — persist this, not the session_id
const user_id = "user-7";

// `let`, not `const`: session-expiry recovery (step 6) swaps in a fresh session_id.
let { session_id } = await tm.session.start({ scope_id, user_id });

start is idempotent on scope_id: call it again any time (after a restart, say) and you get the current session back. So scope_id is the only handle you need to keep.

4. Emit events

Emit events as your app produces them. emit is fire-and-forget — it returns nothing and you never await it on your user's critical path. Give each event an event_idunique within the session, also its idempotency key (reuse it only to retry the same event; a different event needs a different id) — and an event_idx (a per-session counter, contiguous from 0; reset it to 0 when you open a new session — see step 6).

let event_idx = 0; // per-session, contiguous; reset to 0 when you re-open the session (step 6)

tm.session
  .emit({
    session_id,
    event_id: tm.newEventId(), // or your own id, unique within the session
    event_idx: event_idx++,
    event_type: "model_output",
    payload: { text: "…what your model produced…" }, // a minimal model_output shape — your exact schema is set up at onboarding
    base_price_usd: 0.02, // set to price this event; omit for a context event
  })
  .catch((err) => console.error("touchmark emit failed", err)); // never blocks your hot path; stash & retry in production

Set base_price_usd to price an event (it's your current price; Touchmark returns an adjustment). Omit it for a context event — something the model should be scored with but that settles no money on its own (e.g. emit the user's prompt as context so the model's output is scored against what was asked).

For crash-safe retries: persist the event_id with your event before you emit — then a retry after a crash reuses the same id and the server de-duplicates. Minting it inline (as above) is fine for the happy path but loses the id if you crash mid-emit. The emit reference example shows the full resilient pattern (recovery + outbox).

5. Consume valuations

Valuations never come back on emit — scoring runs out-of-band. They arrive on a separate stream you run in its own loop, one per session, alongside your emits. In a multi-instance backend you must keep it to one consumer per session yourself — see Run exactly one consumer per session.

for await (const v of tm.session.streamValuations({ session_id })) {
  // billing is your own system — set the event's price to the absolute fair price Touchmark computed.
  billing.setEventPrice(v.event_id, v.fair_price_usd);
  // v.delta_usd is the change vs. the previous price (+surcharge / −refund) — for display.
}

A few things this loop guarantees:

  • Idempotent by construction. You write an absolute price, so re-applying the same valuation just re-writes the same number — a no-op. A reconnect re-delivers the tail of a session (at-least-once), and that re-delivery can't double-charge.
  • Gap-proof per event. Each fair_price_usd is the absolute price for one event_id, and the base_price_usd you supplied is that event's initial valuation — so the event is priced from the moment you emit it. If a valuation is dropped, the next valuation of that same event carries the full fair_price_usd and overwrites it; if none ever arrives, the base price simply stands. Either way there's no running total to keep contiguous and nothing to reconcile. delta_usd rides along only as the +surcharge / −refund to show your user. (See money model.)

The copy-pasteable version — the loop, fatal-error handling, and the billing seam in one tested file — is the consumer reference example.

6. Handle session expiry

Sessions lapse after about a week with no emits (consuming the valuation stream doesn't reset the clock); the next emit then throws session_closed. The fix is always: re-open the scope and re-emit with the same event_id. The re-opened session is new, so its counter restarts — re-emit this event as event_idx: 0 and reset your counter.

import { TouchmarkError } from "@touchmark/sdk";

try {
  await tm.session.emit(/* … */);
} catch (err) {
  if (err instanceof TouchmarkError && err.code === "session_closed") {
    ({ session_id } = await tm.session.start({ scope_id, user_id }));
    event_idx = 0; // new session — restart the per-session counter
    await tm.session.emit(/* … same event_id, event_idx … */);
  }
}

You write this wrapper once. The copy-pasteable version — fire-and-forget, recovery, and an outbox in one tested file — is the reference example.

Putting it together

The minimal shape, for illustration — it leaves out the recovery you'd run in production. For copy-paste-ready code with session_closed recovery, an outbox, and dead-lettering, use the tested reference examples (emit, consume).

import { Touchmark } from "@touchmark/sdk";

const tm = new Touchmark({ api_key: process.env.TOUCHMARK_API_KEY! });
const scope_id = "project-42";
const user_id = "user-7";

// Open the session and start consuming valuations for it (one loop per session).
const { session_id } = await tm.session.start({ scope_id, user_id });
// This loop never returns — it long-polls for the life of the session — so it runs in its own task
// alongside your emits. In production pass an AbortSignal (the `signal` option) to stop it on
// shutdown; see examples/consume.ts.
void (async () => {
  for await (const v of tm.session.streamValuations({ session_id })) {
    billing.setEventPrice(v.event_id, v.fair_price_usd); // billing is your own system; absolute price, delta_usd display-only
  }
})().catch((err) => console.error("touchmark valuation stream stopped", err));

// Emit events as they happen — fire-and-forget.
tm.session
  .emit({
    session_id,
    event_id: tm.newEventId(),
    event_idx: 0,
    event_type: "model_output",
    payload: { text: "…what your model produced…" },
    base_price_usd: 0.02,
  })
  .catch((err) => console.error("touchmark emit failed", err));

streamValuations runs until the session ends or you abort it (pass an AbortSignal — see the API reference and consume); in a throwaway script it will simply keep polling, which is why this snippet never calls process.exit.

Next steps

  • API reference — every option, method, and type.
  • Errors & recovery — what each error means and how to handle it, including degraded mode (what happens when Touchmark is unreachable).
  • Reference example: emit — production-grade emit with recovery + outbox, tested in CI.
  • Reference example: consuming valuations — the stream loop that applies fair prices to your bill, tested in CI.