An AI gateway invoice can tell you the total. It does not automatically tell you which application, environment, or tenant caused a sudden increase.
That becomes a real operational problem when several services share the same gateway. A support assistant, billing workflow, and documentation tool may all be healthy from an availability perspective while one of them quietly produces far more requests or spend than usual.
Attribution Wheel is an open-source TypeScript sample that treats spend attribution as a durable workflow:
The sample runs on Telnyx Edge Compute and uses one Stateful Actor per AI Gateway token group. A built-in demo mode lets you test the baseline, spike, and recovery flow without generating inference traffic or sending a real alert.
Attribution gets harder when it is reconstructed later from application logs. Logs may use different identifiers, arrive late, or omit the information needed to connect a request with a service or tenant.
Attribution Wheel puts the dimensions on the request itself through the x-ltg-metadata header:
metadata = {
"service": "support",
"env": "prod",
"tenant": "example-health"
}
headers = {
"Authorization": f"Bearer {service_token_key}",
"Content-Type": "application/json",
"x-ltg-metadata": json.dumps(metadata),
}
The sample then requests spend grouped by metadata:service. It can also build report views grouped by metadata:tenant and metadata:env.
This makes the attribution dimensions part of the metered request rather than a separate reconciliation pipeline.
The sample combines four pieces:
bootstrap() creates one token group and separate keys for the support, billing, and docs services.AttributionWheel actor owns the schedule, sweep cursor, token-group reference, and masked key references.spend_daily and spend_dims.
The actor is selected by name:
const id = env.WHEELS.idFromName(group);
const wheel = env.WHEELS.get(id);
Using the token-group name as the actor identity gives each group an independent state machine and ledger workflow.
The central SQL table uses (date, service) as its primary key:
CREATE TABLE spend_daily (
date TEXT NOT NULL,
service TEXT NOT NULL,
requests INTEGER NOT NULL DEFAULT 0,
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
spend REAL NOT NULL DEFAULT 0,
reserved_spend REAL NOT NULL DEFAULT 0,
unknown_requests INTEGER NOT NULL DEFAULT 0,
baseline_ratio REAL NOT NULL DEFAULT 0,
breach INTEGER NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT 'gateway',
note TEXT,
PRIMARY KEY (date, service)
);
Writes use ON CONFLICT(date, service) DO UPDATE. Reprocessing a service for the same day therefore updates its row instead of inserting another copy.
The actor also checkpoints the last processed service in a cursor. If a sweep is interrupted, a later call for the same day can resume after that cursor. This protects the ledger from duplicate rows, but production alert delivery still deserves its own idempotency key or outbox if duplicate suppression must survive every possible failure between sending an SMS and recording that it was sent.
That distinction matters: database idempotency and notification delivery guarantees are related, but they are not the same thing.
Global spend thresholds are often too blunt. A large service may cross them during normal traffic while a smaller service can multiply its spend without reaching the same absolute number.
Attribution Wheel calculates a seven-day history for each service and uses two checks:
The defaults are a 40-percentage-point share increase or 2.0x mean daily spend.
For example, if support normally represents 20% of requests and reaches 62%, the increase is 42 percentage points. That crosses the default share threshold.
The sample records the result as an anomaly, continued breach, recovery, or normal row. It sends an alert only when the code sees a new breach, rather than sending another message for every sweep while the same condition remains active.
bootstrap() schedules the first run. After each scheduled sweep, nightly() schedules the next run for 23:00 UTC:
async nightly(): Promise<void> {
try {
await this.sweep(shiftDate(todayUtc(), -1));
} finally {
await this.schedule(this.secondsUntil2300(), "nightly");
}
}
The scheduled handler and the manual sweep(day) RPC use the same code path. That makes the workflow easier to test and allows an operator to rerun a specific day deliberately.
Each sweep:
Requests that cannot be attributed to a known metadata value are surfaced as unknown rather than silently assigned to another service.
Clone the sample and install its dependencies:
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/attribution-wheel
npm install
cp .env.example .env
Keep demo mode enabled:
TELNYX_API_KEY=your_telnyx_api_key_here
ATTRIBUTION_DEMO_MODE=true
ATTRIBUTION_ONCALL_E164=<ON_CALL_E164>
ATTRIBUTION_DEMO_NUMBER=<TELNYX_FROM_E164>
ATTRIBUTION_ALERT_SHARE_PCT=40
ATTRIBUTION_SPIKE_MULTIPLE=2.0
AI_GATEWAY_MODEL=Kimi-K3
Deploy the Edge project and bootstrap the actor:
telnyx-edge auth api-key set "$TELNYX_API_KEY"
telnyx-edge types
telnyx-edge ship
curl -s -X POST "$BASE/rpc/bootstrap?group=main" -d '[]'
Seed seven days of synthetic baseline data, then simulate a spike and recovery:
curl -s -X POST "$BASE/rpc/seed?group=main" \
-d '[7]'
curl -s -X POST "$BASE/rpc/simulate?group=main" \
-d '["2026-11-10", "spike"]'
curl -s -X POST "$BASE/rpc/sweep?group=main" \
-d '["2026-11-10"]'
curl -s -X POST "$BASE/rpc/simulate?group=main" \
-d '["2026-11-11", "recovery"]'
curl -s -X POST "$BASE/rpc/sweep?group=main" \
-d '["2026-11-11"]'
In demo mode, alerts are logged instead of sent and the history is clearly marked as synthetic. The dashboard and report(day) RPC still show the complete attribution flow.
For a live deployment:
ATTRIBUTION_DEMO_MODE=false. x-ltg-metadata values.
Metadata can contain tenant identifiers, so use internal opaque IDs rather than customer names or sensitive information. Establish retention and access policies for the attribution ledger as well.
The repository includes a smoke test for the actor surface, spend-row normalization, and dashboard rendering:
npm test
The implementation, setup guide, API reference, demo generator, and dashboard are available in the open-source repository:
Explore the Attribution Wheel sample on GitHub
Attribution does not need to begin as a separate analytics platform. A useful first step is simply to put stable dimensions on each request, preserve the gateway's daily totals in an idempotent ledger, and compare every service with its own history.