SDK 2.1: platform toolkit + vesting engine
Concept · Observability

Export bounded signals from every process

Choose the Node or web binding, initialize once, and emit only fields approved by the kernel allowlist.

v2.1.0 stable
SDK 2.1 is the stable release on the latest dist-tag. Install it with pnpm add @tokenops/sdk. The receipt-free and Safe create surface of /fhe-airdrop remains @beta.

The observability subpaths provide a process-level exporter for structured logs, Sentry traces and errors, metrics, and alert webhooks. Install zod@^4.1.11 in every environment. The Sentry bindings are optional peers: @sentry/node@^11.4.0 for API, worker, and indexer processes, or @sentry/nextjs@^11.4.0 for the web app.

Choose an environment entry#

ImportUse it forLoads
@tokenops/sdk/observabilityUniversal registries, TRACE_SAMPLE, isListedSpanName, and sentryOptions.No Sentry peer
@tokenops/sdk/observability/nodeNode API, worker, and indexer processes.@sentry/node
@tokenops/sdk/observability/webThe Next.js web process.@sentry/nextjs

Do not import a process entry into shared code just to read constants; use the universal entry so the wrong Sentry SDK cannot enter that bundle.

Initialize once, then emit#

telemetry/api.ts
ts
import {
  captureError,
  count,
  flushTelemetry,
  initTelemetry,
  log,
  span,
  timing,
} from "@tokenops/sdk/observability/node";
import { newCorrelationRef } from "@tokenops/sdk/kernel";

initTelemetry({
  process: "api",
  environment: "production",
  release: "0123456789abcdef0123456789abcdef01234567",
  dsn: process.env.SENTRY_DSN ?? "",
});

const correlationRef = newCorrelationRef();
log("info", "claims.recorded", {
  correlationRef,
  component: "api",
  count: 3,
});

const result = await span(
  "claims.record-operation",
  async () => ({ accepted: true }),
  { correlationRef, operation: "record" },
);

count("claims.accepted", { component: "api", count: 1 });
timing("claims.latency", 42, { component: "api" });

try {
  await Promise.reject(new Error("database credentials must not escape"));
} catch (error) {
  captureError(error, { correlationRef });
}

await flushTelemetry(2_000);

Initialization is synchronous and process-global for that imported entry. The release must be exactly 40 lower-case hex characters, and DSN must be non-empty. A second call throws telemetry-already-initialized; any operation before initialization throws telemetry-not-initialized. Worker initialization also requires alertWebhookUrl.

telemetry/web.ts
ts
import {
  initTelemetry,
  span,
} from "@tokenops/sdk/observability/web";

initTelemetry({
  process: "web",
  environment: "testnet",
  release: "0123456789abcdef0123456789abcdef01234567",
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN ?? "",
});

export function loadSigningReview(): Promise<Response> {
  return span(
    "signing.review",
    () => fetch("/api/signing-review"),
    { component: "web", route: "signing-review" },
  );
}

Names and fields are closed by default#

Log and metric names must contain at least two dot-separated lower-case segments, use only letters, digits, and hyphens inside each segment, and stay at or below 128 characters. Invalid names throw telemetry-name-invalid.

Every field passes the kernel's FIELD_CLASSES allowlist. Unknown names and non-primitive values are dropped and increment telemetry.field-dropped. If a person id is present, public-address fields are dropped too. This applies to logs, span attributes, metric tags, Sentry extras, and Sentry tags.

  • log writes one JSON line through the supplied writer (stdout by default). Sentry log delivery is disabled.
  • count increments by one; put bounded dimensions in its tags, not the measurement value.
  • timing refuses negative values and emits a millisecond distribution.
  • flushTelemetry(timeoutMs)resolves the Sentry SDK's boolean flush result.

Span registry and sampling#

telemetry/options.ts
ts
import {
  TRACE_SAMPLE,
  isListedSpanName,
  sentryOptions,
} from "@tokenops/sdk/observability";

const options = sentryOptions({
  process: "worker",
  environment: "production",
  release: "0123456789abcdef0123456789abcdef01234567",
  dsn: "https://public@example.ingest.sentry.io/1",
});

const productionRate = TRACE_SAMPLE.production; // 0.1
const allowed = isListedSpanName("jobs.reconcile-drafts"); // true
const refused = isListedSpanName("database.password-query"); // false

SPAN_NAMES is the fixed operation registry; jobs.* and dynamic.*are the only open families, with a bounded suffix. In testnet, span throws span-name-unlisted for an unlisted name. In mainnet-test and production it runs the function without a trace and counts telemetry.span-unlisted.

EnvironmentTrace sample rate
testnet1.0
mainnet-test1.0
production0.1

Sentry-created spans outside the registry are renamed to <process>.segment or <process>.auto. Browser/resource/measure spans and Replay are disabled.

Error capture protects the original message#

captureError never sends the original error message. An AppError becomes a new error named by its bounded code with code, category, and correlation ref tags; an unknown error becomes internal-error and unavailable. Capture failures are swallowed and counted as telemetry.delivery-failed.

Generated Sentry options also remove user, request, breadcrumb, message, log-entry, cookies, headers, query strings, request bodies, database queries, queue payloads, stack variables, GraphQL contents, and GenAI inputs/outputs. Non-code exception text is replaced with internal-error.

Worker alert delivery#

worker/alerts.ts
ts
import {
  initTelemetry,
  notifyAlert,
} from "@tokenops/sdk/observability/node";

initTelemetry({
  process: "worker",
  environment: "production",
  release: "0123456789abcdef0123456789abcdef01234567",
  dsn: process.env.SENTRY_DSN ?? "",
  alertWebhookUrl: process.env.ALERT_WEBHOOK_URL,
});

await notifyAlert({
  rule: "relayer_stalled",
  scope: "production:1",
  state: "firing",
  transitionId: "relayer_stalled:production:1:42",
  environment: "production",
  chain: 1,
  reason: "consecutive_failures",
  runbook: "O12 RB3",
});

Notifications are strict: scope is platform with a null chain or <environment>:<chain> with the same environment and positive chain id. The webhook receives the transition id as its idempotency key. Each attempt times out after five seconds; failures retry after 1, 2, and 4 seconds.

Exhaustion logs alert.delivery-failed, counts a delivery failure, then throws retryable alert-delivery-failed. Callers still own job retry and escalation policy.

See also