Skip to content

API keys: show when each key was last used #1138

Open

Description

@AnthonyRonning

Context

API users have asked, in different ways, which of their keys are still in use. The API keys settings page shows each key's name and creation date, but nothing about use, so people can't tell which keys are safe to delete or whether a key they think is retired is still making requests.

User intent: on the API keys page, each key shows when it was last used (or "Never used"), so a user can clean up stale keys and spot unexpected use.

This is the first step of a larger direction: the billing server owns per-key usage (this issue), then per-key spend and user-set budgets (#1139). The key secret stays in the enclave; only a non-secret key ID crosses the boundary.

Thanks to @benthecarman, whose #1123 got this started and whose UI work ("Last used …" / "Never used" on each key row) is a good model for the page. That PR records last_used_at in OpenSecret's database from the auth middleware. Looking at where this is headed, we'd rather have the billing server own per-key usage, so the same data can later power spend tracking and budgets without a second pipeline.

Likely approach

The split:

Enclave (OpenSecret) Billing server
Key secret and hash, key name Per-key usage records and "last used"
Create, list, delete, authenticate Reporting per-key usage to the signed-in user

OpenSecret

  1. Add a random, non-secret public_id UUID to user_api_keys, backfilled for existing keys. Not the serial id (enumerable, leaks key counts) and not key_hash (derived from the secret, never leaves the enclave).
  2. Select public_id in the existing API-key auth lookup (services/opensecret/src/db.rs, get_user_by_api_key_hash) and attach it to the request next to the user, so handlers know which key made the call. No extra query.
  3. Add an optional api_key_id to the SQS UsageEvent (services/opensecret/src/sqs.rs) and fill it in build_usage_event (services/opensecret/src/web/openai.rs) for API-key requests.
  4. Return public_id from the list API keys endpoint, and add it to the ApiKey types in both SDKs.

Billing server (separate repository)

  1. Accept the optional api_key_id on usage events and store it with each usage record.
  2. Serve the signed-in user's per-key last-used time, e.g. GET /v1/maple/api-keys/usage, behind the same token auth as the existing /v1/maple/* routes.

Research UI: list keys from OpenSecret, fetch usage from the billing server, and join on public_id. Keys the billing server has never seen show "Never used". If the billing call fails, the page still lists keys and omits the last-used line.

The new event field is optional on both sides, so OpenSecret and the billing server can deploy in either order.

Decisions to make

  1. What "last used" means. Only billed routes send usage events: chat completions, System One and embeddings. Speech, transcription and model listing don't, so keys used only for those would show "Never used". Options: count billed use only and label it that way (simplest), or send a zero-cost activity event from those routes. publish_usage_event_internal skips zero-token events today.
  2. Precision shown. Usage events are processed asynchronously, so "last used" can lag by however long the queue takes. A relative time ("2 hours ago") hides this; an exact timestamp invites questions.
  3. Repository guidance. AGENTS.md and services/opensecret/AGENTS.md say usage truth stays in OpenSecret. Token counts still originate in OpenSecret and the billing server already computes cost from them, so this extends the existing split. The wording should be updated in the same change so it isn't read as a violation.

Acceptance criteria

  • Each key on the API keys page shows its last-used time or "Never used".
  • No key secret, key hash or key name leaves the enclave; only public_id is sent to the billing server.
  • The API-key auth path makes no additional database writes or round trips.
  • Requests rejected by billing or plan checks do not count as use (they publish no usage event).
  • Existing keys get a public_id by migration; new keys get one at creation.
  • An older SDK or client that ignores the new field keeps working.

Non-goals

Current implementation references

  • API-key auth: services/opensecret/src/web/openai_auth.rs (validate_openai_auth)
  • Key model and migration: services/opensecret/src/models/user_api_keys.rs, services/opensecret/migrations/2025-08-21-173429_user_api_keys
  • Usage events: services/opensecret/src/sqs.rs, publish_usage_event_internal in services/opensecret/src/web/openai.rs
  • List/create endpoints: services/opensecret/src/web/protected_routes.rs
  • UI: apps/maple-research/frontend/src/components/settings/api/ApiKeysSettings.tsx
  • Earlier work: feat: show last-used timestamp on API keys settings page #1123

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions