# Spendline integration — instructions for your coding agent

> **For the human reading this:** give this file to your coding agent (Claude Code,
> Cursor, Copilot, etc.) with a message like:
>
> "Read https://www.spendline.ai/integrate.md and integrate this app with Spendline.
> My Spendline URL is `https://www.spendline.ai` and my Spendline API key is in the
> environment as `SPENDLINE_API_KEY`."
>
> That's all you need to do. Everything below is written for the agent.
> Two values it needs from you: your **Spendline URL** (`https://www.spendline.ai`
> unless you were given a pilot/self-hosted URL) and your **Spendline API key**
> (Dashboard → API Keys). Never paste the key into chat — put it in your `.env`.

---

You are integrating this application with **Spendline**, an AI spend-governance
proxy. Spendline sits between this app and its AI providers (OpenAI, Anthropic,
Google, xAI, Mistral, and OpenAI-compatible hosts like DeepSeek, Qwen, Together AI,
Fireworks AI, and Groq). The app keeps its existing provider API keys; you repoint
its AI clients at the proxy and add attribution headers so every call is budgeted,
policy-checked, and recorded against the right customer and team.

Real apps call several providers from several places. Do not assume there is one
client in one file. Work through the phases below **in order**, and do not skip
Phase 1.

`<SPENDLINE_URL>` below means the Spendline URL the human gave you
(default `https://www.spendline.ai`).

## Phase 1 — Audit (before changing anything)

Find **every** place this app talks to an AI provider:

- SDK client constructions: `new OpenAI(`, `OpenAI(` (Python), `new Anthropic(`,
  `Anthropic(` (Python), `AsyncOpenAI`, `AsyncAnthropic`, wrappers from LangChain /
  LlamaIndex / Vercel AI SDK that accept a `baseURL`/`base_url`.
- Raw HTTP calls to provider hosts: `api.openai.com`, `api.anthropic.com`,
  `generativelanguage.googleapis.com`, `api.x.ai`, `api.mistral.ai`,
  `api.deepseek.com`, `api.together.xyz`, `api.fireworks.ai`, `api.groq.com`.
- Env vars and config files that hold provider base URLs.

Produce an inventory table before editing: file, line, provider, API shape
(chat-completions / messages / responses / embeddings / other), streaming or not.
Show it to the human in your final report.

## Phase 2 — Classify each call site

Spendline accepts exactly **three request shapes**:

| Shape | Endpoint | Notes |
|---|---|---|
| OpenAI chat-completions | `<SPENDLINE_URL>/v1/chat/completions` | All OpenAI-compatible providers. Streaming supported. The **model name** picks the provider (`gpt-*` → OpenAI, `gemini-*` → Google, `grok-*` → xAI, `mistral-*` → Mistral, etc.) |
| Anthropic messages | `<SPENDLINE_URL>/v1/messages` | Claude models. Streaming supported. |
| OpenAI Responses API | `<SPENDLINE_URL>/v1/responses` | OpenAI models only. **Non-streaming only** — `stream: true` is rejected with a clear error. Built-in tools like `web_search` work, and their cost is included in the recorded spend. |

**Leave unsupported shapes alone**: embeddings, vector/knowledge-base search,
image/audio generation, and Google's native `generateContent` keep pointing
directly at their provider. Mark them "left as-is" in your report — do not route
them through Spendline.

## Phase 3 — Repoint the supported clients

- **OpenAI SDK** (and any OpenAI-compatible client): set `baseURL` / `base_url` to
  `<SPENDLINE_URL>/v1`. The SDK appends `/chat/completions` (and `/responses`) itself.
- **Anthropic SDK**: set `baseURL` to `<SPENDLINE_URL>` — **no `/v1`**. The
  Anthropic SDK appends `/v1/messages` itself. This asymmetry between the two SDKs
  is real; do not "fix" one to match the other.
- **Raw HTTP**: replace the provider host with the matching Spendline endpoint
  from the table above.
- **Keep the app's existing provider API keys exactly where they are** (OpenAI's
  `Authorization: Bearer`, Anthropic's `x-api-key`). Spendline forwards them to the
  provider; it does not replace them.
- If clients are constructed in many places, prefer extracting one shared
  factory/module so the Spendline config lives in one spot — but only if that
  refactor is safe and small in this codebase.

**Framework wrappers** (same rules apply; only the config surface differs):

