# Claude Code Free-Tier Onboarding (Agent Register + OAuth Fallback)

This guide is optimized for Claude Code in Terminal. The preferred path is `POST /cli/auth/agent-register`, which lets Claude Code exchange its local identity fields for a free-tier API key with zero browser interaction. The older Google OAuth flow remains available as a fallback.

HTML version: `/docs/free-tier-oauth`

Jump to the bash script: [Complete Working Example](#complete-bash-script-end-to-end)

## Quickstart — One Command, Zero Browser

If you have Claude Code installed, paste this into your terminal:

```bash
curl -sL https://cat.fish.dog/scripts/agent-register.sh | bash
```

The script reads your Anthropic-account identity from `~/.claude.json`, POSTs it to `/cli/auth/agent-register`, and saves the resulting `rk_free_*` API key to `~/.ditto_free_tier.env`. Total time under five seconds; no browser, no Google sign-in, no copy-paste.

If `~/.claude.json` is missing (you're not running Claude Code), the script tells you to use the OAuth shell script instead.

For everything else — the full HTTP contract, validation rules, error codes, OAuth flow, polling, and the working bash example — keep reading.

## Fast Facts

- **Preferred command:** `curl -sL https://cat.fish.dog/scripts/agent-register.sh | bash`
- **Fallback command:** `curl -sL https://cat.fish.dog/scripts/free-tier-auth.sh | bash`
- **Required Claude Code file:** `~/.claude.json`
- **Saved key file:** `~/.ditto_free_tier.env`
- **Saved environment variable:** `FISHDOG_FREE_TIER_API_KEY`
- **Core workflow endpoints:** `POST /v1/free/questions` and `GET /v1/jobs/{job_id}`
- **Rate/quota snapshot endpoint:** `GET /v1/billing/usage`
- **Paid-org behavior:** depends on billing mode. **Stripe-billed paid orgs** receive `status: "complete"` with a managed paid `rk_live_*` key (broader scopes) — the agent gets a working paid key without re-OAuth. **Invoiced or other non-Stripe paid orgs** receive `status: "login_required"`; the user signs in and manages keys via the dashboard.
- **Free-tier upgrade path:** quota exhaustion now returns a signed `/upgrade?...` link, not a direct Checkout URL.
- **Post-upgrade refresh signal:** old free keys can return `401` with `error.code="key_rotated_post_upgrade"`.

## What Normally Takes Weeks Now Takes Minutes

What normally takes weeks now takes minutes.

FishDog is a synthetic market research platform. You ask questions. AI-powered personas,
calibrated to real population data, answer them. No recruiting. No scheduling. No waiting.

The free tier gives you and Claude Code direct API access to run quick customer research.

## What Can You Actually Do?

### Test pricing before you launch

> "At $29.99, only 6.3% would subscribe. At $9.99, 65.7% would."

A hedge fund needed to understand ESPN's DTC pricing strategy before making trading
decisions. Traditional research would take weeks. FishDog delivered pricing elasticity
data in 30 minutes -- data that directly informed their investment thesis.

[Read the full case study ->](https://fish.dog/case-studies/espn-dtc-hedge-fund)

### Find the insight that changes everything

> "Sentiment decline reflected trust and consistency concerns -- not price. This was a
> credibility problem, not a cost problem."

An M&A team had 30 days to evaluate Loblaw's No Frills brand. In 16 minutes, FishDog
revealed that customers were leaving because of broken trust, not price. That insight
fundamentally changed the diligence recommendation.

[Read the full case study ->](https://fish.dog/case-studies/pe-ib-diligence-loblaw)

### Validate a complete business concept

Claude Code and FishDog founded a startup together -- from problem identification through
validated pricing, positioning, and deal breakers -- in 4 hours.

The result: [CareQuarter](https://app.carequarter.pro), a care coordination service
for adult children managing aging parents. Three research phases. 32 synthetic personas.
Complete validation of:

- **Positioning winner:** "Stop being the unpaid case manager"
- **Pricing:** $175-325/month (every persona confirmed acceptable range)
- **Trigger moment:** Friday 4pm hospital discharge call
- **Deal breakers:** Rotating staff, no spending caps, data resale

## For Claude Code: What This Unlocks

| Research Type | What You Learn | Example Question |
| --- | --- | --- |
| **Pain Discovery** | The real problem customers face | "What frustrates you most about managing your parents' healthcare?" |
| **Positioning Tests** | Which message resonates | "Which description makes you most interested: [A], [B], or [C]?" |
| **Pricing Validation** | Willingness to pay | "Would you pay $X/month for this? What's the maximum you'd consider?" |
| **Competitive Intel** | Why customers switch | "Why did you choose [competitor] over alternatives?" |
| **Feature Prioritization** | What to build first | "If you could only have one of these features, which would you choose?" |
| **Deal Breaker Identification** | What kills the sale | "What would make you NOT buy this, even if it solved your problem?" |

### You can iterate

This is not a one-shot survey. You can ask follow-up questions to the same shared panel:

1. Ask "What's your biggest frustration with grocery delivery?"
2. Read responses, identify a theme (for example, "substitutions are always wrong").
3. Ask "You mentioned substitutions. Would you pay $5/month extra for guaranteed no-substitution delivery?"
4. Validate or invalidate in minutes.

### Pro tip: Ask open-ended questions

Open-ended questions surface richer insights than yes/no questions:

- **Weak:** "Do you like meal planning apps?"
- **Strong:** "Walk me through how you decided what to cook for dinner last Tuesday."

## Who This Is For

- Users who want the free-tier API key via Claude Code in Terminal.
- A terminal-first flow where agent-register is preferred and Google OAuth is the fallback.

## The Free Tier

- A free-tier API key (prefix `rk_free_`).
- ~12 personas from FishDog's shared research panel (US adults, demographically balanced).
- Unlimited questions (within rate limits).
- Full response text with persona name and reply in `result.results[].reply` (with `result.partial: true` if some replies are still pending).
- API access for Claude Code or any client.
- Access to the core free-tier workflow endpoints:
  - `POST /v1/free/questions`
  - `GET /v1/jobs/{job_id}` (for jobs owned by that same free-tier user account, including replacement keys after rotation)
- Access to the operational usage snapshot endpoint:
  - `GET /v1/billing/usage`
- This key **cannot** access paid endpoints or admin routes.

This is real research capability. For larger panels, custom demographics, or enterprise
features, [contact us](https://fish.dog/contact) or upgrade to a paid plan.

## High-Level Flow (Claude Overview)

### Preferred flow: zero-touch agent register

1. Claude reads the user's local Claude Code identity fields from `~/.claude.json`.
2. Claude POSTs those fields to `/cli/auth/agent-register`.
3. The server returns one of three responses:
   - `status: "complete"` with a new `rk_free_*` free-tier key — the standard case.
   - `status: "complete"` with a managed paid `rk_live_*` key (broader scopes) — when the user's Anthropic account is linked to a Stripe-billed paid Fish.Dog org. This is the agent-register paid-refresh path: the agent gets a working paid key in one round-trip, no re-OAuth required.
   - `status: "login_required"` — when the user is on an invoiced or otherwise non-Stripe paid org. The user signs in via `login_url` and manages keys through the dashboard.
4. Claude saves the key locally (free or paid) and proceeds with API calls; on `login_required`, Claude points the user at `login_url` without minting anything.

### Fallback flow: Google OAuth

1. Claude starts a CLI auth session (`/cli/auth/start`).
2. Claude opens the returned Google authorization URL in the user's browser.
3. The user signs in with Google and consents.
4. Google redirects to a local loopback callback (handled by the CLI).
5. Claude completes the flow (`/cli/auth/complete`) and receives the API key.
6. Claude instructs the user to save the key and shows a working example.

## What Happens When You Hit The Free-Tier Limit

The free-tier flow no longer jumps straight from a `429` to Stripe Checkout.

Current upgrade contract:

1. A quota-exhausted free-tier org-admin receives a signed `/upgrade?org=...&token=...` link.
2. `/upgrade` shows **both**:
   - Sign in with Google
   - Send sign-in link
3. After the browser session is established, the user lands on the Billing tab
   and completes Stripe Checkout there.
4. The checkout-complete webhook flips the org from `free` to `stripe` and
   auto-mints a user-facing paid `rk_live_*` key.
5. The Billing success page shows a short-lived backup reveal of that paid key.

Important:

- the paid key is a new key, not an in-place scope expansion of the old
  `rk_free_*` key
- the old free key may later return a structured `401` refresh response
- the primary recovery path is to re-run `agent-register.sh`

Example refresh error:

```json
{
  "error": {
    "code": "key_rotated_post_upgrade",
    "message": "Your account was upgraded and the previous API key is no longer active.",
    "details": {
      "reason": "A new paid-scope API key was issued for this account.",
      "refresh_command": "curl -sL https://cat.fish.dog/scripts/agent-register.sh | bash"
    }
  }
}
```

If you are using Claude Code skills that understand this contract, the client
can prompt for consent, run the refresh command, and retry once. If you are
using `curl` or a custom script, run the refresh command manually.

## Getting Your API Key (Preferred: Agent Register)

Claude Code can self-register directly with a single POST:

```bash
curl -s -X POST "$DITTO_API_BASE_URL/cli/auth/agent-register" \
  -H 'Content-Type: application/json' \
  -d '{
    "email":"user@example.com",
    "anthropic_account_uuid":"550e8400-e29b-41d4-a716-446655440000",
    "claude_install_id":"4111c046280c9fe601466edf051cfa0ea8208fb93840463149a218989b1a294d",
    "anthropic_org_uuid":"660e8400-e29b-41d4-a716-446655440001",
    "anthropic_account_created_at":"2025-12-05T22:50:50Z",
    "anthropic_org_type":"claude_max",
    "anthropic_billing_type":"stripe_subscription"
  }'
```

Expected success response:

```json
{
  "ok": true,
  "status": "complete",
  "api_key": "rk_free_...",
  "org_uuid": "...",
  "previous_key_revoked": false,
  "next_steps": {
    "summary": "Save the API key and use it with POST /v1/free/questions.",
    "docs_url": "https://cat.fish.dog/docs/free-tier-oauth"
  }
}
```

If Claude Code re-registers for the same Anthropic account, the prior
agent-created key is revoked and the response sets
`previous_key_revoked: true`.

Important:

- Re-register rotates the old agent-created key.
- Replacement keys owned by the same Fish.Dog user can still poll prior
  free-tier jobs and access the same saved free-tier work.
- The revoked key string itself stops working immediately, so the client should
  replace any cached key material after re-registering.

If the user belongs to a **Stripe-billed paid org** (`billing_mode="stripe"`), the server returns a managed paid `rk_live_*` key directly — the agent-register paid-refresh path:

```json
{
  "ok": true,
  "status": "complete",
  "api_key": "rk_live_...",
  "org_uuid": "<uuid>",
  "previous_key_revoked": false,
  "next_steps": {
    "summary": "Save the paid API key and use it against the full API surface.",
    "docs_url": "https://cat.fish.dog/docs/free-tier-oauth"
  }
}
```

The HTTP status is `201` if a fresh paid key was minted, or `200` if an existing managed paid key was reused (`previous_key_revoked` reflects which case). The returned key carries broader paid-tier scopes — your agent is ready to use the full API surface (research groups, studies, demographic filtering) without a separate sign-in.

If the user belongs to a **non-Stripe paid org** (invoiced, direct-pay, etc.), the server returns `login_required` so a human can manage keys via the dashboard:

```json
{
  "ok": true,
  "status": "login_required",
  "login_url": "https://cat.fish.dog/auth/login",
  "next_steps": {
    "summary": "Log in to your paid account to create or manage API keys.",
    "docs_url": "https://cat.fish.dog/docs/free-tier-oauth"
  }
}
```

## Fallback: Getting Your API Key (OAuth Flow)

Use this script to walk the user through the flow.

### Step 1: Start the auth session

Set the API base URL (default is production):

```bash
export DITTO_API_BASE_URL="https://cat.fish.dog"
```

For local development, use `http://127.0.0.1:8000`.

```bash
curl -s -X POST "$DITTO_API_BASE_URL/cli/auth/start" \
  -H 'Content-Type: application/json' \
  -d '{"provider":"google","redirect_uri":"http://127.0.0.1:53682/oauth/callback"}'
```

Copy-paste version (explicit production URL):

```bash
curl -s -X POST "https://cat.fish.dog/cli/auth/start" \
  -H "Content-Type: application/json" \
  -d '{"provider":"google","redirect_uri":"http://127.0.0.1:53682/oauth/callback"}'
```

Expected response (example):

```json
{
  "ok": true,
  "session_id": "<session_id>",
  "authorization_url": "https://accounts.google.com/...",
  "expires_in": 900
}
```

Extract `session_id` and `authorization_url` (example):

```bash
START_RESPONSE=$(curl -s -X POST "$DITTO_API_BASE_URL/cli/auth/start" \
  -H 'Content-Type: application/json' \
  -d '{"provider":"google","redirect_uri":"http://127.0.0.1:53682/oauth/callback"}')
SESSION_ID=$(echo "$START_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['session_id'])")
AUTH_URL=$(echo "$START_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['authorization_url'])")
```

### Optional: Login Button (web helper)

If you are viewing this guide in a browser and want a clear UI action, use this button.
It will call `/cli/auth/start` and open the Google consent screen.

Important: the CLI must be running and listening on the loopback callback URL.

```html
<button id="free-tier-login">Login with Google</button>
<script>
  (function () {
    var button = document.getElementById("free-tier-login");
    if (!button) return;
    button.addEventListener("click", function () {
      fetch("/cli/auth/start", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({
          provider: "google",
          redirect_uri: "http://127.0.0.1:53682/oauth/callback"
        })
      })
      .then(function (res) { return res.json(); })
      .then(function (data) {
        if (!data || !data.authorization_url) {
          alert("Failed to start OAuth session.");
          return;
        }
        window.open(data.authorization_url, "_blank", "noopener,noreferrer");
      })
      .catch(function () {
        alert("Failed to start OAuth session.");
      });
    });
  })();
</script>
```

### Step 2: Start the callback listener

Start a temporary listener before opening the auth URL:

```bash
python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse, json

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        with open('/tmp/oauth_callback.json', 'w') as f:
            json.dump({'code': params.get('code', [''])[0], 'state': params.get('state', [''])[0]}, f)
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'Done! Return to terminal.')

HTTPServer(('127.0.0.1', 53682), Handler).handle_request()
" &
```

### Step 3: Open the authorization URL

Tell the user to open the `authorization_url` in their browser and sign in with Google.

Optional helpers:

```bash
# macOS
open "<authorization_url>"

# Linux
xdg-open "<authorization_url>"
```

### Step 4: Callback behavior (what the user sees)

- The browser will redirect to:
  - `http://127.0.0.1:53682/oauth/callback?code=...&state=...`
- The CLI spins up a local HTTP listener on this loopback address and captures the `code` + `state`.
- The user may see a blank page or a simple success message.
- The port is fixed by the CLI configuration and must match the `redirect_uri`.
- After the redirect, the listener writes `/tmp/oauth_callback.json`.

### Step 4b: Callback capture (stdout-only variant)

If you want to capture the callback via stdout instead of a file, use this variant
before opening the auth URL:

```bash
python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        query = urllib.parse.urlparse(self.path).query
        params = urllib.parse.parse_qs(query)
        print('CODE=' + params.get('code', [''])[0])
        print('STATE=' + params.get('state', [''])[0])
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'Auth complete! Return to terminal.')

HTTPServer(('127.0.0.1', 53682), Handler).handle_request()
"
```

### Step 5: Wait for the callback (file-based)

```bash
# wait up to 60s for the callback file
for _ in {1..60}; do
  [ -s /tmp/oauth_callback.json ] && break
  sleep 1
done
CALLBACK=$(cat /tmp/oauth_callback.json)
CODE=$(echo "$CALLBACK" | python3 -c "import sys,json; print(json.load(sys.stdin).get('code',''))")
STATE=$(echo "$CALLBACK" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))")
```

### Step 6: Complete the flow (CLI handles this)

The CLI should POST the code and state:

```bash
curl -s -X POST "$DITTO_API_BASE_URL/cli/auth/complete" \
  -H 'Content-Type: application/json' \
  -d '{"session_id":"<session_id>","code":"<code>","state":"<state>"}'
```

Copy-paste version (explicit production URL):

```bash
curl -s -X POST "https://cat.fish.dog/cli/auth/complete" \
  -H "Content-Type: application/json" \
  -d '{"session_id":"<session_id>","code":"<code>","state":"<state>"}'
```

If the user is free-tier (new or existing free-tier org), the response includes the key:

```json
{
  "ok": true,
  "status": "complete",
  "api_key": "rk_free_...",
  "org_uuid": "...",
  "next_steps": {
    "summary": "Save the API key and use it with POST /v1/free/questions.",
    "docs_url": "https://cat.fish.dog/docs/free-tier-oauth"
  }
}
```

If the user belongs to a paid org, they will receive:

```json
{
  "ok": true,
  "status": "login_required",
  "login_url": "https://cat.fish.dog/auth/login",
  "next_steps": {
    "summary": "Log in to your paid account to create or manage API keys.",
    "docs_url": "https://cat.fish.dog/docs/free-tier-oauth"
  }
}
```

In that case, Claude should direct the user to log in and create a key via the paid UI.

## Saving The API Key

Claude should instruct the user to save the key to an environment variable:

```bash
export FISHDOG_FREE_TIER_API_KEY="rk_free_..."
```

If the user needs it in a `.env` file:

```
FISHDOG_FREE_TIER_API_KEY=rk_free_...
```

Remind the user: the key is shown once. Store it securely.

## Asking Research Questions

The free-tier key can:
- Submit questions via `POST /v1/free/questions`.
- Poll results via `GET /v1/jobs/{job_id}` for jobs owned by that same
  free-tier user account.

Example:

```bash
curl -s -X POST "$DITTO_API_BASE_URL/v1/free/questions" \
  -H "Authorization: Bearer $FISHDOG_FREE_TIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question":"What do people think about oat milk?"}'
```

Expected response (example):

```json
{
  "ok": true,
  "group_id": 123,
  "count": 12,
  "job_id": "...",
  "agent_ids": [1,2,3],
  "agent_uuids": ["..."],
  "question": "What do people think about oat milk?"
}
```

This response is a queue confirmation, not the final answers.

To fetch answers, poll the job status using the returned `job_id`. Once
`status` is `finished`, the answers are available in `result.results`. If the
aggregate times out, you'll see `result.partial: true` and
`result.pending_job_ids`.

```bash
curl -s -X GET "$DITTO_API_BASE_URL/v1/jobs/<job_id>" \
  -H "Authorization: Bearer $FISHDOG_FREE_TIER_API_KEY"
```

Free-tier keys can only access jobs owned by that same free-tier user account.
The MCP tool `v1.jobs.get` can also be used for the same purpose when calling
through MCP.

### What job polling responses look like

In progress:

```json
{"status":"queued","meta":{"free_tier":true}}
```

Finished (answers are in `result.results[].reply`):

```json
{
  "status":"finished",
  "result":{
    "partial": false,
    "count":12,
    "results":[
      {"id":1,"uuid":"...","name":"...","reply":"...","assistant_message_id":123}
    ]
  },
  "meta":{"free_tier":true}
}
```

Finished with partial results (after ~60s):

```json
{
  "status":"finished",
  "result":{
    "partial": true,
    "pending_job_ids": ["..."],
    "count":12,
    "results":[
      {"id":1,"uuid":"...","name":"...","reply":"..."},
      {"id":2,"uuid":"...","name":"...","status":"started"}
    ]
  },
  "meta":{"free_tier":true}
}
```

Each `result.results[]` entry includes `id`, `uuid`, `name`, `reply`, and
`assistant_message_id` when available. Errors return `error` instead of `reply`.

Extract replies (example):

```bash
echo "$JOB_RESPONSE" | python3 - <<'PY'
import json, sys
data = json.load(sys.stdin)
for item in data.get("result", {}).get("results", []):
    print(item.get("reply", ""))
PY
```

Failed:

```json
{"status":"failed","error":"...","meta":{"free_tier":true}}
```

## Example Questions to Get Started

### For pain discovery
- "Describe the last time you were frustrated trying to [X]. What happened?"
- "What's the most annoying part of [activity] that nobody talks about?"
- "If you could wave a magic wand and fix one thing about [category], what would it be?"

### For pricing
- "How much do you currently spend on [category] per month?"
- "At what price would [product] feel like a bargain? A stretch? Too expensive to consider?"
- "Would you pay $X for [specific benefit]? Why or why not?"

### For positioning
- "Which of these descriptions makes you most interested: [A], [B], or [C]? Why?"
- "In your own words, what does '[tagline]' mean to you?"
- "What would a company have to say to convince you to try their [product]?"

### For competitive intelligence
- "Why did you choose [competitor] over other options?"
- "What would make you switch from [current solution] to something new?"
- "What does [competitor] get right that others don't?"

### For feature prioritization
- "If you could only have one of these features, which would you choose: [A], [B], or [C]?"
- "What's missing from current [category] products that you wish existed?"
- "Rank these features from most to least important for your daily use."

## Claude Code Quick Start

1. Check for an existing key: `[ -f ~/.ditto_free_tier.env ] && source ~/.ditto_free_tier.env`.
2. If no key is present, run the preferred hosted script: `curl -sL https://cat.fish.dog/scripts/agent-register.sh | bash`.
3. If `~/.claude.json` is unavailable or the environment does not expose agent-register, use the OAuth fallback flow instead.
4. Ask a question via `POST /v1/free/questions`.
5. Poll `GET /v1/jobs/{job_id}` until `status` is `finished`.
6. Read replies from `result.results[].reply`, then iterate.

## Complete OAuth Bash Example (Fallback Only)

This script is for the browser-based fallback path only. The preferred path is the
hosted `agent-register.sh` script above. Use this example when Claude Code identity
is unavailable and you need the full loopback OAuth flow explicitly.

```bash
#!/usr/bin/env bash
set -euo pipefail

DITTO_API_BASE_URL="https://cat.fish.dog"
CALLBACK_PORT=53682
CALLBACK_URI="http://127.0.0.1:${CALLBACK_PORT}/oauth/callback"

START_RESPONSE=$(curl -s -X POST "${DITTO_API_BASE_URL}/cli/auth/start" \
  -H 'Content-Type: application/json' \
  -d "{\"provider\":\"google\",\"redirect_uri\":\"${CALLBACK_URI}\"}")

SESSION_ID=$(echo "$START_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('session_id',''))")
AUTH_URL=$(echo "$START_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('authorization_url',''))")

CALLBACK_FILE=$(mktemp)
export CALLBACK_FILE
python3 << 'PY' &
import http.server, socketserver, urllib.parse, os
class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        with open(os.environ['CALLBACK_FILE'], 'w') as f:
            f.write("CODE=%s\n" % params.get('code', [''])[0])
            f.write("STATE=%s\n" % params.get('state', [''])[0])
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'Auth complete! Return to terminal.')
    def log_message(self, *args): pass
with socketserver.TCPServer(("127.0.0.1", 53682), Handler) as httpd:
    httpd.handle_request()
PY

if [[ "$OSTYPE" == "darwin"* ]]; then
  open "$AUTH_URL"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
  xdg-open "$AUTH_URL" 2>/dev/null || true
fi

for _ in {1..60}; do
  [ -s "$CALLBACK_FILE" ] && break
  sleep 1
done
source "$CALLBACK_FILE"
rm -f "$CALLBACK_FILE"

COMPLETE_RESPONSE=$(curl -s -X POST "${DITTO_API_BASE_URL}/cli/auth/complete" \
  -H 'Content-Type: application/json' \
  -d "{\"session_id\":\"${SESSION_ID}\",\"code\":\"${CODE}\",\"state\":\"${STATE}\"}")

API_KEY=$(echo "$COMPLETE_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('api_key',''))")
export FISHDOG_FREE_TIER_API_KEY="$API_KEY"

QUESTION_RESPONSE=$(curl -s -X POST "${DITTO_API_BASE_URL}/v1/free/questions" \
  -H "Authorization: Bearer ${FISHDOG_FREE_TIER_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"question":"What factors matter most when choosing a coffee brand?"}')

JOB_ID=$(echo "$QUESTION_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")

while true; do
  JOB_RESPONSE=$(curl -s -X GET "${DITTO_API_BASE_URL}/v1/jobs/${JOB_ID}" \
    -H "Authorization: Bearer ${FISHDOG_FREE_TIER_API_KEY}")
  STATUS=$(echo "$JOB_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))")
  if [ "$STATUS" = "finished" ]; then
    break
  fi
  sleep 2
done

echo "$JOB_RESPONSE" | python3 -m json.tool
```

## Limitations & Upgrades

- Free-tier keys can ask via `POST /v1/free/questions` and poll
  `GET /v1/jobs/{job_id}` for jobs owned by that same free-tier user account.
- Free-tier keys cannot access studies, groups, billing/admin, or other paid endpoints.
- For the full API reference and paid workflows, see `https://cat.fish.dog/docs/api`.

## Limits & Quotas

- Free-tier traffic is rate limited; requests over the limit return `429`.
- `GET /v1/billing/usage` is available to the same free-tier key and returns
  the current `rate` and `quota` snapshot. No separate billing scope is
  required.
- Respect the `Retry-After` header on both question and polling `429`
  responses.
- The shared free-tier group currently targets 12 agents; the `count` field reflects
  the actual number targeted and may change.
- Question length limits are enforced server-side; keep prompts concise and expect
  validation errors for overly large payloads.

## Common Errors

- **405 Method Not Allowed**
  - You tried `GET /cli/auth/start`. Use POST with JSON.
- **Bad HTTP/0.9 request**
  - If you are running locally, use `http://127.0.0.1:8000`.
  - For production, use `https://cat.fish.dog`.
- **login_required**
  - The user belongs to a non-Stripe paid org (invoiced or direct-pay). Send them to the login URL; do not mint a key. (Stripe-billed paid orgs do NOT return `login_required` — they receive `status: "complete"` with an `rk_live_*` key directly.)
- **Job not accessible for this key**
  - The job belongs to a different user/account context or is not a free-tier
    job.

## What's Next

- Upgrade for larger panels, custom demographics, and deeper study workflows.
- Contact FishDog for enterprise features or private panels: https://fish.dog/contact

## Claude Guidance (Short Form)

- Always guide the user step-by-step.
- Always show exact commands to run.
- Always confirm the key was saved.
- Always remind them the free-tier key can ask via `/v1/free/questions`, poll its own jobs via `/v1/jobs/{job_id}`, and inspect live limits via `/v1/billing/usage`.
