# Set up Tokpad in this project

You are an AI coding agent. The developer asked you to route this project's LLM calls through **Tokpad**: one key for OpenAI and Anthropic models, spend capped by prepaid credit and budgets, and usage visible per tool.

Work through these steps in order. Change as little as possible and show the developer every change.

## 1. Tell the developer what will happen

In a sentence or two: you'll open a Tokpad sign-in for them to approve in their browser, store the key in the project's git-ignored `.env`, point the project's AI clients at Tokpad, and run one test request. Continue unless they object.

## 2. Sign in (about a minute)

Save the script at the end of this file as `tokpad_connect.py` in a temporary directory (not inside the project) and read it. Then, from the project's root, run:

```bash
python3 /tmp/tokpad_connect.py start "<your name, e.g. Claude Code>"
```

It prints a link and a code. Show both to the developer and ask them to open the link, check that the code matches, and approve. They can create a workspace on that page. Then run:

```bash
python3 /tmp/tokpad_connect.py wait
```

- Exit code `0`: connected. It prints the workspace, the models this key can use and the credit left. The key is now in `.env` as `TOKPAD_API_KEY`, next to `TOKPAD_BASE_URL` and `TOKPAD_ANTHROPIC_BASE_URL`. **Never print or commit the key.**
- Exit code `2`: still waiting. Run `wait` again; the code stays valid for 15 minutes.
- Exit code `1`: denied, expired or unreachable. Tell the developer what it printed and stop. If Tokpad can't be reached, send them to https://tokpad.com/#start.

No Python? Use the API directly. `POST https://app.tokpad.com/api/v1/device/start` with `{"client_name": "..."}`, show the developer `verification_uri_complete` and `user_code`, then `POST /api/v1/device/token` with `{"device_code": "..."}` every `interval` seconds until it returns HTTP 200. Errors: `authorization_pending` keep waiting, `slow_down` wait 5 seconds longer, `access_denied` or `expired_token` stop. Write the returned `api_key`, `base_url` and `anthropic_base_url` to `.env` without echoing them.

## 3. Find where the project calls a model

Search for LLM clients: OpenAI-compatible SDKs, the Anthropic SDK, HTTP calls to provider endpoints, and framework settings (LangChain, LlamaIndex, Vercel AI SDK and similar). List the call sites and the model names they use before editing anything.

## 4. Point those clients at Tokpad

- **OpenAI-compatible clients**: base URL from `TOKPAD_BASE_URL`, API key from `TOKPAD_API_KEY`.
- **Anthropic SDK**: base URL from `TOKPAD_ANTHROPIC_BASE_URL`, API key from `TOKPAD_API_KEY`.
- Read both from the environment; never hard-code them.
- Keep the model names the project already uses. Tokpad accepts plain names such as `gpt-6.1-sol` or `claude-sonnet-5-5`. If a model isn't in the list `wait` printed, keep the change but tell the developer: Tokpad will refuse it until they allow that model (Plan page), or they can switch to the suggested model.
- Don't remove the project's existing provider keys, and don't change prompts, tools or business logic.
- If a client has no configurable base URL, leave it unchanged and tell the developer.

## 5. Run one test

Send one short request through the updated client, or:

```bash
set -a; . ./.env; set +a
curl -s "$TOKPAD_BASE_URL/chat/completions" -H "Authorization: Bearer $TOKPAD_API_KEY" -H "Content-Type: application/json" \
  -d '{"model": "<suggested_model from step 2>", "messages": [{"role": "user", "content": "Reply with OK"}]}'
```

If it fails, show the error and don't silently undo anything. What the common errors mean:

- `401`: the key isn't recognised (check `.env` was loaded).
- `402 budget_exceeded`: there's no credit left, or the monthly budget is used up. The developer adds credit on the Billing page of their workspace.
- `403` saying the key is inactive: the workspace has no credit yet, or someone paused the key. The developer adds credit (or resumes the key) in their workspace.
- `403 model_blocked`, or `could not auto resolve a provider`: that model isn't allowed for this key. Use a model from the list `wait` printed, or ask the developer to allow it.

## 6. Report back

1. The files you changed, with a short diff summary.
2. The test result.
3. Any clients you couldn't route, and any models outside the key's list.
4. Where to watch spend and add credit: the `admin_url` printed in step 2.

## Notes

- Tokpad records request metadata (model, tokens, cost) to enforce budgets. Prompt and response content isn't stored.
- The developer can pause or delete this key in Tokpad at any time.

## tokpad_connect.py

```python
#!/usr/bin/env python3
"""Connect this project to Tokpad. Python 3.8+ standard library only.

  python3 tokpad_connect.py start "Claude Code"   # prints a link and a code for the developer to approve
  python3 tokpad_connect.py wait                  # waits up to ~100 s; exit 0 connected, 2 still waiting, 1 stopped

On success the key goes straight into ./.env (never printed) and .env is git-ignored.
"""
import json
import os
import platform
import ssl
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request

# app.tokpad.com first; the Render address keeps working while that domain is being connected.
APPS = [os.environ["TOKPAD_APP_URL"].rstrip("/")] if os.environ.get("TOKPAD_APP_URL") else \
    ["https://app.tokpad.com", "https://tokpad-app.onrender.com"]
STATE = os.path.join(tempfile.gettempdir(), "tokpad-signin-%s.json" % os.getuid() if hasattr(os, "getuid") else "tokpad-signin.json")
ENV_KEYS = ("TOKPAD_API_KEY", "TOKPAD_BASE_URL", "TOKPAD_ANTHROPIC_BASE_URL")


def post(app, path, body):
    """Returns (status, json), or (None, error text) when the address can't be reached."""
    req = urllib.request.Request(app + path, data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=60) as r:  # a sleeping server can take a while to answer
            return r.status, json.load(r)
    except urllib.error.HTTPError as e:
        try:
            return e.code, json.load(e)
        except ValueError:
            return e.code, {"error": "http_%d" % e.code}
    except (urllib.error.URLError, OSError) as e:
        if isinstance(getattr(e, "reason", None), ssl.SSLCertVerificationError):
            return post_with_curl(app + path, body)  # this Python has no CA certificates (common on macOS)
        return None, str(e)


def post_with_curl(url, body):
    """Same request through curl, which uses the operating system's trusted certificates. Verification stays on."""
    try:
        r = subprocess.run(["curl", "-sS", "-m", "60", "-X", "POST", "-H", "Content-Type: application/json",
                            "--data-binary", "@-", "-w", "\n%{http_code}", url],
                           input=json.dumps(body), capture_output=True, text=True)
    except OSError as e:
        return None, "Python can't verify HTTPS certificates and curl isn't available (%s)" % e
    if r.returncode != 0:
        return None, r.stderr.strip() or "curl failed"
    text, _, code = r.stdout.rpartition("\n")
    try:
        return int(code), json.loads(text or "{}")
    except ValueError:
        return int(code), {"error": "http_%s" % code}


def unreachable(app, err):
    sys.exit("Can't reach %s (%s). Tokpad self-serve sign-in may not be live yet: ask the developer to request "
             "access at https://tokpad.com/#start" % (app, err))


def start(agent):
    body = {"client_name": "%s on %s" % (agent, platform.node() or "this computer")}
    for app in APPS:
        status, d = post(app, "/api/v1/device/start", body)
        if status is not None:
            break
    if status is None:
        unreachable(app, d)
    if status != 200:
        sys.exit("Tokpad sign-in could not start: %s" % (d.get("detail") or d))
    fd = os.open(STATE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w") as f:
        json.dump({"app": app, "device_code": d["device_code"], "interval": d["interval"], "deadline": time.time() + d["expires_in"]}, f)
    print("Open %s and approve the code %s (valid %d minutes)." % (d["verification_uri_complete"], d["user_code"], d["expires_in"] // 60))


def save_env(t):
    path = os.path.abspath(".env")
    lines = []
    if os.path.exists(path):
        with open(path) as f:
            lines = [l for l in f.read().splitlines() if l.split("=", 1)[0].strip() not in ENV_KEYS]
    lines += ["TOKPAD_API_KEY=" + t["api_key"], "TOKPAD_BASE_URL=" + t["base_url"],
              "TOKPAD_ANTHROPIC_BASE_URL=" + t["anthropic_base_url"]]
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w") as f:
        f.write("\n".join(lines) + "\n")
    added = not git_ignores_env()
    if added:
        with open(".gitignore", "a") as f:
            f.write("\n.env\n")
    return path, added


def git_ignores_env():
    """True when .env can't be committed by accident (ignored, or not a git checkout at all)."""
    try:
        if subprocess.run(["git", "rev-parse", "--is-inside-work-tree"], capture_output=True).returncode != 0:
            return True
        return subprocess.run(["git", "check-ignore", "-q", ".env"], capture_output=True).returncode == 0
    except OSError:  # git not installed
        return True


def wait(budget_s=100):
    try:
        with open(STATE) as f:
            st = json.load(f)
    except (OSError, ValueError):
        sys.exit("No sign-in in progress. Run: python3 tokpad_connect.py start")
    interval, end = st["interval"], min(time.time() + budget_s, st["deadline"])
    while time.time() < end:
        time.sleep(interval)
        status, t = post(st["app"], "/api/v1/device/token", {"device_code": st["device_code"]})
        if status is None:
            continue  # a dropped connection: try again on the next poll
        if status == 200:
            os.remove(STATE)
            path, added = save_env(t)
            info = {k: t.get(k) for k in ("workspace", "budget", "models", "suggested_model", "credit_remaining", "admin_url")}
            info.update(connected=True, env_file=path, gitignore_updated=added, key_hint="..." + t["api_key"][-4:])
            print(json.dumps(info, indent=2))
            return 0
        err = t.get("error")
        if err == "slow_down":
            interval += 5
        elif err != "authorization_pending":
            os.remove(STATE)
            print({"access_denied": "The developer denied the request. Nothing was changed.",
                   "expired_token": "The code expired. Run start again for a new one."}.get(err, "Sign-in stopped: %s" % err))
            return 1
    if time.time() >= st["deadline"]:
        os.remove(STATE)
        print("The code expired. Run start again for a new one.")
        return 1
    print("Still waiting for the developer to approve. Run wait again.")
    return 2


if __name__ == "__main__":
    cmd = sys.argv[1] if len(sys.argv) > 1 else ""
    if cmd == "start":
        start(sys.argv[2] if len(sys.argv) > 2 else "Coding agent")
    elif cmd == "wait":
        sys.exit(wait())
    else:
        sys.exit(__doc__)
```
