Skip to content

Cost, audit, ClawHub & credentials API

Cost tracking and budget status, audit log query/verify, the ClawHub marketplace, and the encrypted credential and gcloud config stores.

These endpoints cover the operational and security surface of a running Revka gateway: how much your agents are spending, the tamper-evident audit trail of security-relevant events, the ClawHub skill marketplace, and the two metadata-only stores that back workflow credentials and Cloud Run configuration. Use them to drive the dashboard’s cost and audit pages, scrape spend into your own tooling, install community skills, or wire workflow steps to encrypted credentials.

Most routes here require a bearer token from the pairing flow. A few are deliberately different: GET /api/cost is unauthenticated read-only telemetry, while the cost-ingest and credential-resolve endpoints require the internal service token rather than a user bearer token — these are called out explicitly. For the configuration side of cost and observability, see Cost tracking & budgets and Observability & tracing; for the audit log internals, see Audit log.

All /api/* routes share a 64 KiB request body cap and a configurable timeout (default 30 s, env REVKA_GATEWAY_TIMEOUT_SECS), with per-route overrides where noted.

Revka persists every LLM API call’s token counts and computed USD cost to a JSONL ledger at <workspace>/state/costs.jsonl. The tracker rolls up session, daily, and monthly totals and breaks spend down by model, by agent, and by runtime source (gateway, channel, sidecar). A process-global singleton ensures the gateway and channel paths share a single ledger and a single budget check. A legacy .revka/costs.db store is migrated automatically on first start, and malformed JSONL lines are skipped with a warning rather than failing the read.

Cost is computed as (tokens / 1_000_000) × price_per_million, using the per-model prices configured under [cost.prices]. Model lookup is fuzzy: Revka tries an exact match, then provider/model, then the suffix after /, then a prefix match. When no price entry matches, the record is stored at zero cost (with a debug log).

GET /api/cost returns the cost summary. It requires no auth — it is read-only operator telemetry consumed by the dashboard. When cost tracking is disabled, it returns a fully zeroed summary (with budget.state set to "disabled") rather than an error, so dashboards degrade gracefully.

GET /api/cost
{
"cost": {
"session_cost_usd": 0.42,
"daily_cost_usd": 1.87,
"monthly_cost_usd": 12.34,
"total_tokens": 184320,
"request_count": 57,
"by_model": { },
"by_agent": { },
"by_source": { },
"budget": {
"enabled": true,
"daily_limit_usd": 10.0,
"monthly_limit_usd": 100.0,
"warn_at_percent": 80,
"daily_remaining_usd": 8.13,
"monthly_remaining_usd": 87.66,
"daily_percent": 18.7,
"monthly_percent": 12.34,
"state": "ok"
}
}
}

The by_model, by_agent, and by_source breakdowns cover the current process lifetime only; daily_cost_usd and monthly_cost_usd are read from the full ledger. Records with no source tag are bucketed under "runtime" in by_source.

To feed token usage from a trusted sidecar (for example the operator-mcp runtime) into the shared ledger, POST /api/cost/usage. Because this endpoint mutates the budget ledger, it is gated by the service token — present it in the X-Revka-Service-Token header, not as a bearer token.

POST /api/cost/usage
X-Revka-Service-Token: <service-token>
Content-Type: application/json
{
"model": "gpt-4o",
"provider": "openai",
"input_tokens": 1000,
"output_tokens": 250,
"source": "sidecar",
"agent_id": "my-agent",
"agent_title": "My Agent"
}

Only model is required. provider and source default to "sidecar" when omitted or blank. A successful ingest returns { "recorded": true, "usage": { ... } }; if cost tracking is disabled the call returns 200 with { "recorded": false, "reason": "cost tracking disabled" }, and a request without a valid service token returns 401.

Budgets are configured in the [cost] section and enforced per process (not per session) by the global tracker. The warn and exceeded thresholds are compared against the projected total (current usage plus the estimated cost of the pending call), not just the current usage.

[cost]
enabled = true
daily_limit_usd = 10.0 # default
monthly_limit_usd = 100.0 # default
warn_at_percent = 80 # warn at 80% of a limit
allow_override = false # allow a --override flag to bypass
[cost.enforcement]
mode = "warn" # "warn" | "block" | "route_down"
route_down_model = "gpt-4o-mini" # used with "route_down"
reserve_percent = 10 # reserve 10% for critical ops
[cost.prices]
"claude-sonnet-4-20250514" = { input = 3.0, output = 15.0 } # USD per 1M tokens
"gpt-4o" = { input = 2.5, output = 10.0 }

The enforcement mode controls what happens when a limit is crossed:

ModeBehavior when a budget is exceeded
warnLog a warning and allow the request (default)
blockReject the request
route_downSwitch to the cheaper route_down_model

The budget.state field in the cost summary reflects the current standing: "ok", "warning" (past warn_at_percent), "exceeded" (past a limit), or "disabled". See Cost tracking & budgets for the full configuration reference and the pricing-table format.

The audit log is an append-only, tamper-evident record of security-relevant events: command executions, file access, config changes, auth successes and failures, policy violations, and generic security events. Each entry is linked into a SHA-256 hash chain (entry_hash = SHA-256(prev_hash || canonical_JSON_of_content)) with a monotonically increasing sequence, so modifying any past entry invalidates every entry after it. Entries can optionally be signed with HMAC-SHA256. The log requires [security.audit] enabled = true in config (the default).

[security.audit]
enabled = true # default
log_path = "audit.log" # relative to the revka dir
max_size_mb = 100 # rotate at this size
sign_events = false # optional HMAC-SHA256 signing

GET /api/audit returns recent events, newest first, behind a bearer token.

GET /api/audit?limit=50&event_type=command_execution&since=2026-01-01T00:00:00Z
Authorization: Bearer <token>
ParameterTypeDefaultMeaning
limitint50 (max 500)Maximum events to return
event_typestringFilter by type (see below)
sinceRFC 3339Lower bound on the event timestamp

event_type accepts command_execution, file_access, config_change, auth_success, auth_failure, policy_violation, or security_event. The response wraps the events with a count and an audit_enabled flag:

{
"events": [
{
"timestamp": "2026-06-18T10:00:00Z",
"event_id": "<uuid>",
"event_type": "command_execution",
"actor": { "channel": "telegram", "user_id": "123", "username": "@alice" },
"action": { "command": "ls -la", "risk_level": "low", "approved": false, "allowed": true },
"result": { "success": true, "exit_code": 0, "duration_ms": 15 },
"security": { "policy_violation": false, "rate_limit_remaining": 19, "sandbox_backend": "firejail" },
"sequence": 42,
"prev_hash": "<64 hex>",
"entry_hash": "<64 hex>",
"signature": "<64 hex>"
}
],
"count": 1,
"audit_enabled": true
}

The signature field is present only when sign_events = true. When audit logging is disabled, the endpoint returns 200 with an empty events array, count: 0, and audit_enabled: false — so a dashboard never errors out just because auditing is off.

GET /api/audit/verify walks the on-disk log and validates the hash chain end to end. It is the fastest way to detect tampering: if any entry was edited, deleted, or reordered, verification fails.

GET /api/audit/verify
Authorization: Bearer <token>
{ "verified": true, "entry_count": 1842 }

On a broken chain (or when auditing is not enabled), it returns verified: false with an error describing the failure:

{ "verified": false, "error": "chain broken at sequence 41" }

To enable HMAC signing, set a 32-byte key (64 hex characters) in the REVKA_AUDIT_SIGNING_KEY environment variable before the gateway starts — the logger reads it at construction time, and startup fails if the key is missing or the wrong length. Log rotation resets the chain to genesis, so each rotated .N.log archive is independently verifiable. For the chain format, signing setup, and CLI verification, see Audit log.

ClawHub is the skill marketplace at clawhub.ai. These routes proxy the marketplace API so you can search, browse trending skills, view detail, and one-click-install a skill into your local Kumiho memory and workspace. All four routes require a bearer token.

GET /api/clawhub/search?q=rust+debugging&limit=20
GET /api/clawhub/trending?limit=20
GET /api/clawhub/skills/{slug}
POST /api/clawhub/install/{slug}
Authorization: Bearer <token>
ParameterWhereDefaultMeaning
qqueryrequired (search)Search terms
limitquery20Maximum results
slugpathClawHub skill slug

GET /api/clawhub/skills/{slug} returns the skill detail and, when available, attaches the rendered SKILL.md under a skill_md field. POST /api/clawhub/install/{slug} fetches the skill’s SKILL.md, writes it to ~/.revka/workspace/skills/<slug>.md, registers the skill as an item in your Kumiho memory_project’s Skills space (tagging the revision published), and returns:

{
"installed": true,
"slug": "rust-debugging",
"name": "Rust Debugging",
"kref": "kref://CognitiveMemory/Skills/rust-debugging.skill",
"description": "Systematic Rust debugging workflow"
}

If ClawHub is disabled, the routes return 400 with {"error": "ClawHub integration disabled"}; an unreachable marketplace returns 502.

[clawhub]
enabled = true # default
api_url = "https://clawhub.ai" # default — base URL for the ClawHub API
api_token = "clh_..." # optional; only required to publish skills
KeyTypeDefaultMeaning
enabledbooltrueEnable the ClawHub integration
api_urlstring"https://clawhub.ai"Base URL of the ClawHub API (point at a private instance if needed)
api_tokenstring?unsetclh_… token; needed only for publishing — anonymous browsing and installing work without it

Installed skills use the same registration path as skills created in the dashboard, so they appear in the Skills, tools & integrations pages and the skills API.

Workflow steps that call authenticated services bind to an auth profile: an encrypted credential stored at rest with ChaCha20-Poly1305 AEAD via the gateway’s SecretStore. The store is split into two surfaces with different trust levels — a bearer-auth metadata view, and a service-token-only resolve path that is the only way to decrypt a credential.

GET /api/auth/profiles # list metadata (bearer)
POST /api/auth/profiles # create a static-token profile (bearer)
DELETE /api/auth/profiles/{id} # delete a profile (bearer)
POST /api/auth/profiles/{id}/resolve # decrypt the credential (service token only)

GET /api/auth/profiles returns metadata summaries — note the absence of any token field:

{
"profiles": [
{
"id": "github:My Token",
"provider": "github",
"profile_name": "My Token",
"kind": "token",
"account_id": null,
"workspace_id": null,
"expires_at": null,
"created_at": "2026-06-18T10:00:00Z",
"updated_at": "2026-06-18T10:00:00Z"
}
]
}

POST /api/auth/profiles creates a static-token (API-key) profile. The plaintext token is encrypted by the store before persistence and never echoed back; the response is the new metadata summary with 201 Created.

POST /api/auth/profiles
Authorization: Bearer <token>
Content-Type: application/json
{ "provider": "github", "profile_name": "My Token", "token": "ghp_..." }
FieldRequiredNotes
provideryesProvider identifier, e.g. github
profile_nameyesHuman label; combined with provider to form the id
tokenyesRaw bearer / API key (encrypted at rest)
account_idnoOptional account hint
kindnoDefaults to "token"; "api_key" is a synonym. "oauth" is rejected with 400 — OAuth profiles must be created through the /config consent flow

Creation responses worth handling: 400 for missing fields or an unsupported kind, 409 if a profile with the same provider+profile_name already exists, and 429 when the per-IP rate limit trips (10 attempts per 60 s window, then a 5-minute lockout; loopback callers are exempt). The limiter trusts only the socket peer, never X-Forwarded-For.

POST /api/auth/profiles/{id}/resolve decrypts and returns the credential. It requires the X-Revka-Service-Token header (compared in constant time) and sets Cache-Control: no-store on the response so the secret is never cached by intermediaries.

POST /api/auth/profiles/{id}/resolve
X-Revka-Service-Token: <service-token>
{
"token": "ghp_...",
"kind": "token",
"provider": "github",
"profile_name": "My Token",
"expires_at": null
}

An unknown id returns 404; an empty static token returns 410 Gone (with code auth_profile_empty), and an OAuth profile whose expires_at is in the past returns 410 Gone (with code auth_profile_expired), so the runtime fails the step rather than sending a stale credential.

The service token is generated at gateway startup and persisted to <state_dir>/service-token (mode 0600 on POSIX). It is distinct from the user-facing pairing bearer token — see Pairing & authentication and the Security model.

These metadata-only endpoints list and create named gcloud SDK configuration profiles on the host, backing the workflow editor’s Cloud Run / A2A IAM selector. A gcloud configuration references credentials in the local Cloud SDK credential store but is not itself a secret, so responses carry only display metadata — never access, refresh, or identity tokens. Both routes require a bearer token and shell out to gcloud with a 20 s timeout.

GET /api/gcloud/configs
POST /api/gcloud/configs
Authorization: Bearer <token>

GET /api/gcloud/configs runs gcloud config configurations list and returns the parsed configs. If gcloud is not on PATH, it returns available: false (not an error) so the UI can hide the feature:

{
"available": true,
"configs": [
{
"name": "default",
"is_active": true,
"account": "[email protected]",
"project": "construct-498201",
"run_region": "us-central1",
"compute_region": "us-central1"
}
],
"error": null
}

POST /api/gcloud/configs creates a new configuration (via gcloud config configurations create --no-activate) and then sets the supplied properties.

POST /api/gcloud/configs
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "my-config",
"project": "my-gcp-project",
"account": "[email protected]",
"run_region": "us-central1",
"compute_region": "us-central1"
}
FieldRequiredNotes
nameyesASCII alphanumerics plus -, _, .; must start with a letter or digit; max 64 chars
projectyesGCP project ID (core/project)
accountnoAccount email (core/account)
run_regionnoCloud Run region (run/region)
compute_regionnoCompute region (compute/region)

On success the response is the created GcloudConfigSummary with 201. A duplicate name returns 409; a missing gcloud binary returns 503 with code gcloud_not_found; invalid input returns 400 with a specific code (for example gcloud_config_invalid_name).

Method & pathAuthPurpose
GET /api/costnoneCost summary and budget status
POST /api/cost/usageservice tokenIngest sidecar token usage into the ledger
GET /api/auditbearerRecent audit events (filterable)
GET /api/audit/verifybearerVerify the audit hash chain
GET /api/clawhub/searchbearerSearch ClawHub skills
GET /api/clawhub/trendingbearerTrending ClawHub skills
GET /api/clawhub/skills/{slug}bearerClawHub skill detail
POST /api/clawhub/install/{slug}bearerInstall a skill into local Kumiho
GET /api/auth/profilesbearerList credential profile metadata
POST /api/auth/profilesbearerCreate a static-token credential profile
DELETE /api/auth/profiles/{id}bearerDelete a credential profile
POST /api/auth/profiles/{id}/resolveservice tokenDecrypt and return a credential
GET /api/gcloud/configsbearerList gcloud configuration profiles
POST /api/gcloud/configsbearerCreate a gcloud configuration profile