- **LangChain**: `ChatOpenAI` takes `base_url`/`baseURL` and
  `default_headers`/`defaultHeaders`; `ChatAnthropic` takes the Anthropic base URL
  (no `/v1`) via its client options. Set the Spendline headers there.
- **Vercel AI SDK**: `createOpenAI({ baseURL: '<SPENDLINE_URL>/v1', headers: {...} })`
  and `createAnthropic({ baseURL: '<SPENDLINE_URL>', headers: {...} })`.
- **Other wrappers** (LlamaIndex, LiteLLM, etc.): find where the wrapper exposes
  the underlying client's base URL and default headers and apply the same values.
  If a wrapper offers no way to set headers, call that out in your report instead
  of forcing it.
- In frameworks, `x-customer-id` still must vary per request — pass headers at
  call time where the framework supports it, or construct the model client where
  the customer is known.

## Phase 4 — Add attribution headers to every routed call

Send these on every request through Spendline (SDKs: `defaultHeaders` /
`default_headers`; per-request overrides where needed):

- `x-spendline-key` — the Spendline API key, read from env (`SPENDLINE_API_KEY`).
  **Never** put it in `x-api-key`: Anthropic uses `x-api-key` for the Claude
  provider key, so the Spendline key would be sent to Anthropic and rejected.
- `x-agent-id` — a stable name for the bot/agent/feature making the call
  (e.g. `support-bot`, `doc-summarizer`). One per agent, not per request.
- `x-customer-id` — the end-user/customer who triggered **this** request. Set it
  **per request**, not per process — a hardcoded value makes per-customer cost
  tracking useless. If the call site doesn't know the customer, thread it through
  from the request context; ask the human if there is genuinely no customer concept.
- `x-spendline-tags` — a JSON object that must include a cost-centre key: one of
  `cost_center`, `costCenter`, `department`, or `team`.
  Example: `{"team":"support","env":"prod"}`.

Optional, for richer dashboards: `x-workflow-id` (groups a multi-step run),
`x-step-name` (labels a step), `x-source-route` (the app route that triggered the
call).

Calls missing the required fields are rejected with HTTP 400
`missing_required_attribution` when attribution enforcement is on — send them from
day one.

## Phase 5 — Resilience and config

- Put the Spendline base URL and key in env config (`SPENDLINE_URL`,
  `SPENDLINE_API_KEY`) behind **one on/off flag** (e.g. `SPENDLINE_ENABLED`) so the
  proxy can be disabled without a code change.
- On a **network error, timeout, or 5xx** from Spendline, retry once directly
  against the provider so the app stays up.
- **Never** fall back on `401`, `403`, or `402`. `402` is a deliberate budget
  block and `403` a policy block — routing around them defeats the point.
  Surface these to the app as a clear error instead.

## Phase 6 — Verify

For each provider the app uses, make one real call through Spendline — one
streaming and one non-streaming where the app uses both. (Responses API:
non-streaming only.) Then check the results:

| Result | Meaning | Fix |
|---|---|---|
| 200 + normal response | Connected; call appears in the Spendline dashboard within seconds | Done |
| 401 | Spendline key rejected | Fresh key from Dashboard → API Keys into `SPENDLINE_API_KEY` |
| 400 `missing_required_attribution` | Required headers missing | Send `x-agent-id`, `x-customer-id`, and `x-spendline-tags` with a cost-centre key |
| 402 | A budget blocked the call | Expected behavior — check Dashboard → Budgets |
| 403 | A policy/governance rule blocked it | Check Dashboard → Policies and Governance |
| Other 4xx/5xx | Usually a wrong provider key or unknown model name | Check the provider key and model |

Ask the human to confirm the calls appear in their Spendline dashboard.

## Phase 7 — Report

Finish with a summary for the human: the Phase 1 inventory table with a status
column — **repointed**, **left as-is (unsupported shape)**, or **needs a
decision** — plus the env vars you added and the verification results per
provider.

## If you get stuck

If you cannot complete the integration or a verification call keeps failing after
applying the fixes above, don't loop — produce a **diagnostic block** for the
human containing: the Phase 1 inventory table, the exact failing request (URL,
headers **with all key values redacted**, model) and the full response body,
and the SDK/framework names and versions involved. Tell the human to email it to
**fida@spendline.ai** — that block contains everything needed to answer in one
reply.
