Genspark billing importer (Super Agent — reconciliation only)
What this is
Section titled “What this is”The spendguard-importer-genspark crate (D16) is a Rust binary +
library that:
- Pulls usage from the Genspark admin API
(
/v1/admin/usage?workspace=…&from=…&to=…) — feature-gated behindliveso the default build is HTTP-free. - Converts credits to estimated USD via a vendored price table at
services/importer_genspark/assets/genspark_credit_prices.json. - Emits signed
spendguard.audit.import.genspark_creditCloudEvents tagged withreservation_source=subscription_meterandimport_source=genspark_team_api— landing on the same dashboard surface as D13 subscription-meter rows and the D14 Devin importer rows.
The default merge gate is a sanitized fixture replay — the
committed tests/fixtures/genspark_usage.json snapshot. Live mode is
operator-opt-in.
Credit → $ conversion
Section titled “Credit → $ conversion”| Plan | Monthly $ | Monthly credit grant | usd_per_credit |
|------|-----------|---------------------|-------------------|
| plus | $19.99 | 10,000 | $0.001999/credit |
| pro | $24.99 | 12,500 | $0.0019992/credit |
| premium | $249.99 | 125,000 | $0.00199992/credit |
The conversion is pure:
round(credits_consumed × usd_per_credit × 1_000_000) in micro-USD,
saturating at i64::MAX on overflow. The price table’s
pricing_version is stamped on every emitted row at the moment of
conversion — back-revisions to the rate file do not rewrite
historical audit rows.
Unknown-plan fallback
Section titled “Unknown-plan fallback”If the Genspark API returns a plan slug that is not in the
embedded price table (e.g. a future "enterprise" tier, or a
mid-window rename), the importer emits the row with
amount_micro_usd = 0 and stamps
reason_code = "genspark_plan_unknown". Dashboards must distinguish
“unknown rate” from “zero spend” — the credit field is still
populated, so operators see Genspark consumption volume even when the
dollar mapping is unknown. Both fields are always set together;
setting only one would either silently mis-price (visible non-zero
USD with no rate) or leave the dashboard with no signal.
Install
Section titled “Install”Fixture mode (default merge gate)
Section titled “Fixture mode (default merge gate)”cargo run -p spendguard-importer-genspark --bin importer_genspark -- \ --mode fixture \ --fixture services/importer_genspark/tests/fixtures/genspark_usage.json \ --tenant <your-tenant> \ --budget <your-budget-id>The binary prints one CloudEvent per record to stdout; the demo
runner script (deploy/demo/import_genspark_fixture_demo.sh) wires
the INSERT path against the demo postgres.
Live mode (operator-opt-in)
Section titled “Live mode (operator-opt-in)”export GENSPARK_API_TOKEN=<Admin API token, at least 32 chars>export GENSPARK_API_BASE_URL=https://api.genspark.ai/v1 # optional override
cargo run -p spendguard-importer-genspark --features live --bin importer_genspark -- \ --mode live \ --tenant <your-tenant> \ --budget <your-budget-id>The live feature pulls reqwest with rustls-tls only (no
native-tls, no openssl-sys) and uses a 30s per-request timeout
plus bounded exponential backoff (cap: 1 hour) for resilience against
rate-limits.
The runtime gate on GENSPARK_API_TOKEN enforces three checks at
startup with distinct error messages so operators can debug:
- Missing — the env var is not set.
- Empty — the env var is set but blank after trimming whitespace.
- Too short — the value is fewer than 32 characters (catches
placeholders like
"TODO"/"changeme").
Scope the token to the Genspark Admin Usage API scope only — least privilege.
Idempotency
Section titled “Idempotency”Re-running the same window does not double-emit. The CloudEvent
event.id is a deterministic UUIDv5 derived from
(workspace_id, task_id, window_end); canonical_ingest dedups via the
existing event_replay_dedup table.
You can re-run the demo as many times as you like:
make -C deploy/demo demo-verify-import-genspark-fixturemake -C deploy/demo demo-verify-import-genspark-fixture # idempotent; 0 new rowsSample CloudEvent
Section titled “Sample CloudEvent”{ "specversion": "1.0", "type": "spendguard.audit.import.genspark_credit", "source": "spendguard-importer-genspark", "id": "018f4a3a-d971-7c16-91fe-d0169e715ba0", "time": "2026-06-08T12:00:00Z", "datacontenttype": "application/json", "subject": "tenant/demo/genspark/workspace/FAKE_ws_001/task/FAKE_task_001", "data": { "schema_version": "v1alpha1", "tenant_id": "demo", "budget_id": "genspark-budget", "workspace_id": "FAKE_ws_001", "task_id": "FAKE_task_001", "plan": "plus", "credits_consumed": 3200.0, "usd_per_credit": 0.001999, "amount_micro_usd": 6396800, "reason_code": null, "pricing_version": "genspark-credit-v1-2026-06", "window_start": "2026-06-01T00:00:00Z", "window_end": "2026-06-01T01:00:00Z", "reservation_source": "subscription_meter", "import_source": "genspark_team_api", "ingestion_mode": "fixture", "fixture_provenance_sha256": "fd2c0bb772bfbf2605ce09204aed0025cd754c3edce7296ee281637e5a52baf6", "task_category": "research" }}Sample price table
Section titled “Sample price table”{ "pricing_version": "genspark-credit-v1-2026-06", "effective_from": "2026-06-01T00:00:00Z", "currency": "USD", "rates": [ { "plan": "plus", "monthly_usd": 19.99, "monthly_credits": 10000, "usd_per_credit": 0.001999 }, { "plan": "pro", "monthly_usd": 24.99, "monthly_credits": 12500, "usd_per_credit": 0.0019992 }, { "plan": "premium", "monthly_usd": 249.99, "monthly_credits": 125000, "usd_per_credit": 0.00199992 } ]}Why we cannot gate
Section titled “Why we cannot gate”The Genspark Super Agent loop runs entirely inside Genspark’s cloud VM. The customer’s egress proxy never sees the LLM API call payload — Genspark’s VM dials OpenAI / Anthropic / other providers from inside their own network boundary. The admin API exposes only post-hoc credit aggregates, not a pre-call hook.
This is Archetype IV in the SpendGuard framework-coverage analysis: a fully managed cloud agent where the customer has zero network visibility. The only feasible integration is reconciliation — pull the bill after the fact, surface it on the dashboard, alert on threshold crossings. Gating is architecturally impossible without Genspark shipping a pre-call webhook.
Operator scheduling
Section titled “Operator scheduling”The importer is a periodic worker, not a daemon. Schedule it via cron, Kubernetes CronJob, or whatever your environment supports:
# Kubernetes CronJob example — pull every hour.apiVersion: batch/v1kind: CronJobmetadata: name: spendguard-importer-gensparkspec: schedule: "0 * * * *" jobTemplate: spec: template: spec: containers: - name: importer image: ghcr.io/m24927605/agentic-spendguard/importer-genspark:latest env: - name: GENSPARK_API_TOKEN valueFrom: secretKeyRef: name: genspark-admin-token key: token args: - --mode=live - --tenant=$(SPENDGUARD_TENANT_ID) - --budget=$(SPENDGUARD_BUDGET_ID) restartPolicy: OnFailureWhat you see on the dashboard
Section titled “What you see on the dashboard”- Estimated $ per task, per workspace — labelled with
pricing_version. - Credits consumed (always present, even for unknown-plan rows).
reservation_source = subscription_meterfilter groups Genspark spend alongside Devin and the Claude Code Pro / Codex subscription-meter rows (advisory rows that skip the BYOK ledger).- Plan-level pivot:
plusvsprovspremiumvs unknown. - Task-category breakdown (
research/code_generation/ …) when the vendor populates it.
Implementation pointers
Section titled “Implementation pointers”- Crate scaffold + Cargo.toml:
services/importer_genspark/Cargo.toml— workspace-excluded,publish = false,livefeature gated. - Pure conversion:
services/importer_genspark/src/import_record.rs(import_record_to_audit_row) — no I/O, no clock read. - Price table:
services/importer_genspark/src/credit_price_table.rs— saturating arithmetic, rejects NaN/Infinity/negative inputs. - CloudEvent builder:
services/importer_genspark/src/cloudevent_envelope.rs— golden-tested viatests/golden/cloudevent_v1alpha1_*.json. - Fixture loader:
services/importer_genspark/src/fixture_loader.rs— hard-rejects non-synthetic IDs at parse time (T9). - Live client:
services/importer_genspark/src/live/client.rs— rustls-onlyreqwestwrapper with typed 401/403/429/5xx errors plus a strict three-way token gate (missing / empty / too-short). - Migration:
services/ledger/migrations/0061_import_source_widen_genspark.sql— widens the existingaudit_outbox.import_sourceCHECK to includegenspark_team_api.
For the full design rationale + slice-level breakdown, see
docs/specs/coverage/D16_genspark_importer/design.md.