Documentation

Till lets you create scoped API keys for AI agents with activation limits plus conservatively reserved token and spend budgets. Give agents scoped credentials instead of distributing upstream keys directly.

New to Till? New accounts are manually onboarded during the controlled beta. Request beta access; existing customers can use the dashboard.

Quick Start

  1. Obtain beta access Request controlled-beta onboarding. Existing customers should authenticate with their tenant admin key in the dashboard. Save any issued admin key securely; it is a bearer credential.
  2. Install the Python SDK pip install till-ac - or use the REST API directly.
  3. Create a scoped key Set limits and get a disposable key your agent can use as a drop-in replacement.
# Connect a provider once (repeat for every account you use) curl -X PUT https://api.till.ac/admin/providers/openai \ -H "Authorization: Bearer till_admin_..." \ -H "Content-Type: application/json" \ -d '{"upstream_key":"sk-proj-...","is_default":true}' # Create one provider-agnostic scoped key curl -X POST https://api.till.ac/admin/keys \ -H "Authorization: Bearer till_admin_..." \ -H "Content-Type: application/json" \ -d '{"max_activations":50,"max_tokens":100000,"max_spend_cents":500}'

Python SDK

The Python SDK supports key administration and OpenAI-compatible proxy use. The dashboard and REST examples above are the canonical provider-connection workflow while automatic-routing helpers propagate to client releases.

pip install till-ac

The SDK provides:

Full SDK docs and source: pypi.org/project/till-ac

Scoped Keys

A scoped key is a disposable, provider-agnostic proxy key with enforced limits. The format:

till_sk_<lookupKey>.<encryptedRoutingReference> # Example (truncated): till_sk_mz90mM7bQ...VtkE.aGVsbG8gd29ybGQ...

When an agent makes a request with a scoped key, Till:

  1. Extracts the lookup key and finds the key record in the database
  2. Checks activation and previously recorded token/spend usage
  3. Selects a configured provider from the route, model, and account default
  4. Decrypts that provider connection in service memory (AES-256-GCM)
  5. Forwards the request to the selected upstream provider
  6. Tracks usage from the response and increments counters

Usage Controls

Every scoped key can have up to three controls. Their enforcement timing differs:

You can set any combination. For any token- or spend-budgeted generation request, declare an output cap using max_tokens, max_completion_tokens, max_output_tokens, or Google generationConfig.maxOutputTokens. The conservative request bound must fit the remaining budget. When a request asks the upstream for several generations at once - via OpenAI n or best_of, or Google candidateCount (under generationConfig) - Till multiplies the reserved output bound by that count, because every generation honors the output cap and settles its usage against the budget; lower the generation count or the output cap if the multiplied bound no longer fits the remaining budget. When a Google Gemini request declares an explicit reasoning ceiling with generationConfig.thinkingConfig.thinkingBudget, Till folds that positive token count into the reserved output bound, because Gemini bills thinking tokens at the output rate and does not count them against maxOutputTokens; a dynamic budget (-1) names no finite ceiling and is not reserved. Background-mode requests (background: true) are rejected on token- or spend-budgeted keys because their usage is reported only on retrieval and never settles against the budget; use a key with only an activation limit for background generations.

Providers

Till supports 12 AI providers. All OpenAI-compatible providers use the same proxy path format:

Credential Storage Model

Provider credentials are AES-256-GCM encrypted at rest with application-held key material and authenticated tenant/provider binding. The database stores:

Provider credential plaintext and complete scoped tokens are never stored or returned after creation. Database contents alone are insufficient to decrypt provider credentials without the separate application encryption key. This is not a zero-knowledge system: Till decrypts the selected credential while proxying, and compromise of a scoped token, the running service, or both database and encryption key remains in the threat model.

Authentication

All admin API requests require your admin key in the Authorization header:

# Admin operations (create keys, check status, etc.) Authorization: Bearer till_admin_your_key_here

The proxy endpoint uses the scoped key itself as authorization - your agent just uses it like a normal API key.

API Endpoints

Base URL: https://api.till.ac

Connect a provider

PUT /admin/providers/openai { "upstream_key": "sk-proj-...", "is_default": true }

Use GET /admin/providers to list configured connections and DELETE /admin/providers/:provider to remove one. Credentials and ciphertext are never returned.

Create a scoped key

POST /admin/keys { "max_activations": 50, "max_tokens": 100000, "max_spend_cents": 500, "expires_at": "2026-05-15T00:00:00Z", "metadata": {"name": "agent-task-42"} }

Only max_activations is required after at least one provider connection exists. The key automatically uses current and future account connections. The optional expires_at field sets a time-based expiry (ISO 8601).

List keys

GET /admin/keys GET /admin/keys?status=active GET /admin/keys?provider=openai&search=agent-name

