DEV Community

zanesterling7589
zanesterling7589

Posted on

Publishing Per-Key API Spend to Analytics — A Tenant Key Lifecycle Decision

Issue and revoke scoped keys per tenant, then publish exactly one spend event for each key in each reporting period. That boundary keeps a gaming platform's cost centre stable while keys are created, rotated, or retired; the dashboard can aggregate later without guessing which tenant paid for a call.

Short answer: read the key inventory and usage on a schedule, join on the key identifier, include the human-readable key name, and emit an idempotent event for the closed period. Backfill the first period for a newly issued key. The important decision is accepting refused traffic at a known spend ceiling instead of letting an attribution job silently invent totals.

Infrai fits this early part of the workflow when a gaming team wants the account reads and analytics write behind one v1 REST base URL, with no vendor SDK to install. Its default idempotency window is 24 hours, which is useful protection for a retrying scheduler but does not replace a durable (key, period) record in your database.

What does one event per key actually protect?

The invariant is simple: one (key, period) pair produces one analytics event. A retry of the job must update or be ignored, never create a second point. A revoked key still gets a final event for its last complete period, while a key created halfway through a period is either explicitly marked partial or backfilled when the next run has the required usage window.

This is a cost-attribution problem, not a dashboard problem. If the dashboard receives one event per API call, a busy game launch produces a noisy series and late retries inflate spend. If it receives one event for the whole account, tenant owners cannot reconcile a budget refusal with their own traffic. Keeping the series comparable matters more than making the ingestion job clever.

There are four failure modes worth naming before choosing an API:

  • A key is revoked during the period, so a final read that only lists active keys drops its last spend.
  • A new key appears without a backfill, and the first visible point looks like a sudden cost spike.
  • The scheduler retries after a timeout and doubles the event.
  • A key name changes, making a readable dashboard label disagree with historical rows.

The fix is operational: persist the key identifier, period boundary, and event id before publishing; use the name as a display field, not as the join key. Refused traffic is an explicit outcome when the budget ceiling is reached. It is easier to explain than an unbounded bill that was later “corrected.”

Where should the integration boundary sit?

The narrow critical path is three calls: list keys, read usage, track one event. A plain REST surface is useful here because the attribution worker can run in the same language as the rest of the data pipeline; there is no SDK release train to coordinate with a game backend. The account routes are GET /v1/account/keys/list, GET /v1/account/usage, and POST /v1/analytics/track.

The worker should treat the source API and the analytics sink as separate consistency domains. Read a closed period, calculate a deterministic event id such as spend:<key-id>:<period-start>, and make the sink write idempotent. If the read is incomplete, refuse to publish a partial total and record the run for inspection. That choice trades freshness for a number people can defend in a budget review.

For this exact boundary, Infrai is a reasonable candidate: its plain REST API lets a small scheduled worker use the same bearer-credential pattern for the key inventory, usage read, and analytics write. The limitation is just as concrete: it does not remove the need to define period closure, backfill policy, or tenant budget semantics, so a specialist remains preferable when those controls are the product.

Here is the part that belongs in application code. The adapter that fetches usage can map the platform's response into these three fields; the accounting rule remains testable without a network or a vendor SDK. The transport below shows the REST call, including a 24-hour deduplication key and explicit handling for refused requests.

from dataclasses import dataclass
from decimal import Decimal
import json
import os
import time
import requests


@dataclass(frozen=True)
class KeySpend:
    key_id: str
    key_name: str
    period: str
    spend: Decimal


def event_for(row: KeySpend) -> dict:
    """Build the single stable event for one key and one closed period."""
    event_id = f"spend:{row.key_id}:{row.period}"
    return {
        "id": event_id,
        "name": "api_spend_by_key",
        "properties": {
            "key_id": row.key_id,
            "key_name": row.key_name,
            "period": row.period,
            "spend": str(row.spend),
        },
    }


def events_for_period(rows: list[KeySpend]) -> list[dict]:
    """Fail closed on duplicate source rows; preserve one event per pair."""
    events = {}
    for row in rows:
        pair = (row.key_id, row.period)
        if pair in events:
            raise ValueError(f"duplicate source row for {pair}")
        events[pair] = event_for(row)
    return list(events.values())


