# valuemaxx — AI margin intelligence (cost-per-outcome with confidence) ## capabilities - ingest_otlp_span [surfaces=API; mode=request_response]: Ingest one OTLP span as a CostEvent (universal/TS producer path). - list_cost_sources [surfaces=API|MCP|CLI; mode=request_response]: List the wired cost sources (authoritative spend / reconciled actuals). - capture_healthcheck [surfaces=API|MCP|CLI; mode=request_response]: Report capture liveness and the effective capture granularity. - create_connection [surfaces=API|MCP|CLI; mode=request_response]: Mint an inbound URL for a third-party app. The URL is returned once. - set_connection_secret [surfaces=API|MCP|CLI; mode=request_response]: Store the provider's own signing secret, sealed, so deliveries can verify. - list_connections [surfaces=API|MCP|CLI; mode=request_response]: List this tenant's inbound connections, revoked ones included. - revoke_connection [surfaces=API|MCP|CLI; mode=request_response]: Revoke an inbound connection. Its URL stops resolving immediately. - ingest_webhook_outcome [surfaces=API; mode=webhook_inbound]: Ingest an inbound outcome webhook: verify signature + ingest key before parse, then bind run_id via T3 echo or fall back to T4 entity (labeled). - validate_outcome_rule [surfaces=API|MCP|CLI; mode=request_response]: Validate an outcomes.yaml document through the safe loader and AST allowlist; an eval/exec/dunder predicate is rejected, never executed. - list_outcome_rules [surfaces=API|MCP|CLI; mode=request_response]: Summarize the rules declared in an outcomes.yaml document. - list_recent_outcomes [surfaces=API|MCP|CLI; mode=request_response]: The individual outcomes behind a figure, newest first, with a count of the ones that did not reach billing grade and the reason each did not. Answers 'which events are these?' about a number. - bind_outcome [surfaces=API|MCP|CLI; mode=request_response]: Bind an outcome event to the agent run that produced it via the binding cascade (exact->deterministic->candidate->likely), returning the tier-labeled attribution result. - list_pending_reviews [surfaces=API|MCP|CLI; mode=request_response]: Every advisory binding awaiting review for the calling tenant — candidate, likely and unbound outcomes. These are excluded from every billing-grade denominator until a human confirms them. - list_review_queue [surfaces=API|MCP|CLI; mode=request_response]: Return the pending review attribution result for ONE given (tenant-scoped) outcome — a candidate/likely/unbound binding awaiting human confirmation. Advisory, never billing-grade. - reconcile_day [surfaces=API|MCP|CLI; mode=request_response]: Reconcile one match key's per-request estimates against the authoritative billed total: prorate so the reconciled values sum exactly to the invoice, append an additive reconciliation record (never an update to the estimate), and surface any >10% drift. - cost_breakdown [surfaces=API|MCP|CLI; mode=request_response]: Project a mixed reconciliation-state cost window into the honest reconciled / provisional / estimate_only breakdown, never collapsing an estimate into a billed number. - allocated_cost_rollup [surfaces=API|MCP|CLI; mode=request_response]: Allocate one run's fully-loaded cost across the three tiers (direct/measured, shared-proportional by declared key, fixed overhead with idle GPU quarantined beside the unit cost). Returns pct_unallocated and both H7 confidence fields; never smears idle capacity into the unit cost. - run_metric [surfaces=API|MCP|CLI; mode=request_response]: Validate, compile, and run a user-defined metric (a typed allowlist DSL: filter -> outcome -> join -> measure), returning a result whose every cell carries the conservative confidence (minimum_tier + distribution) and excludes candidate/likely and retracted outcomes from the billing-grade denominator. - discover_agents [surfaces=API|MCP|CLI; mode=request_response]: Cluster captured LLM calls into agent/prompt clusters via the deterministic group-by backbone + Drain skeletons. Every cluster is unconfirmed (human-confirm is onboarding). - confirm_eval_suite [surfaces=API|MCP|CLI; mode=request_response]: Confirm which repository and command a call site's evals run from, after `valuemaxx find-evals` proposed it. Nothing can be dispatched until this exists: it is what a GitHub OIDC token is checked against, `source` is a descriptive label only - what authorizes a run is the job token, never a name typed into a form. - import_promptfoo_suite [surfaces=API|MCP|CLI; mode=request_response]: Import an existing promptfoo suite as eval criteria: llm-rubric assertions become judge-scored criteria (the rubric text used verbatim), contains/not-contains become exact checks. Every unsupported assertion type is returned, never approximated. - evaluate_switch [surfaces=API|MCP|CLI; mode=request_response]: Decide whether switching models improves cost PER OUTCOME, and label how provable that is. A repricing alone stays observational and is never safe_to_switch; only a POWERED non-inferiority test on the outcome rate earns the randomized label. Returns what each confidence margin would cost in units per arm, so a team can pick a bar it can afford to prove. - estimate_switch_cost [surfaces=API|MCP|CLI; mode=request_response]: Project what today's traffic would cost on a different model, by repricing the incumbent's OWN observed token vectors against the candidate's price card. Always ESTIMATED (list price over traffic the candidate never served); returns no number when the model cannot be priced, rather than a misleading zero. - discover_switch_candidates [surfaces=API|MCP|CLI; mode=request_response]: Automatically generate a bounded shortlist of cheaper models by repricing this tenant's observed token mix against every current price card. This is discovery, not a quality claim or permission to switch; candidates must still pass replay and outcome evaluation. - run_eval_funnel [surfaces=API|MCP|CLI; mode=async_job]: Run the full eval funnel (discover->dataset->grade->search->cost-gate->recommend) for one candidate against the incumbent. Long-running: returns a job id to poll. Never auto-switches. - run_automatic_eval_funnel [surfaces=API|MCP|CLI; mode=async_job]: Automatically price-rank cheaper models, live-replay a bounded top set on the same captured workload, measure quality and p95 latency, and persist the cheapest candidate that clears every requested constraint. Never auto-switches production traffic. - upload_eval_bundle [surfaces=API|MCP|CLI; mode=request_response]: Upload a packed eval suite, load it into this call site's sandbox and pin a checkpoint. With check=true (the default) the suite is also run once under the real egress policy, so a suite that cannot run sealed fails here rather than during an unattended experiment. - get_recommendation [surfaces=API|MCP|CLI|NOTIFY; mode=request_response]: Return the latest eval recommendation for an incumbent model (parity, confidence grade, label source) within the tenant scope. Aggregate-only, safe for NOTIFY digests. - approve_gate [surfaces=API|MCP|CLI; mode=request_response]: Record a human cost-gate approval for an eval phase (smoke=phase 1, confirmation=phase 2). The estimate is the consent; phase 2 is only reachable after phase 1 is approved. - lint_call_site [surfaces=API|MCP|CLI; mode=request_response]: Report structural cache misalignment and exact duplicate calls within a run; never rewrites prompt semantics. - authorize_optimization_deployment [surfaces=API|MCP|CLI; mode=request_response]: Authorize a production deployment only from an explicit enabled per-call-site application policy. - get_active_optimization_deployment [surfaces=API|MCP|CLI; mode=request_response]: Return the separately authorized active deployment for one call site. - stop_optimization_deployment [surfaces=API|MCP|CLI; mode=request_response]: Stop every live deployment at a call site so enforcement returns to the host's original configuration, recording who stopped it and why. - list_optimization_deployments [surfaces=API|MCP|CLI; mode=request_response]: List a call site's deployment history, newest first, so an operator can see what is live, what preceded it, and why anything was stopped. - search_configurations [surfaces=API|MCP|CLI; mode=request_response]: Cost-prefilter discrete request configurations, then evaluate all survivors at 50, 200, and 1000 observed examples while retaining the top half. - get_optimization_frontier [surfaces=API|MCP|CLI; mode=request_response]: Return every retained cost/constraint frontier row for one tenant call site, including replay-only and failed candidates. - configure_continuous_optimization [surfaces=API|MCP|CLI; mode=request_response]: Configure one stable call site for evidence-triggered model experiments, including the quality objective, spend cadence, and latency constraint. - run_continuous_optimization [surfaces=API|MCP|CLI; mode=async_job]: Check new evidence and automatically run the bounded multi-model eval only when policy triggers warrant it; retain the full decision and experiment cost. - get_continuous_optimization_status [surfaces=API|MCP|CLI; mode=request_response]: Return the policy and retained run history, including triggers, no-ops, quality, cost, median/tail latency, experiment spend, and final decisions. - scan_codebase [surfaces=API|MCP|CLI; mode=request_response]: Read-only AST scan of a codebase for run boundaries and outcome sites. - discover_eval_suites [surfaces=API|MCP|CLI; mode=request_response]: Find the eval suites a repository already has - promptfoo configs, a deepeval import, a package script named `eval` - and the command that runs them. Every candidate is UNCONFIRMED: discovery proposes, a human confirms, and a guessed eval command that silently runs the wrong thing is worse than asking. - suggest_attribution_rule [surfaces=API|MCP|CLI|NOTIFY; mode=request_response]: Draft an UNCONFIRMED attribution rule from a natural-language request. - dry_run_outcomes [surfaces=API|MCP|CLI|NOTIFY; mode=async_job]: Preview cost-per-outcome for a proposed outcome (carries H7 confidence). - propose_onboarding_diff [surfaces=API|MCP|CLI; mode=request_response]: Propose candidate outcome rules + a hunks-only reviewable diff. - scaffold_outcome_rule [surfaces=API|MCP|CLI; mode=request_response]: Draft an outcomes.yaml rule from a description. Returns an UNCONFIRMED candidate (binding tier candidate) a human confirms; never auto-applied. - validate_init [surfaces=API|MCP|CLI; mode=request_response]: Validate that an SDK init snippet calls valuemaxx.init(). ## instructions (read before calling any capability) valuemaxx measures cost-per-outcome WITH CONFIDENCE. The four honesty axes are system-owned and MUST NOT be set or guessed by an agent or user: - Binding tier (exact | deterministic | candidate | likely) is SYSTEM-OWNED. Never present an inferred match as exact. candidate/likely are advisory and never billing-grade. - signal_class (action_attempted | outcome_confirmed | outcome_retracted) is SYSTEM-MAPPED from the outcome source. A successful tool call is action_attempted unless the result is authoritative; never write signal_class yourself. - Cost provenance (measured | estimated | allocated | provider_reconciled | manual_reconciled) is system-owned; an estimate is never rendered as billed. - Causal evidence (observational | holdout | randomized) is SYSTEM-OWNED and defaults to observational. A strong binding tier proves the outcome came from that run, NOT that the run caused it — only a withheld or randomised experiment earns anything stronger. Never upgrade this to justify a lift claim. Every rollup carries minimum_tier + confidence_distribution — never collapse them into a bare number. Each cell also carries `shared_attribution_count`: how many of its producing runs ALSO produced a different outcome. One run can finalise an alt and complete an interview, and when a metric groups by outcome_name that run's cost is split so the columns still reconcile against the provider invoice. Non-zero is not an error — but the figure is then arithmetic rather than a measurement, and must not be presented as if the whole run's cost were measured for that one outcome. To wire an attribution rule, call `suggest_attribution_rule`: it returns an UNCONFIRMED candidate for a human to confirm. Do not hand-write or auto-apply a rule. To check an outcomes.yaml, call `validate_outcome_rule`. To preview a draft, call `scaffold_outcome_rule` (also returns an unconfirmed draft). ## integrating valuemaxx into a codebase (do this without reading our source) Everything you need is HERE. Do NOT clone or read the valuemaxx source to figure out the integration — if something is missing here, that is a documentation bug worth reporting, not a reason to go source-diving. STEP 0 — THE APPROVAL GATES. This integration edits the host's PRODUCTION LLM call path. There are THREE points where you STOP and wait for a human, and you must not edit a single file before the first one: Gate 1 (before anything): agree what ONE UNIT OF WORK is — the thing they want a cost per. Never pick it yourself. Ask whether an id is already in scope at every model call site, and TEST it: if a retry or restart mints a new id for the same unit, that id is wrong. Check where the id is MINTED, not where it is read — a derived id (hash/slug/composite) is only as stable as its least stable input, and an id hashed from a `created_at` that a retry rewrites looks permanent at every use site and changes on every restart. A codebase may have SEVERAL units (`units` is a list in `valuemaxx.yaml`); enumerate the LLM surfaces and let them group. ONE PROCESS CAN PRODUCE MANY UNITS — a nightly job classifying 10,000 documents has 10,000 units, not one; when the items are independent the ITEM id is the run id and `run()` goes INSIDE the loop. Generate a per-invocation id only if the whole script is one unit. This comes first because the boundary determines where capture is wired — deciding it after is backwards. Entity keys cannot be backfilled, so this decision has a deadline. Gate 2 (before any capture wiring): present the exact list of files you would change with one line on why each, which model-call entry points it covers and which it does NOT, and anything you had to assume. Then ask which scope they want and WAIT. Offer the smallest useful option first — usually the host's one LLM wrapper, one file. Gate 3 (before writing outcomes.yaml): present the candidate outcomes AND the GRAIN each is recorded at — whole workflow, one step inside it, one item of a batch, or an external confirmation. Grain is a SEPARATE decision from the unit of work: the run boundary decides what cost is grouped, the outcome site decides what that group is divided by, and both are legitimate at more than one level. Picking whichever call site is convenient silently answers a question the user never saw. They compose, so ask which they want (plural), and show the arithmetic each grain implies. See `scaffold_outcome_rule` / `suggest_attribution_rule`, which return UNCONFIRMED candidates by design. AFTER the gates: cut a branch from REMOTE main (fetch first), commit, push, open a PR. Never leave the work on whatever branch was checked out, and never stash or revert a dirty tree to make room — those changes are not yours. If the scope GROWS mid-change, stop and re-ask — a one-file change that became a five-file change is no longer what they approved. Threading a handle from where the env lives to where the call happens is the usual cause: each hop looks trivial and the total is a signature change across the codebase. Never run destructive git commands (stash/checkout --/reset/clean) on the host's tree, and never weaken a host safety setting (lockfile policy, supply-chain minimum-release-age, a CI gate) to unblock yourself — use a one-off per-command override, or tell the user. STEP 1 — THE GATEWAY IS THE DEFAULT PATH. Do not reach for the SDK first. Capture happens in a proxy the host points its provider base URL at, so the integration is a config change rather than code in the request path: client = OpenAI( base_url="https:///openai/v1", # line 1 default_headers={"x-vmx-key": "vmx_live_...", # line 2 "x-vmx-agent": "support-bot"}) # only UNCHANGING values ... client.chat.completions.create(..., # line 3 extra_headers={"x-vmx-run-id": order_id, # <- THEIR durable id, "x-vmx-outcome": "order_fulfilled"}) # PER CALL (optional) The run id is per REQUEST because it changes per unit; putting it in `default_headers` on a long-lived client stamps every unit with the first one's id and collapses them into one. Nothing errors — the numbers are just wrong. Only put it on the client when that client serves exactly one unit of work. Routes: /openai /anthropic /gemini /deepseek /openrouter. Append whatever path the SDK already appends — the gateway forwards everything after the route prefix verbatim, so an OpenAI client wants `/openai/v1` (its own paths start `/chat/...`) while an Anthropic client wants `/anthropic` (it appends `/v1/messages` itself). Every `x-vmx-*` header is stripped before forwarding; the host's provider key passes through and is never stored. PER-CALL vs PER-CLIENT HEADERS. A long-lived client whose run id changes per unit sets `x-vmx-run-id` PER REQUEST, not in `default_headers` — a per-request header wins over a default of the same name. Do not construct a client per unit of work. OpenAI (python): client.chat.completions.create(..., extra_headers={...}) Anthropic (ts): client.messages.create({...}, { headers: {...} }) Put only the values that never change (`x-vmx-key`, often `x-vmx-agent`) on the client. STREAMING FROM OPENAI: the host MUST set `stream_options={"include_usage": true}`. OpenAI omits usage from a stream unless asked, so without it there is nothing to read and the calls capture ZERO tokens. The gateway will not add the flag — that would change the request, breaking the invariant that the provider sees exactly what the host wrote. Spans from such a stream are flagged `partial_recovered` rather than reported as a confident zero, but the fix belongs to the host. DeepSeek's current Chat Completions stream reports usage on its final chunk without the flag; Anthropic and Gemini also report terminal usage independently. HEADERS ARE THE WHOLE CONTRACT. Everything the SDK asked for in code is a string: x-vmx-key the tenant x-vmx-run-id the unit of work (or W3C `baggage: valuemaxx.run_id=...`) x-vmx-agent grouping label x-vmx-entity- durable business ids the unit is about x-vmx-outcome the outcome this call completes (recorded only on 2xx) x-vmx-experiment which comparison this call is an arm of x-vmx-variants the arms, comma-separated. Send this and the GATEWAY assigns: it hashes (experiment, run id) to pick an arm and ECHOES the choice back in the `x-vmx-variant` response header. Read that once per unit and use it for the unit's calls. Assigning it yourself is allowed and always wins, but then the split is only as unbiased as your own logic — if it correlates with traffic source, time of day or customer size, the experiment measures that instead of the model. x-vmx-variant which arm. No engine reads these yet; they are captured now because a variant stamp CANNOT be added to traffic after it has run, so any comparison over history depends on stamping it before the history exists. x-vmx-app which of the host's surfaces made the call (one tenant, several products) USE THE HOST'S OWN DURABLE ID as `x-vmx-run-id` (`order_9182`, not a UUID we mint). It groups the calls of one unit AND makes a delayed outcome free: when a webhook arrives days later carrying that same id, it binds at `exact` with no time window and no inference. If the host cannot supply one, the gateway mints one and ECHOES it back in the `x-vmx-run-id` response header for the host to stamp outward. THE OUTCOME CONTRACT — one primitive, everything else compiles to it: POST /v1/outcome { "name": "order_fulfilled", "run_id": order_id } optional: entity {..}, value, occurred_at, identifier, source; ?strict=true It rests on the one invariant every codebase shares regardless of architecture: at the moment a business fact becomes true, some code is executing with the relevant business id in a variable. Deliver the tuple then. If it is expressible in curl, it is expressible in every language — no SDK, no patchable function, no framework knowledge required. ONE SHAPE, ALWAYS. Every outcome is this same call — an in-process one, a webhook days later, a queue consumer. Only which optional fields you fill changes. And the REPLY is uniform too: it always carries `attached` (did this reach any spend), `attachment` (run_id | entity | run_unmatched | entity_unmatched | none) and, when it did not attach, a `hint` naming what you can change. Check `attached` on your first integration call — a 200 alone does not mean the event found its cost. Contract discipline (Stripe/Segment-grade): - `identifier`: caller idempotency key. A duplicate (tenant, identifier) is accepted-and-ignored, so at-least-once senders (waitUntil replays, webhook retries) never inflate the denominator. Always set it when you can. - `occurred_at`: accepted 35 days back to 5 minutes forward; outside that is a 422, not a silent clamp. Late REAL outcomes fit; clock bugs surface. - `entity_window_days`: how far back an ENTITY-bound outcome may reach for its runs. Default 1 day. A B2B deal closing in month three needs ~90; a support ticket needs hours. Only you know your lag, and one global value must either strand the slow case or over-match the fast one. Widening buys REACH, never trust: an entity match stays `candidate` and stays out of the billing-grade denominator at any window. Irrelevant when you send `run_id` — a shared-id join uses no window at all, which is another reason to send it. - `?strict=true`: rejects an event with neither run_id nor entity. Default is permissive — unbound-but-visible beats silently dropped. - The TIER is always decided server-side. A caller states what happened; it never states how much to trust the link. SHORTCUTS that emit this same event (use when the host's shape matches; never required): `x-vmx-outcome` header on the producing call (fires on 2xx, zero extra requests); settle rules and decorators (planned, not built). INBOUND WEBHOOKS — read this before promising a "no code" path. The endpoint is `POST /ingest_webhook_outcome`, HMAC-signed (`X-Signature`) over the raw body and verified BEFORE parse. What does NOT exist yet is the per-source mapping config that would turn a raw Stripe or Zendesk payload into the tuple. So today a third-party provider cannot post to it directly: something of the host's must receive the provider's webhook and forward the tuple. For a host that already has a webhook handler — the usual case for a CRM or billing outcome — that is ONE `POST /v1/outcome` line inside a handler they already own, which is the documented delayed path and is genuinely small. Do not tell a user this step is config-only. OUTCOMES OLDER THAN THE 35-DAY WINDOW. `occurred_at` is validated to 35 days back / 5 minutes forward. A B2B outcome can legitimately land later than that (a deal that closes in month three). OMIT `occurred_at` in that case: the event is then stamped at ingest time and still binds to its run at `exact` via the shared id, because a shared-id join uses no time window at all. What you lose is only the true event time, not the attribution. Never clamp or fabricate a timestamp to fit the window, and never drop the outcome. THE ALIAS — when the id you had is not the id you end up with: POST /v1/alias { "from": {"session_id": "abc"}, "to": {"lead_id": "8172"} } Use it when work happens under one identity and the outcome arrives under another: an anonymous chat session that later becomes a known lead, a trial account that converts, two records merged in a CRM. Without it that early spend is ORPHANED — the money was real, the outcome was real, and nothing joins them, so the unit cost silently excludes the anonymous half of the story. - Each side is exactly ONE entity key. Two keys on one side would assert every cross-pairing between them, a much larger claim than you wrote, so it is a 422. - Resolution is symmetric and transitive: session→lead and lead→account means a query for any one of the three considers all three. - Applied at QUERY time. Nothing already stored is rewritten, so a span keeps recording what the caller actually sent, and an alias asserted months later still re-joins the history it names. Post it whenever you learn it. - Asserting the same edge twice is one claim, not two. THE ALIAS MATCHES ON ENTITY KEYS, so the anonymous side must have carried one. This is the half that is easy to miss: posting the alias does nothing if the early calls never attached the key it names. End to end, all three steps: 1. the anonymous call — attach the key you DO have (hyphens become underscores, so this header is what `{"session_id": ...}` matches): x-vmx-entity-session-id: abc x-vmx-run-id: abc (a session is its own unit for now) 2. the moment the identity is known — post the edge: POST /v1/alias {"from":{"session_id":"abc"},"to":{"lead_id":"8172"}} 3. the outcome, days later, naming only the lead — no alias mentioned: POST /v1/outcome {"name":"meeting_booked","entity":{"lead_id":"8172"}} Step 3 now costs out the spend from step 1, because step 2 made the two keys one entity at query time. Skip step 1 and there is nothing to re-join: the alias is a claim ABOUT entity keys, not a way to invent one after the fact. RUNNING IT. There is no hosted signup yet — the user runs both pieces: docker run -p 8000:8000 ghcr.io/monaal10/valuemaxx-backend:latest fly deploy -c fly/gateway/fly.toml # VALUEMAXX_GATEWAY_BACKEND=https:// The key is whatever `VALUEMAXX_INGEST_KEYS` maps ({key: tenant-uuid}); unset, the backend serves a single dev key `dev`. Do not write `vmx_live_...` into a host's config without telling them where it comes from. NEVER commit a gateway URL pointing at localhost. It replaces the provider base URL, so it is on the REQUEST path — a deployed worker resolving 127.0.0.1 to its own isolate breaks every model call, not just telemetry. Ship it empty; set per env. Detecting an EXISTING gateway integration: the step-0 SDK greps (`init(`, `run(`) report "unwired" on a repo that is fully wired through the gateway. Also `grep -rn "x-vmx-\|VALUEMAXX_GATEWAY_URL"`. STEP 1b — the SDK, which carries the RUN ID. It does not capture; the gateway does, because it sees the whole request and response. The SDK is an accessory to step 1, never an alternative to it: an `init()` with no repointed base URL captures nothing. pip install valuemaxx # Python npm install valuemaxx # TypeScript/JS `init()` needs `tenant_id`, `ingest_key` and `endpoint`, and `endpoint` is the GATEWAY base URL — the same one step 1 pointed the provider client at. BOTH SDKs ARE AT PARITY — same entry, same options, same wire, pinned by goldens in tests/wire_contract/ that diff the real producers rather than by inspection. entry TS `import { init, run } from "valuemaxx"` PY `from valuemaxx.sdk import init, run` run boundary TS `run(id, { agentName, entityKeys }, fn)` PY `with run(run_id=..., agent_name=..., entity_keys=...)` record outcome TS `recordOutcomeNow(name, config, opts)` PY `record_outcome_now(name, config=...)` THE THREE CARRIES. Each is opt-in via an init option; T3 is the only one nothing else can do. T1 runIdHeaderTargets / run_id_header_targets stamps x-vmx-run-id on calls to the gateway, so a `run()` block does not have to thread the header by hand at every call site. T2 baggageTargets / baggage_targets the run id rides W3C baggage across a hop between the host's OWN services. T3 runIdInjectionTargets / run_id_injection_specs the run id is stamped into a THIRD PARTY's call (Stripe, HubSpot, Zendesk) so their webhook echoes it back days later. The gateway sees calls to the gateway; it never sees the host call Stripe. Without T3 that webhook binds at `candidate`, which is not billing-grade, and the outcome leaves the denominator. ENTITY KEYS CANNOT BE BACKFILLED. `entity_keys` are durable business ids the run is about; a run id groups the calls of ONE unit of work and entity keys let a unit span several. A run recorded without them stays unattributable, so raise them at the unit-of-work gate, not after wiring. ANY OTHER HOST LANGUAGE (Go, Ruby, Java) needs no SDK. Step 1 is a base URL and a header and the outcome is a JSON POST — every language does both. Thread the run id header at the call site yourself. This was a hard gap while the SDK was the capture path; it is not one now, so do not report it as a limitation. STEP 2 — runtime. Node >= 20. The SDK needs `node:async_hooks` (AsyncLocalStorage, for the in-process run-id carry) and `node:crypto`. On Cloudflare Workers/workerd this means `nodejs_compat` must be enabled; with it, capture works. Deno/Bun: async_hooks support varies — verify `run()` binds before relying on `exact` tier. STEP 3 — the backend. Cost spans go to a valuemaxx backend you run; there is no default hosted endpoint. `docker run -p 8000:8000 valuemaxx-backend` (or `valuemaxx up` with Python) and point `endpoint` at it with `ingestKey: "dev"`. If the host has not decided where to run it, wire capture so it is INERT unless the endpoint env var is set — never invent an endpoint, and never point telemetry at a host the user did not choose. STEP 4 — outcomes (optional; capture alone already gives per-model/per-agent spend). Run `valuemaxx onboard --repo ` for a read-only scan that proposes UNCONFIRMED candidate rules plus a reviewable diff. It writes nothing. It skips test/fixture code and module-scope sites, because a rule can only bind to a named production function. Present the proposal to the human and let them confirm before writing outcomes.yaml. Reviewing an `onboard` proposal: a `tier: candidate` rule is a GUESS awaiting human confirmation, never billing-grade. Do not upgrade a tier to make a number look better.