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_id — unique 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_usdis the absolute price for oneevent_id, and thebase_price_usdyou 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 fullfair_price_usdand 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_usdrides 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-gradeemitwith recovery + outbox, tested in CI. - Reference example: consuming valuations — the stream loop that applies fair prices to your bill, tested in CI.