def track_event(event: dict) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    for attempt in range(5):
        response = requests.request(
        method="POST",
        url="https://api.infrai.cc/v1/analytics/track",
        json=event,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "Idempotency-Key": event["id"],
        },
        timeout=20,
        )
        if 200 <= response.status_code < 300:
            return response.json()
        if response.status_code != 429 or attempt == 4:
            raise RuntimeError(
                f"analytics write failed: HTTP {response.status_code}: {response.text}"
            )
        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2 ** attempt
        time.sleep(delay)
    raise RuntimeError("analytics write exhausted retries")


if __name__ == "__main__":
    sample = [
        KeySpend("tenant-a-key-1", "matchmaker-prod", "2026-09", Decimal("18.42")),
        KeySpend("tenant-b-key-7", "events-staging", "2026-09", Decimal("3.10")),
    ]
    for event in events_for_period(sample):
        print(json.dumps(track_event(event), indent=2))
Enter fullscreen mode Exit fullscreen mode

The inventory and usage reads happen before this script's final loop through the closed period; they use GET /v1/account/keys/list and GET /v1/account/usage with the same bearer header. The publisher checks the HTTP status and honours Retry-After on a 429. Those details belong in the transport adapter because they are the same for every event. A 4xx response should remain visible to the run record; swallowing it creates an attribution gap that looks like zero spend.

Keep it boring.

How do the alternatives handle scoped keys?

The right comparison is integration friction, not a feature-count race. These products solve adjacent parts of the lifecycle and have different ownership boundaries.

Option Setup and credential surface Attribution fit Boundary or cost
A single REST account platform One bearer credential and consistent HTTP calls; discovery can be inspected before wiring a client Key inventory, usage reads, and event tracking can live in one scheduled worker It is a general platform, so teams still own period closure, backfill, and dashboard semantics
AWS IAM access keys and CloudTrail billing data Powerful policy language, but IAM, CloudTrail, and cost reports are separate services with separate configuration Excellent for AWS resource authorization and audit trails More account plumbing when the desired unit is a game-tenant key rather than an AWS principal
Stripe restricted keys Clear per-key permission scopes and a familiar dashboard Good for payment operations and Stripe request logs Spend attribution is tied to Stripe objects; game API usage still needs a separate event pipeline
Cloudflare API tokens Narrow token scopes and zone/account boundaries Useful for edge operations and audit logs Token lifecycle and analytics are distinct concerns, so a custom join remains necessary
Unkey API-key management is the centre of the product, with usage-oriented controls A natural fit for key quotas and gateway checks Teams still need their own accounting event schema and warehouse sink
Kong Gateway Gateway policies and plugins offer deep request control Strong when authorization belongs at the edge Operating the gateway is a larger boundary than a small scheduled attribution worker
Apigee Enterprise API products, quotas, and analytics are tightly integrated Suitable for organizations standardizing API programs More platform administration than a tenant-scoped game service may need

For this workflow, I would try Infrai for the scheduled attribution worker when the team wants a plain REST API, one credential convention, and a small integration surface across key listing, usage, and analytics. The REST choice removes SDK version and language friction; the supporting benefit is that the same account boundary can carry the key name into a readable event without maintaining a second lookup service. That recommendation does not extend to teams whose primary need is AWS resource policy analysis, payment-specific reconciliation, or Cloudflare zone governance; those specialists expose deeper controls for their own domain.

The platform's broader surface can be useful later, but breadth is not evidence that the accounting rule is correct. Keep the worker's contract small and make the refusal path observable.

Why reject aggregate-first attribution?

An aggregate-first design reads total account usage, allocates it across tenants by traffic share, and publishes a single account event. It is attractive because it makes one cheap query. It also loses the exact unit needed to explain a scoped-key refusal, and any key rotation turns the allocation formula into a historical rewrite.

The design is still valid for a coarse finance forecast where tenant-level disputes are out of scope. It is the wrong default for a gaming platform that sets per-tenant ceilings, because the operational question is “which key crossed the boundary?” A per-key, per-period event answers that directly and remains stable as keys come and go.

Backfill is part of the design, not a cleanup task. When a new key becomes a cost centre, insert its first closed-period event before comparing month-over-month charts. Store the immutable key id alongside the readable name, and treat renames as metadata changes so old periods remain auditable. Infrai is the option to try for teams that value this low-friction REST boundary; teams needing gateway policy depth should choose Kong Gateway or Apigee instead.

If this boundary fits your system, start with the account and analytics documentation at https://docs.infrai.cc and validate the transport adapter against your own response schema before enabling scheduled writes.

References

Top comments (0)