Filter by status (active, exhausted, revoked, expired), provider, or search (searches key IDs and metadata).

Check key status

GET /admin/keys/:id

Update a key

PATCH /admin/keys/:id { "metadata": {"name": "renamed-agent"}, "ip_whitelist": ["10.0.0.0/8"], "max_activations": 200, "max_tokens": 50000, "max_spend_cents": 1000, "expires_at": "2026-12-31T23:59:59Z", "name": "renamed-agent" }

Update a key's limits, metadata, or IP allowlist without resetting counters. All fields are optional. New limits must not be below current usage. Set max_tokens, max_spend_cents, or expires_at to null to remove that ceiling. Raising or removing an exhausted limit, or extending a lapsed expiry, reactivates the key immediately with its counters intact; revoked keys stay revoked.

Clone a key

POST /admin/keys/:id/clone { "max_activations": 100 }

Creates a fresh automatic key with the same routing mode, limits, IP allowlist, and metadata as the source, but with reset counters. Automatic keys do not require a provider credential. Legacy fixed-provider keys still require upstream_key. If the source key's expiry has already passed, include a future expires_at (or null to remove the expiry) — the clone is rejected otherwise, since inheriting the lapsed expiry would return a key that immediately fails as expired. The new key's metadata includes cloned_from.

Key usage

GET /admin/keys/:id/usage

Returns a structured usage breakdown with remaining and percent for activations, tokens, and spend.

Reactivate a key

POST /admin/keys/:id/reactivate { "max_activations": 100 }

Resets counters on an exhausted or expired key. The agent keeps its existing scoped key token. Optionally update limits in the body. If the key's expiry has already passed, include a future expires_at (or null to remove the expiry) — reactivation is rejected otherwise, since keeping the lapsed expiry would return a key that immediately fails as expired.

Revoke a key

DELETE /admin/keys/:id

Bulk revoke

POST /admin/keys/bulk-revoke { "ids": ["key-id-1", "key-id-2", "key-id-3"] }

Revoke up to 100 keys in a single call. Returns per-key status (revoked, not_found, or already_revoked). Useful for rotating upstream keys or decommissioning agent fleets.

Key stats

GET /admin/keys/stats

Returns aggregate counts (total, active, exhausted, revoked, expired), total activations/tokens/spend used, and a legacy/default-provider byProvider grouping. Automatic keys can route to multiple providers, so this grouping is not a per-request audit log.

Date range filtering

GET /admin/keys?created_after=2026-05-01T00:00:00Z&created_before=2026-05-07T00:00:00Z

Filter key listings by creation date using created_after and created_before query parameters (ISO 8601). Combine with other filters for time-based auditing.

Key introspection (scoped key)

GET /till/validate Authorization: Bearer till_sk_your-scoped-key

Read-only key introspection — returns valid (the top-level go/no-go boolean: true while the key can serve a request, false once it is revoked, expired, or exhausted, its tenant is inactive or out of monthly quota, or it has no configured provider to route to), key status, usage breakdown with percent per dimension, max_usage_percent, and a healthy boolean. healthy is narrower than valid: it is true only when the key is valid and below 80% on every limit dimension, so a key past 80% reads healthy: false while still being valid: true and fully usable. Branch a pre-flight go/no-go on valid, not healthy — treating healthy: false as a stop would abandon a working key with capacity still remaining. The response also carries bottleneck (which limit dimension will exhaust first), estimated_remaining_requests, exhaustion_eta, the configured limits (including ip when an IP allowlist is set) and ip_whitelist ranges, provider details, and quota info. It also returns warnings — a structured array of human-readable reasons the key is not fully healthy (why valid is false, such as "tenant status is …", "key has been revoked", or the "no provider connections are configured…" pointer, plus any limit dimension at or above 80% used), or null when none apply. This is the structured pre-flight reason the rejection notes below point at when they say GET /till/validate surfaces a stop ahead of time; read it to explain a valid: false result to the operator rather than parsing status_line. Does not consume an activation. Agents should call this for pre-flight checks before dispatching work.

Provider discovery

GET /till/providers

