Usage metering
September 19, 2026 · View on GitHub
Kody records per-user usage events at runtime chokepoints for cost attribution,
admin cohort views, and abuse detection. This document describes the event
schema, the recordUsage() helper contract, which chokepoints are instrumented,
and the recipe for instrumenting a new chokepoint.
Deliberately out of scope: quotas, plans, billing, enforcement, and admin dashboards (see Entitlements for plan limits). This document covers event capture and rollups only.
Per-user isolation
Usage metering follows the repo-wide isolation invariant: every event carries a
required userId, the Analytics Engine index is the userId, and the D1 rollup
table is keyed by user_id. Admin and account reads stay scoped to one user.
The event schema
One schema covers every chokepoint. It is defined in
packages/worker/src/usage/record-usage.ts:
type UsageEvent = {
userId: string // required; owning user
eventType: UsageEventType // see the metric table below
entityId?: string | null // metered entity id when one exists
durationMs?: number | null // wall-clock duration of the metered unit
cpuMs?: number | null // CPU time, only when the platform exposes it
bytes?: number | null // bytes moved/stored when meaningful
eventCount?: number // coalesced units in one write; defaults to 1
outcome: 'success' | 'error'
timestamp?: string // ISO 8601; defaults to time of recording
surface?: string | null // closed UWD surface; AE blob6
executeShape?: string | null // execute thin/glue class; AE blob7
cacheReuse?: 'hit' | 'miss' | null // billing-aligned LOADER reuse; AE blob8
codeChars?: number | null // module-graph text length; AE double4
paramsChars?: number | null // stable JSON length of params; AE double5
}
eventType is a closed union defined in the dependency-free
packages/worker/universal/usage-event-types.ts (re-exported by
record-usage.ts). Add new members there (never ad hoc strings at call sites)
so the set of metrics stays reviewable in one place. The feature-flag registry
declares flag success metrics against this union, and the flag-exposure stream
(FLAG_EXPOSURES dataset, see
feature-flags.md) is joined with these
events for the admin on/off cohort readout.
Metrics and their chokepoints
eventType | Metered unit | Recorded at | entityId |
|---|---|---|---|
execute | one MCP execute-tool sandbox evaluation | packages/worker/src/mcp/executor.ts (execute), only when the run surface is execute | none |
package_export | one saved-package bundled-code run | packages/worker/src/mcp/run-kody-registry.ts (bundled runs with a package context) | package id |
package_static_call | one call of a statically imported package export (function-valued, incl. default) | sandbox-side wrapper stamped by the bundler; validated and recorded host-side by packages/worker/src/usage/package-static-call-usage.ts (wired in run-kody-registry.ts) | callee package id |
job_run | one job execution | packages/worker/src/jobs/service.ts (executeJobOnce) | job id |
workflow_run | one Cloudflare Workflow run | packages/worker/src/package-runtime/package-workflows.ts (DynamicCallableWorkflow.run) | workflow instance id |
realtime_session | one realtime websocket session | reserved — not instrumented | session id |
outbound_fetch | one outbound fetch through the gateway | packages/worker/src/mcp/fetch-gateway.ts (KodyFetchGateway.fetch) | request host |
email_send | one outbound email send attempt | packages/worker/src/email/outbound.ts (sendOutboundEmail) | email message id |
email_received | one inbound receive attempt for a routed inbox | packages/worker/src/email/inbound.ts (handleInboundEmail, after inbox resolution) | email message id (when stored) |
dynamic_worker_day | first use of one Dynamic Worker id on a UTC day | packages/worker/src/mcp/executor.ts after createStableDynamicWorkerId (sandbox surfaces) and packages/worker/src/package-runtime/package-app.ts (APP_LOADER with a stable id); uniqueness via UserMeter.claimDynamicWorkerDay. Each event carries surface (Analytics Engine blob6). | worker id |
dynamic_worker_invoke | one LOADER evaluate (hit or miss) on the execute-sandbox path | packages/worker/src/mcp/executor.ts after claimDynamicWorkerDay, on every signed-in sandbox surface (execute, job, package_export, workflow, …). Observe-only. cacheReuse is miss when claimDynamicWorkerDay.created === true and hit otherwise. Also carries codeChars, paramsChars, durationMs, surface, and executeShape when known. No worker id, source, params, or package names. | none |
durable_object_gb_seconds | one typed per-user Durable Object RPC burst (wall-clock in durationMs; admin converts to GB-s at 128 MB). Same-outcome RPCs in one request coalesce into a single Analytics Engine point whose eventCount is the RPC count. | createMeteredDurableObjectStub on storageRunnerRpc when USAGE_EVENTS is bound. Other per-user RPC factories can adopt the same helper; UserMeter, Mailbox, RunLog, and RepoSessionIndex stay unwrapped so admin usage reads do not inflate the metric. Observe-only / unmetered: excluded from fleet event-count rankings, entitlement-pressure candidate selection, and customer usage emails. Never billed. | DO class name |
durable_object_rows_read | StorageRunner SQLite rowsRead from one sqlQuery (skipped when rowsRead < 1). Same-outcome bursts coalesce; hourly rollups recover the unit count from Analytics Engine double3. | recordDurableObjectRowsRead in StorageRunner.sqlQuery when USAGE_EVENTS is bound. Other customer Durable Objects are not instrumented, so this meter undercounts rather than overcharges. Observe-only for fleet event-count rankings and entitlement-pressure selection; monthly overage math and compute-include warning emails still read the rollup. | DO class name |
email_received covers receive attempts once an inbound message is routed to a
known, enabled inbox: stored messages record success; unverified-account
rejections, size rejections, entitlement rejections, and parse failures record
error. The bytes field always carries the raw message size, including for
rejected mail. Mail rejected before inbox resolution (unknown alias, disabled
inbox) has no owning user and is not metered.
createMeteredDurableObjectStub must not wrap the RpcStub as the Proxy target.
Cloudflare RPC binds stub methods to the receiver; a Proxy-of-stub is not a
valid RPC receiver, so every StorageRunner call throws "Proxy could not be
serialized because it is not a valid RPC receiver type". That includes
packageStorage() get of a missing key on export, subscription, job, and
execute — storageRunnerRpc is the only production call site, so one broken
wrapper fails every surface that touches package storage. The wrapper is a
plain-object Proxy that Reflect.get / Reflect.applys against the original
stub. Local and workers-unit env omit USAGE_EVENTS, which skips the wrapper;
tests that need the production path bind a stub Analytics Engine dataset.
package_static_call: statically imported package export calls
Static imports (import fn from 'kody:@scope/pkg/export') are the default way
package code is reused, so calls through them are metered per call:
- Metered unit: one call of a function-valued export (named or default) that
was statically imported from a saved package. Non-function exports pass
through unwrapped; imports that are never called record nothing.
userIdis the user the host run executes as,entityIdis the callee package id,durationMsis the wall time of the call (through settlement for async functions), andoutcomeiserroriff the call threw or rejected — the error still propagates to the caller unchanged. - Provenance is bundler stamping, never sandbox strings. The import
rewriter's proxy module (
ensurePackageProxyinpackages/worker/src/package-runtime/module-graph-import-rewriting.ts) knows which saved package akody:@…specifier resolved to and bakes that id into the generated proxy (createMeteredPackageImportProxySource), which wraps function exports with__kodyMeterStaticPackageExportfrom the virtual runtime module — the same stamping discipline as per-packagepackageStorage()routing. Root self-imports are not stamped (the run already recordspackage_exportfor that package). - Host-side validation (trust model, stated honestly): stamped ids ride in
generated code, but that code still executes inside the sandbox realm, so a
malicious module could forge reports. The host
(
createPackageStaticCallMeterToolsinpackages/worker/src/usage/package-static-call-usage.ts) only records events whose stamped id is in the bundle's static dependency package ids recorded at build time (bundle.dependencies) — a strict subset of thepackageStorage()grant set, which additionally grants the run's own package id and dynamic-import dependencies, neither of which the bundler ever stamps into a metered proxy — and silently drops the rest with a debug log. Forgery can therefore at worst inflate counts for packages the bundle already statically depends on. - Delivery never blocks the call path. Each call does one synchronous buffer
push (capped at 200 events per run — Analytics Engine allows 250
writeDataPointcalls per invocation, one per event, and the run's other usage events need headroom; overflow is dropped and the cap is enforced host-side too); the run wrapper inrunBundledModuleWithRegistryflushes the buffer over a runtime bridge in batches at the end of the run, while the sandbox RPC dispatchers are still live — a fire-and-forget RPC per call would race dispatcher teardown and lose events. The awaited flush is bounded by a short timeout so a slow bridge never owns the run's tail latency, and it loops (bounded) so async calls that settle during an in-flight flush are still delivered; calls that have not settled when the run's entrypoint finishes (a promise the run never awaited) are not metered — metering never extends a run's lifetime, and such a dangling promise may never settle inside the sandbox at all. Flush failures are swallowed. - Coverage: every surface that funnels through
runBundledModuleWithRegistry(ad hoc execute with statickody:@…imports, saved-package invocations, and the job/workflow/service runs built on them). Package-app fetch handlers use a separate runtime bridge and do not bind the meter; the wrapper no-ops there. - Capacity note: the 200-events-per-run cap shares the ~250
writeDataPoint-per-invocation Analytics Engine budget with every other usage event in the same invocation — in particularoutbound_fetch, which also scales per operation. A heavy run (many static calls and many gateway fetches) can exceed the budget and Analytics Engine silently drops the overflow points. The eventual fix is aggregation (coalescing per-callee counts/durations into fewer data points), not raising the cap.
Nesting: metrics are independent, do not sum across types
Execution surfaces nest. A package job funnels through the bundled-module runner
and the sandbox executor, so it produces one job_run and one package_export.
It does not also emit execute — that metric is MCP execute tool work
only, matching execute_calls_per_day and first_execute_at. A bundled
execute-tool run that calls statically imported package exports still produces
one package_static_call per call inside that run's execute span. Each
metric answers its own question (execute is ad-hoc execute-tool volume;
job_run is job activity; package_export is saved-package entrypoints;
package_static_call is per-callee reuse). dynamic_worker_day is the
Cloudflare bill unit: one unique Dynamic Worker id per user per UTC day, on
every sandbox surface that creates a worker. dynamic_worker_invoke is the
observe-only reuse meter: one event per execute-sandbox LOADER evaluate,
including later claims of the same id on the same UTC day. Do not add it to
entitlements or fleet event-count rankings.
PlanLimits.maxUniqueWorkerDaysPerMonth is the public included allotment shown
on /pricing. It is not in entitlementResources and does not replace the hard
daily execute / job_run caps, or the public-ladder weekly execute and
outbound-fetch windows. usageGet and the account usage UI report this meter
(and Durable Object rows-read) with whatCounts / howToReduce so the include
is self-explanatory. When unique-worker-day pressure is hot (limit denial or
over 80%), those payloads also include a short mechanic line: meter name plus
what a unique worker day is. Agent-facing package docs do not repeat the cost
model; see Platform efficiency.
Unique worker days by surface
Every dynamic_worker_day event carries a closed surface tag (Analytics
Engine blob6): execute, job, package_export, workflow, subscription,
app_fetch, app_realtime, retriever, webhook, or unknown.
package_export is the usage name for run-record surface export. Call sites
pass the mapped surface; unknown is only for a missing mapping. When the
registry does not own the run record (keyed package invocations, inline
workflows), callers pass runSurface so UWD is not inferred as
package_export.
D1 usage_rollups stay keyed by (user_id, metric, month) — the monthly total
is unchanged. Surface share is an Analytics Engine query. Hourly aggregation
does not write a second rollup dimension. Use
buildUniqueWorkerDayBySurfaceQuery in
packages/worker/src/usage/aggregate-rollups.ts:
SELECT
if(blob6 = '', 'unknown', blob6) AS surface,
sum(_sample_interval) AS unique_worker_days
FROM kody_usage_events
WHERE timestamp >= toDateTime('2026-09-01 00:00:00')
AND timestamp < toDateTime('2026-10-01 00:00:00')
AND blob2 = 'dynamic_worker_day'
GROUP BY surface
ORDER BY unique_worker_days DESC
Empty blob6 means the event has no surface tag. Preview uses
kody_usage_events_preview. Execute share for the month is
sumIf(_sample_interval, blob6 = 'execute') / sum(_sample_interval) on that
same filter.
Ad hoc execute events may also carry executeShape on blob7
(thin_single_export | thin_few_exports | glue): a host-side best-effort
class of the caller-authored source string. It is not used for billing and is
not shown to agents. Unparseable source omits the field.
Dynamic Worker reuse (hit vs miss)
Every execute-sandbox LOADER evaluate records dynamic_worker_invoke after
claimDynamicWorkerDay, including when the claim returns created: false.
cacheReuse is billing-aligned: miss on the first (user, workerId, UTC day)
claim, hit on later claims of that id the same day. The event also carries
durationMs (sandbox evaluate wall-clock), codeChars (total character length
of the module-graph text hashed into the id), surface, executeShape when the
run classified one, and paramsChars (character length of a key-sorted JSON
serialization of params; 0 when params is omitted, null, a non-object,
an array, or empty {} — not 2 from stringifying {}). Payloads never include
source, param keys or values, package names, or worker ids.
D1 usage_rollups for dynamic_worker_invoke is the monthly invoke count (and
summed duration). Hit rate and average paramsChars are Analytics Engine
queries. Use buildDynamicWorkerInvokeReuseQuery and
buildDynamicWorkerReuseRatioQuery in
packages/worker/src/usage/aggregate-rollups.ts:
SELECT
if(blob8 = '', 'unknown', blob8) AS cache_reuse,
if(blob6 = '', 'unknown', blob6) AS surface,
sum(_sample_interval) AS invokes,
sum(double1 * _sample_interval) / sum(_sample_interval) AS avg_duration_ms,
sum(double4 * _sample_interval) / sum(_sample_interval) AS avg_code_chars,
sum(double5 * _sample_interval) / sum(_sample_interval) AS avg_params_chars
FROM kody_usage_events
WHERE timestamp >= toDateTime('2026-09-01 00:00:00')
AND timestamp < toDateTime('2026-10-01 00:00:00')
AND blob2 = 'dynamic_worker_invoke'
GROUP BY cache_reuse, surface
ORDER BY invokes DESC
Fleet hit rate is
sumIf(_sample_interval, blob8 = 'hit') / sum(_sample_interval) on
blob2 = 'dynamic_worker_invoke'. Unique-day yield is
dynamic_worker_day / dynamic_worker_invoke (or / execute for execute-tool
volume). Same-user same-graph executes that vary only params reuse one worker
id; execute-surface hit rate and unique-day yield are the fleet readouts for
that reuse.
APP_LOADER package-app isolates record UWD (dynamic_worker_day) when the
worker id is stable (app_fetch / app_realtime). They do not emit
dynamic_worker_invoke. One-off APP_LOADER.load() without a hashed id has no
worker id to claim and does not emit dynamic_worker_day.
LOADER worker identity
createStableDynamicWorkerId in packages/worker/src/mcp/dynamic-worker-id.ts
mints the execute-sandbox worker id. The hash is the sandbox-contract version
(dynamicWorkerCacheKeyVersion, bumped only when the executor or identity
contract changes), the LOADER binding name, the acting userId and
storageContext, compatibility date/flags, the main module name, and the module
graph (agent code plus the generated executor harness).
Timeout, allowOutboundFetch, and the excluded fetch hostname are baked into
that harness text, so they are not hashed again. Execute params,
packageContext, live MCP connect/tool metadata, and the unstamped
packageSecrets binding (present only when that evaluate's package id is set)
arrive on evaluate RPC and are not part of the key: the same user and module
graph reuse one isolate when only those per-call values change. Deploy SHA
(APP_COMMIT_SHA), email, and other request-only gatewayProps fields are also
omitted, so a parent deploy remints ids only when the harness or module graph
actually changes.
userId and storageContext stay in the key because LOADER.get reuses the
first factory's WorkerCode for a given id, including the KodyFetchGateway
globalOutbound stub. Those props authorize outbound fetch, secrets, and quota,
so two users (or two storage contexts) with the same module graph still get
distinct ids. UWD metering stays (userId, workerId, day) via
claimDynamicWorkerDay and is independent of this hash.
When modules are not deterministically hashable, the id is a UUID and is not
reused. Hashable modules still produce a stable id when userId is null or
APP_COMMIT_SHA is unset.
Customer-facing monthly overage is unique worker days plus Durable Object
rows-read, billed on the public ladder at computeOverageRatesUsd when
compute-overage-charging is on. Unpaid Free is a soft-block, not a charge.
Amounts below Stripe's $0.50 USD minimum are not invoiced. Legacy Standard/Pro
is warned, not billed. Never add durations across different eventType values —
that double counts nested layers. Within one eventType, each chokepoint
records exactly one event per metered unit, so sums are safe.
Admin usage and insights convert dynamic_worker_day counts to a gross
Cloudflare estimate (unique days × \$0.002). The 1,000 included unique
worker-days per month are account-wide, so per-user dollars are not a net bill
share. Worker ids stay inside UserMeter and Analytics Engine entityId; account
export does not list them.
Sinks
recordUsage() picks its sink by environment:
-
Workers Analytics Engine is the write path in production and preview (
USAGE_EVENTSdataset binding, configured for theproductionandpreviewenvironments inpackages/worker/wrangler.jsonc). When the binding is present, each event is one non-blockingwriteDataPointcall and nothing else — a per-event D1 upsert would serialize every metered request (execute, fetch, email, jobs, ...) on D1's single writer. Data point layout:indexes:[userId]blobs:[userId, eventType, entityId ?? '', outcome, timestamp, surface ?? '', executeShape ?? '', cacheReuse ?? ''](surfaceis blob6,executeShapeis blob7,cacheReuseis blob8; all empty when unset)doubles:[durationMs ?? 0, cpuMs ?? 0, bytes ?? 0, codeChars ?? 0, paramsChars ?? 0]. Coalesceddurable_object_gb_secondsanddurable_object_rows_readpoints store the coalesced unit count in the third double instead of bytes so hourly rollups can recoverevent_count.codeCharsis double4 (module-graph text length ondynamic_worker_invoke).paramsCharsis double5 (key-sorted JSON length of evaluateparams; 0 whenparamsis omitted,null, a non-object, an array, or empty{}).
Analytics Engine is the analysis store (sampling-tolerant, high cardinality). Do not build enforcement on it.
-
D1
usage_rollupsis a derived aggregate (defined inpackages/worker/migrations/0001-squashed-init.sql): per-user, per-metric, per-month counters:- key:
(user_id, metric, month)wheremetricis theeventTypeandmonthis the UTCYYYY-MMprefix of the event timestamp - counters:
event_count,error_count,total_duration_ms,total_cpu_ms,total_bytes
In production/preview the rows are recomputed hourly by
aggregateUsageRollupsinpackages/worker/src/usage/aggregate-rollups.ts(gated byshouldRunUsageAggregationCronin the scheduled handler).kody-jobsfires cron and forwards to originJobsHost, which runs this lane: it queries the Analytics Engine SQL API for the current UTC month grouped by user and metric (weighting by_sample_interval, since Analytics Engine samples under load) and batch-upserts absolute values — an idempotent recompute, not increments. Analytics Engine retention (~90 days) always covers a full month, so month-to-date recompute is complete; prior months already in D1 stay untouched. The aggregation needsCLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_TOKENand no-ops with a debug log when either (or theUSAGE_EVENTSbinding) is missing.Local-dev direct fallback: when
USAGE_EVENTSis absent (local dev, tests),recordUsageupsertsusage_rollupsdirectly per event, so local admin pages and workers-unit tests work without Analytics Engine access.The rollup is the cheap read path for month-to-date admin and cohort views: one point lookup per user, metric, and month.
- key:
Agent package popularity (MCP instructions hint)
Separate from usage_rollups, D1 table agent_package_conversation_uses (also
defined in 0001-squashed-init.sql) tracks distinct conversations in which
a signed-in user’s agents used a saved package via MCP execute (package-export
runs attributed to that execute call, plus static/dynamic kody:@… deps
attributed to that execute call’s conversationId).
- Key:
(user_id, package_id, conversation_id)— upsert updateslast_used_at; the same package in the same conversation counts once. Storedconversation_idvalues are SHA-256 hex digests of the MCP conversation id (cardinality only; not reversible to the raw id). - Read path: take the user’s last N distinct agent conversations
(default 40) with
last_used_atwithin a hard max age (default 180 days), count how many of those conversations used each package (package rows must also be within max age), joinsaved_packagesforkody_id+ description, top 8 name bullets forbuildMcpServerInstructions. Cold start (no rows) omits the section. List failures (missing table, transient D1 errors) return[]so MCP init stays up. - Writes are best-effort and never throw into the invoke path (same spirit as
recordUsage). Do not widenusage_rollupsfor conversation cardinality.
Helpers live in packages/worker/src/usage/agent-package-conversation-uses.ts.
Helper contract
import { recordUsage } from '#worker/usage/record-usage.ts'
await recordUsage(env, {
userId,
eventType: 'job_run',
entityId: job.id,
durationMs,
outcome: execution.ok ? 'success' : 'error',
})
Guarantees and rules:
recordUsagenever throws and never rejects. Metering must not break the path it observes. Sink failures are logged at warn level; expected local-dev degradation is logged at debug level.- Every recorded event also emits a
kody.usage.{eventType}trace span (when Workers tracing is available) withkody.user_id,kody.event_type,kody.outcome, and the optionalkody.entity_id/kody.duration_ms/kody.bytesattributes. It nests under the active platform span, so traces become searchable by user and feature with no chokepoint changes. Span emission follows the same never-throws contract. - It accepts any object with optional
USAGE_EVENTS/APP_DBbindings (UsageEnv), so the fullEnvcan be passed directly. - Graceful degradation: in local dev and tests where the Analytics Engine
binding is not present, the event is logged via
console.debugand upserted intousage_rollupsdirectly; whenAPP_DBis missing (or the table does not exist), the rollup write is skipped with a debug log. - If
userIdis empty, the event is skipped entirely. Callers on paths that can run without a user (for example anonymous gateway fetches) must guard withif (userId)and not invent placeholder ids. - The returned promise resolves quickly (one
writeDataPoint, or one D1 upsert in local dev).awaitit inline, or pass it toctx.waitUntil(...)inside Durable Objects when the caller must not block.
Recipe: instrumenting a new chokepoint
-
Pick the metered unit. One event per semantic unit (one run, one fetch, one send). If your chokepoint already funnels through an instrumented layer, that is fine — you are adding a new metric, not replacing one — but never record the same
eventTypetwice for one unit. -
Add the
eventTypeto theUsageEventTypeunion inpackages/worker/src/usage/record-usage.tsif it does not exist, and add a row to the metric table in this document. -
Find the narrowest span where you have all of: the
userId, the entity id, the start time, and the outcome. Wrap it:const startedAtMs = Date.now() let outcome: 'success' | 'error' = 'success' try { // existing work } catch (error) { outcome = 'error' throw error } finally { if (userId) { await recordUsage(env, { userId, eventType: 'my_metric', entityId, durationMs: Date.now() - startedAtMs, outcome, }) } }A result object that carries an
errorfield (likeExecuteResult) counts asoutcome: 'error'even when nothing throws. -
Populate optional fields when cheap.
bytesfor transfer-shaped metrics,cpuMsonly when the platform exposes it. Leave fields you cannot measure asundefined— do not approximate. -
Do not change behavior. No new throws, no altered return values, no added latency beyond the awaited write (use
ctx.waitUntilin DOs if needed). -
Test it. Pick the flavor with the test flavor decision matrix. In
*.node.test.ts, spy on the helper:const usageModule = await import('#worker/usage/record-usage.ts') const recordUsageSpy = vi .spyOn(usageModule, 'recordUsage') .mockResolvedValue(undefined)Assert one call per metered unit with the expected
userId,eventType,entityId, andoutcomefor both a success and a failure path, thenrecordUsageSpy.mockRestore(). The exemplar is the usage test inpackages/worker/src/mcp/executor.node.test.ts. For*.workers.test.tssuites with a real local D1, create the table withensureUsageRollupsTestSchemafrompackages/worker/src/usage/test-schema.tsand assert onusage_rollupsrows instead.
Execute interpretable share (q)
Measurement-only Analytics Engine dataset for the share of MCP execute modules
that are interpretable pure glue (suitable for a hypothetical fixed-interpreter
tier). This is not a recordUsage() event and does not enter
usage_rollups. The classifier and write path live in
packages/worker/src/mcp/execute-interpretable.ts.
A module is the q numerator when it is pure orchestration over kody.* /
kody:runtime and has none of: kody:@ package imports, bare npm imports,
node: builtins, ambient fetch / createAuthenticatedFetch /
oauthClientCredentials, computed import(expr), or any other
non-kody:runtime specifier. Type-only imports are ignored. Unparseable source
is conservative (not interpretable). This is not an interpreter runtime.
Production dataset kody_execute_interpretable_events (preview:
kody_execute_interpretable_events_preview). Binding
EXECUTE_INTERPRETABLE_EVENTS on origin and platform (where MCP execute runs).
Schema:
| Field | Value |
|---|---|
index1 | execute_interpretable_q (shared sampling population) |
blob1 | interpretable or non_interpretable |
blob2 | glue, or first disqualifier: has_package_import, has_npm, has_node_builtin, has_fetch, has_dynamic_import, has_unsupported_import, unparseable |
double1 | 1 |
No user, source, run, or request identity. Recording is a no-op without the
binding. Query with sum(_sample_interval):
SELECT
blob1 AS class,
blob2 AS reason,
SUM(_sample_interval) AS executes
FROM kody_execute_interpretable_events
WHERE timestamp > NOW() - INTERVAL '7' DAY
GROUP BY class, reason
ORDER BY executes DESC
q = interpretable / total from that grouping, or
sumIf(_sample_interval, blob1 = 'interpretable') / sum(_sample_interval).
MCP search duration
Measurement-only Analytics Engine dataset for MCP search wall clock and the
exclusive tiles that should sum to it. This is not a recordUsage() event,
does not enter usage_rollups, and is not a UWD surface. Search-scope and
memory retrievers already mint dynamic_worker_day with surface retriever.
The write path lives in packages/worker/src/mcp/tools/search-observability.ts.
Production dataset kody_mcp_search_events (preview:
kody_mcp_search_events_preview). Binding MCP_SEARCH_EVENTS on origin and
platform (where MCP search runs). Schema:
| Field | Value |
|---|---|
index1 | mcp_search (shared sampling population) |
blob1 | success or failure |
blob2 | list, entity, or entity-batch |
blob3 | intent task name, or empty |
blob4 | trimmed or intact |
blob5 | offline or online |
double1 | durationMs |
double2 | unaccountedMs |
double3 | loadAndRankMs |
double4 | waitingItemsMs |
double5 | memoryEnrichmentMs |
double6 | rowAndRegistryLoadMs |
double7 | retrieversMs |
double8 | intent confidence (-1 when unknown) |
double9 | trimmedMatchCount |
No user, query, conversation, or request identity. Recording is a no-op without
the binding. Query with _sample_interval weights:
SELECT
quantile(0.5)(double1) AS p50_ms,
quantile(0.95)(double1) AS p95_ms,
avg(double2) AS avg_unaccounted_ms,
avg(double4) AS avg_waiting_ms,
SUM(_sample_interval) AS calls
FROM kody_mcp_search_events
WHERE timestamp > NOW() - INTERVAL '1' HOUR
Onboarding funnel
Best-effort Analytics Engine points for the signup-to-checkout funnel. Not a
recordUsage() event and not a UWD surface. First-seen stages also write the
matching users.first_*_at column so a repeat does not emit again. Recording
never throws and is a no-op without the binding.
Production dataset kody_onboarding_funnel_events (preview:
kody_onboarding_funnel_events_preview). Binding ONBOARDING_FUNNEL_EVENTS on
origin, platform, and runtime. Schema:
| Field | Value |
|---|---|
index1 | stable user id |
blob1 | stage (signup_started, first_search, checkout_completed, …) |
blob2 | MCP client family, sanitized waiting card id, or plan |
blob3 | mcp_connect_failed error class, otherwise empty |
blob4 | sanitized OAuth client id (hostname when the id is a URL) |
double1 | 1 |
No email, prompt, secret, or free-text error. /admin/insights reads unique
users per stage for 7 and 28 days with count(DISTINCT index1). Those counts
are sampled, so they are a floor. Local dev and missing credentials render the
table empty.
SELECT
blob1 AS stage,
count(DISTINCT index1) AS users
FROM kody_onboarding_funnel_events
WHERE timestamp > NOW() - INTERVAL '7' DAY
GROUP BY stage
Reading the data
- Analytics Engine: query the
kody_usage_eventsdataset (SQL API) filtered by theindex1user id; blob/double positions are listed above. Remember that Analytics Engine samples: count withsum(_sample_interval)and sum values withsum(doubleN * _sample_interval). Coalesceddurable_object_gb_secondspoints store the RPC count indouble3, so that metric'sevent_countissum(if(double3 > 0, double3, 1) * _sample_interval)andtotal_bytesstays 0. - D1:
SELECT * FROM usage_rollups WHERE user_id = ?1 AND month = ?2gives every metric for a user's month in one small scan. - Admin usage drill-down (on the admin users page):
packages/worker/src/admin/user-usage-data.tscaches its per-user rollup read model for ~5 minutes inBUNDLE_ARTIFACTS_KVvia the@epic-web/cachifiedadapter inpackages/worker/src/kv-cachified.ts(key prefixderived-cache:v1:), keyed by user id + current month, falling through to direct D1 queries when KV is unavailable. Usage is loaded for one selected account at a time, so admin reads stay O(1) per view as the user base grows. - Fleet visibility (
/admin/insights, loader inpackages/worker/src/admin/fleet-usage-insights.ts): bounded SQL overusage_rollupsfor the current UTC month — top-10 combined runtime duration (execute + job_run + workflow_run), top-10 event counts (excluding observe-onlydynamic_worker_invokeand Durable Object meters), per-metric duration leaders, and an entitlement-pressure panel that reusesreadAdminEntitlementConsumptionfor the top ~15 users by those same customer event counts, passing each row'susers.entitlement_ladderso legacy Standard/Pro is scored againstlegacyPlanLimits. Queries areLIMIT-bounded; entitlement reads run with modest concurrency. - Launch signals (
/admin/insights, loader inpackages/worker/src/admin/launch-signals.ts): COUNT / GROUP BY over indexeduserscolumns andplatform_feedback.status. Rough MRR mapsusers.stripe_price_idthrough the known Standard/Pro catalog (public $12 / $120 and $49 / $480, plus retired list prices) and never calls Stripe or pages the user table.plan,stripe_plan, and overlay-awareeffectivePlanstay separate so gift/referral Standard does not look like paid MRR. Cost-vs-pay ranks the current month's top unique-worker-day consumers (bounded scan) and a Risk panel: catalog-paid accounts over list MRR, unpaid accounts at ≥$1 / 500 unique days (50% of the $2 / 1,000 unique-day included-bucket alert), and Standard/Pro rows whosestripe_price_idis missing or not in the catalog. Free pennies, max, andkentcdoddsare not tagged. Admin-role accounts stay out of the unpaid near-allotment warn bucket; catalog-paid admins over list MRR still appear as paid underwater. The 5-minute insights KV cache (admin-insights:v11) covers the assembled page; RunLog-derived charts come from the hourly RunLog KV snapshot. - Proactive alerts (
usage_entitlement_alertscheduled lane inpackages/worker/src/app/usage-entitlement-alerts.ts): hourly sweep of the same ~15-user bound. Emitsfleet.entitlement.crossedto admin-owned packages once when a swept account first crosses 80% or 100% of a plan-limit resource, when a non-admin account's combined execute, job_run, and workflow_run duration for the month first exceedsfleetRuntimeDurationAlertThresholdMs(24h), when a non-admin account's unique Dynamic Worker cost for the month first reaches the plan-aware threshold (Free $2 / 1,000 unique days, Standard $12, Pro $49), or when a non-admin account hits 100% ofexecute_calls_per_dayon three of the last seven UTC days. Staying over the same threshold does not emit again. Admin-role dogfooding stays on/admin/insightsrankings and can still appear as an entitlement crossing, but does not page the runtime-duration, unique-worker-cost, or repeated- execute signals. KV prefixfleet-entitlement-crossing:v1claims each crossing. Execute-cap trains also writefleet-entitlement-hit:v1day keys so a later drop below 100% the same day does not erase the count. Admin links in the payload are built withjoinAppUrlso a trailing slash onAPP_BASE_URLcannot producehttps://host//admin/…. See Admin events. - Fleet package error rate (same
usage_aggregationhour): a second Analytics Engine SQL, not grouped by user, totalspackage_export,package_static_call,job_run, andworkflow_runfor the last completed hour vs the hour before and the last 24 hours vs the 24 hours before. The snapshot lives at the platform KV keyfleet-package-error-rate:v1and feeds/admin/insights. When the combined rate rises past a volume floor, a third query ranks owners of recent-window errors. Shares use the anonymous window error total so a truncated owner sample cannot look like one account. One account at ≥80% of those errors, or three accounts together at ≥80%, is concentrated; otherwise the spike stays fleet-wide. A follow-up query then loads package ids only for the named owners. Kody still fansfleet.package_error_rate.elevatedto admin-owned packages in every case. Concentrated payloads name usernames and package name leaves only. The KV cooldown keyops-alert:fleet-package-error-rate:v1suppresses repeat pages for six hours. The payload has no user ids, package UUIDs, emails, or error strings. See Admin events.
Usage campaign
The hourly usage_entitlement_alert lane also runs the usage-state lifecycle
campaign (sendUserUsageCampaignEmails). One campaign state per user. The
evaluator branches on activation stamps and live reads (paged distinct inbound
clientIds, enabled jobs / last job run, execute-rollup depth, Stripe paid,
stock entitlement pressure). It is not a fixed week-1/3 calendar drip.
| State | |
|---|---|
VerifiedNoMcp | Connect-an-agent template, 2 sends max |
ConnectedNoPackage | Save-a-package template (personalized with mcp_client_name when set), 2 sends |
PackagedSingleClient | Second-agent / portability template, 1–2 sends. Trial CTA only while the 14-day Standard gift is still unreceived (describeSecondAgentStandardGift status none) |
Activated | Campaign silence. After ≥7 days, one advocate_referral_testimonial mail if not already sent |
Cooling | One “home’s still here” poke, then terminal quiet |
LimitAware | Campaign silence; entitlement-warning mail owns the nudge |
Paid | Campaign silence except the same one-shot advocate mail (1 forever). Billing transactional still owns paid-plan mail |
last_active_at does not bump on job_run. Enabled jobs and last_run_at are
read separately so a quiet interactive user with a live schedule stays
Activated. Cooling requires stale last_active_at plus no job activity. When
both of those stamps are missing, the newest of first_saved_package_at /
first_mcp_connected_at is the fallback — a missing last_active_at is not
treated as 21 days stale. A failed jobs list does not count as "no jobs":
Activated and Cooling rows stay put, and Cooling is not mailed. First
observation still uses the stamp-based quiet check so a quiet packaged user
seeds Cooling instead of a later event transition that would backfill the
poke. Once a user has been Activated or Cooling, they do not fall back to
PackagedSingleClient mail when strong-use or client count dips — recent
activity returns them to Activated silence. ever_activated survives LimitAware
and Paid so those silent states cannot erase that history. A LimitAware snapshot
that already shows two clients, a live job, or strong use sets the flag even
when the resolved state is still LimitAware, so leaving the cap after a later
dip does not become PackagedSingleClient mail. LimitAware from VerifiedNoMcp
without those signals still leaves the flag off. Cooling is one lifetime poke:
cooling_terminal is sticky, so a later re-entry does not retry send 1 or stall
the sweep.
The advocate mail is not a drip and does not reopen Activated or Paid campaign
caps (those stay 0). Eligibility is current Activated or Paid, tenure of
seven days from first_activated_at (or entered_at when that stamp is not set
yet), a live referral shareUrl (/signup?ref=<username> from
referralSharePath), and no prior advocate_referral_testimonial ledger row.
Seed origin does not block it after tenure; first observation still only
persists. Tips opt-out suppresses it. The CTA is the account's referral invite
link; the secondary action is mailto:me@kentcdodds.com (the existing
testimonial channel — the homepage carousel has no intake form).
First sweep of an existing user seeds the current state without mailing
(backfill is out of scope). Verify-time connect-agent mail is send 1 of
VerifiedNoMcp (origin=event). If that first mail fails closed, the verify
path still opens an event-origin row with send_count 0 so the hourly sweep can
retry after the normal first-send dwell instead of seeding the user permanently.
The campaign upsert keeps MAX(send_count) and the later last_sent_at when
the state is unchanged, and never downgrades event to seed, so a later sweep
persist cannot clobber that verify-time row. A real state change still resets
send_count. Later sends wait 24 hours after a transition and 5 days between
sends in the same state. The send ledger claim is INSERT OR IGNORE on
(user_id, state, send_index) and is released if the Cloudflare send fails or
unsubscribe-token minting fails (no footerless campaign mail). A lost claim race
does not persist a stale send_count. Kit is not part of this machine.
Campaign mail is the only surface gated by the Kody tips preference
(user_tips_email_opt_outs). Each campaign send includes an “Unsubscribe from
tips” footer and RFC List-Unsubscribe / List-Unsubscribe-Post one-click
headers. The signed /unsubscribe/tips route sets that stamp; transactional
verify, billing, and error-rate mail is never suppressed. Distinct inbound
client counts come from loadInboundMcpConnectionState. A failed grant listing
does not treat the count as 0: packaged users without Activated history hold
PackagedSingleClient and persist without sending, so a later successful
listing stays a seed instead of an event backfill. A failed execute-rollup read
does not treat depth as 0: the same packaged row persists without sending, so a
later successful read can still become Activated silence instead of second-agent
mail.