No auth required. Returns a list of all supported providers with id, name, base_url, auth_style, openai_compatible flag, and models_with_pricing (how many of that provider's models Till has a spend-tracking price for). SDKs and agents can dynamically discover which providers are available; a provider with models_with_pricing: 0 has no priced models yet, so a spend-limited key routed to it would be rejected on the first generation request (spend limits require a known model price) — pre-flight the exact model with GET /till/pricing?provider=<id>.

Pricing discovery

GET /till/pricing GET /till/pricing?provider=openai GET /till/pricing?provider=openai&model=gpt-4o

No auth required. Returns pricing data used for spend tracking. Without filters, lists all models with pricing. Filter by provider to see one provider's models. Add model to check pricing for a specific model — returns 404 if unknown. Each entry includes input_per_1k_cents and output_per_1k_cents. SDKs can verify spend tracking accuracy before sending requests.

Proxy Usage

To use a scoped key, keep the same request paths your SDK already uses and point the base URL to Till:

# Instead of: base_url = "https://api.openai.com/v1" api_key = "sk-proj-your-real-key" # Use: base_url = "https://api.till.ac/v1" api_key = "till_sk_your-scoped-key"

Common passthrough paths:

The proxy transparently forwards requests, tracks usage, and enforces limits. From the agent's perspective, only the base URL and API key change.

Choosing a provider

A provider-agnostic scoped key routes each request to one of your connected providers automatically. Native routes are detected from the path — /v1/messages goes to Anthropic, /v1beta/…:generateContent to Google, and Cohere's /v2/ and native /v1/ routes to Cohere. OpenAI-compatible /v1/ routes are resolved from the request's model, falling back to your default connection when the model alone does not identify a single provider.

When your account has more than one compatible connection and you want a specific one, send the x-till-provider request header with a provider id (case-insensitive):

curl -X POST https://api.till.ac/v1/chat/completions \ -H "Authorization: Bearer till_sk_your-scoped-key" \ -H "x-till-provider: openrouter" \ -H "Content-Type: application/json" \ -d '{"model":"...","messages":[...]}'

Till validates the choice before it consumes an activation, so a routing rejection costs nothing. The request is rejected when the header names an unsupported id (list valid ids with GET /till/providers), a provider you have not connected (add it with PUT /admin/providers/:provider or from the dashboard), or a provider that cannot serve that route's request format. A legacy fixed-provider key ignores automatic routing and accepts an x-till-provider value only when it matches its embedded provider.

Response Headers

Every proxied response includes headers your agent can use to track its own key state:

Upstream Failure Responses

When Till has already dispatched your request to the selected provider but the attempt does not complete, it reports the failure mode as a distinct HTTP status and error code so an agent can choose whether to retry without parsing free text. Both responses carry a JSON error object with code, provider, key_id, and key_name (when set), plus a link header pointing back to this Proxy Usage section.

Because both failures happen after the request is dispatched upstream, the activation is counted, not refunded — Till refunds an activation only for a request it rejects locally before dispatch. Token and spend budgets are not charged when the provider reports no usage.

Local Rejection Responses

These are the pre-dispatch rejections the paragraph above refers to: Till stops the request before any provider call, so the activation is refunded and no token or spend budget is charged. Each carries a JSON error object with a machine-readable code (plus key_id and key_name when the key is known) and a link header pointing at the relevant docs section, so an agent can branch on the stop without parsing free text. A budget rejection (token_limit_exceeded / spend_limit_exceeded) also carries a retriable boolean, because the same code covers both a spent envelope and a merely oversized or malformed request. The six groups differ in what the caller should do next.

Plans

These are controlled-beta platform prices for new subscriptions. Provider inference is billed separately under your own provider accounts. Eligible existing paid beta subscriptions retain their founding price under the published price-protection policy.

All plans include all 12 providers; activation, token, estimated-spend, expiry, and IP controls; the dashboard; and the Python SDK. The plans differ by key and monthly activation capacity, not by control type. Subscriptions do not include provider usage, uptime guarantees, compliance certifications, SSO, or dedicated support.

FAQ

What counts as one activation?

One proxied HTTP request equals one activation. A 20-message conversation equals 20 activations (one per API call). A streaming response is still one activation.

Can I set only one type of limit?

Yes. Activation-only, spend-only, all three, or any pair are supported. Token- or spend-budgeted generation requests require a declared output-token cap so Till can reserve conservative capacity before dispatch. Unset dimensions have no per-key limit beyond the plan's monthly activation cap.

How accurate is dollar tracking?

Till's table contains 250+ provider/model identifiers. Spend-limited requests require a matching model price, reserve conservative capacity before dispatch, and settle to provider-reported usage. This remains an operational estimate, not a substitute for the provider invoice: cached tokens, batch discounts, provider price changes, and missing stream usage can differ. Keep provider-side budgets and invoice monitoring enabled.

What happens when a key is exhausted or expires?

Any request using an exhausted or expired key gets a clear error response with the reason (activation limit, token limit, dollar limit reached, or TTL expired). The agent can handle this gracefully. Use the clone endpoint to quickly create a replacement with the same config.

Can I use Till with frameworks like LangChain or LlamaIndex?

Many do. A framework must allow a custom base URL and API key. Till forwards provider paths and common streaming responses, but compatibility is not guaranteed for every SDK feature or provider endpoint. Test the exact combination before production use.