diff --git a/.mcp.json.example b/.mcp.json.example index b426aea..f4da8d6 100644 --- a/.mcp.json.example +++ b/.mcp.json.example @@ -1,11 +1,11 @@ { - "_comment": "Copy to .mcp.json (gitignored). The qupick server is the backend's in-process MCP interface, served at /mcp by `uvicorn backend.api.app:app`. Set QUPICK_API_KEY to the agent's API key (emailed at registration; in local dev it is printed to the backend console as `[email:console] API key ...`). Claude Code expands ${QUPICK_API_KEY} at load time; an unset var leaves public tools working and 401s the per-agent tools.", + "_comment": "Copy to .mcp.json (gitignored). The qupick server is the backend's in-process MCP interface, served at /mcp by `uvicorn backend.api.app:app`. Paste the agent's API key directly into the Authorization header below, replacing REPLACE_WITH_YOUR_KEY (emailed at registration; in local dev it is printed to the backend console as `[email:console] API key ...`). Claude Code does NOT expand ${VAR} in .mcp.json headers, so the key must be a literal string here — this file is gitignored, so the real key is safe. A placeholder/invalid key leaves public tools working and 401s the per-agent tools.", "mcpServers": { "qupick": { "type": "http", "url": "http://127.0.0.1:8000/mcp", "headers": { - "Authorization": "Bearer ${QUPICK_API_KEY}" + "Authorization": "Bearer REPLACE_WITH_YOUR_KEY" } } } diff --git a/AGENTS.md b/AGENTS.md index 4d99ecb..a1f9f56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Vendored at [`skills/qupick/SKILL.md`](skills/qupick/SKILL.md). Delegates purcha **Triggers:** "pay with my worst performer", "use my worst crypto to buy X". -**Requires:** the **qupick MCP server** — the portfolio backend served at `http://127.0.0.1:8000/mcp`, exposing `mcp__qupick__*` tools. The backend must be up when the session starts for the tools to register; if they are missing the skill offers to start it backgrounded with `MARKET_DATA_SOURCE = config.backend.marketDataSource` (default `synthetic`) and then has the user reconnect MCP (`/mcp`). Also a local `skills/qupick/config.json` (copy of the committed `config.example.json`; gitignored because it holds the real email), and `QUPICK_API_KEY` set to the agent's key (emailed at registration; `.mcp.json` passes it as the Bearer header). Without the config the skill falls back to fully-interactive, on-chain-only behaviour. +**Requires:** the **qupick MCP server** — the portfolio backend served at `http://127.0.0.1:8000/mcp`, exposing `mcp__qupick__*` tools. The backend must be up when the session starts for the tools to register; if they are missing the skill offers to start it backgrounded with `MARKET_DATA_SOURCE = config.backend.marketDataSource` (default `synthetic`) and then has the user reconnect MCP (`/mcp`). Also a local `skills/qupick/config.json` (copy of the committed `config.example.json`; gitignored because it holds the real email), and the agent's API key pasted **directly** into the `.mcp.json` `Authorization: Bearer` header (emailed at registration; Claude Code does not expand `${VAR}` in `.mcp.json` headers, so a literal key is required). Without the config the skill falls back to fully-interactive, on-chain-only behaviour. **Selection vs settlement.** The skill always computes the worst performer — `min(μ)` over held crypto that Bitrefill accepts (`mcp__qupick__get_market`, static `PAYMENT_METHOD_MAP`). Selection is never bypassed by funding. It then resolves `config.funding.priority` against live balances (`GET /accounts/balance`) and on-chain holdings, settling against the first source that covers `price × (1 + fee_buffer_pct/100)`: @@ -68,7 +68,7 @@ On shortfall (`funding.on_shortfall`): `reject` stops; `confirm` warns and waits **Single human stop.** The flow is built to pause in exactly one place — the purchase approval. `mcp__bitrefill__buy-products` is deliberately kept off the `.claude/settings.local.json` allowlist. The six `mcp__qupick__*` tools are allowlisted (none spend real money), and the only `curl` is the read-only `/v2/accounts/balance` endpoint (write the URL first so prefix matching works). A purchase via `curl POST /v2/invoices` is **not** allowlisted and still prompts. -**Agent (re)use:** seeds over the Bitrefill-payable currencies (BTC, ETH, BNB, SOL, XRP, USDT, USDC, DOGE, ZEC, ALGO, FIL) via `mcp__qupick__register_agent` + `mcp__qupick__optimize`, or re-uses the existing agent — `get_agent` succeeding (with the configured `QUPICK_API_KEY`) means skip creation. +**Agent (re)use:** seeds over the Bitrefill-payable currencies (BTC, ETH, BNB, SOL, XRP, USDT, USDC, DOGE, ZEC, ALGO, FIL) via `mcp__qupick__register_agent` + `mcp__qupick__optimize`, or re-uses the existing agent — `get_agent` succeeding (with the key configured in the `.mcp.json` Bearer header) means skip creation. #### `mcp__qupick__get_market` diff --git a/README.md b/README.md index 1d395c1..3b67a08 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,63 @@ portfolio — the asset with the lowest expected return μ — then retune the p The agent logic lives in [`SKILL.md`](skills/qupick/SKILL.md); this README is the operator's quick-start. Purchase mechanics are delegated to the sibling [`bitrefill`](skills/bitrefill/SKILL.md) skill. +## Register & set up (hosted) + +The fastest path uses the **live hosted server** at `https://qupick.quip.network` — you don't run any +backend yourself. Five steps from clone to first run: + +1. **Install the skill.** Install `qupick` and its sibling `bitrefill` together (see + [Install the skill](#install-the-skill)). + +2. **Configure your defaults.** Copy the example and fill it in (see [Configure](#configure)): + + ```bash + cp skills/qupick/config.example.json skills/qupick/config.json + ``` + + Set at least `defaults.name` and `defaults.email` — registration emails your API key to that + address, and both `name` and `email` must be unique on the server. + +3. **Point Claude Code at the hosted MCP server.** Copy the example and set the `url` to the hosted + endpoint. The API key goes **directly in the `Authorization` header** — Claude Code does **not** + expand `${VAR}` in `.mcp.json` headers, so an environment variable won't reach the server. You have + no key yet, so leave a placeholder for now (`.mcp.json` is gitignored, so the real key is safe here + later): + + ```bash + cp .mcp.json.example .mcp.json + ``` + + ```json + { "mcpServers": { "qupick": { "type": "http", + "url": "https://qupick.quip.network/mcp", + "headers": { "Authorization": "Bearer REPLACE_WITH_YOUR_KEY" } } } } + ``` + +4. **Register to get your API key.** With the placeholder still in place, start Claude Code and ask + the agent to proceed. `register_agent` is a **public** tool — it works without a valid key — and it + is called with your `config.defaults` (name, email, sliders); the server **emails your API key** to + that address (it is never shown in the response). A repeat email or name returns `409` with detail + `email already registered` / `name already taken` (or `email or name already taken`). + +5. **Paste the key and reconnect.** Put the emailed key into `.mcp.json`, replacing the placeholder in + the `Authorization` header: + + ```json + "headers": { "Authorization": "Bearer " } + ``` + + Then reload the server with `/mcp` inside Claude Code. The per-agent `mcp__qupick__*` tools now + authenticate and the agent runs its first `optimize`. + +That's it — continue to [Use it](#use-it). To run the backend yourself instead of using the hosted +server, see [Development](#development). + ## Prerequisites -- The **qupick MCP server** — the portfolio backend served at `http://127.0.0.1:8000/mcp` (see below), - registered via `.mcp.json` and exposing `mcp__qupick__*` tools. +- The **qupick MCP server** — the portfolio backend, registered via `.mcp.json` and exposing + `mcp__qupick__*` tools. Use the hosted server at `https://qupick.quip.network/mcp` (above) or run + it locally at `http://127.0.0.1:8000/mcp` (see [Development](#development)). - The **Bitrefill MCP** connected (`https://api.bitrefill.com/mcp`, OAuth or API key), or the Bitrefill REST API key — used for product search, balance reads, and invoice creation. - A funding source the waterfall can draw on: a pre-funded **Bitrefill account balance** (USD, EUR, @@ -43,8 +96,8 @@ cp skills/qupick/config.example.json skills/qupick/config.json ``` `config.json` is gitignored (it holds your real email). Identity is **not** in the config — the -agent's API key lives in the `QUPICK_API_KEY` environment variable (see [Connect the MCP -server](#connect-the-mcp-server)). Fields: +agent's API key lives in the `.mcp.json` `Authorization` header (see +[Register & set up](#register--set-up-hosted)). Fields: | Field | Purpose | |-------|---------| @@ -70,60 +123,6 @@ to spend fiat first, or drop `account_fiat` to only ever sell crypto. This config plus the permission allowlist in `backend/.claude/settings.json` make a run stop in **exactly one** place — the purchase approval. See [Permissions & approvals](#permissions--approvals). -## Run the qupick server - -The backend serves both its REST API and the **qupick MCP server** (mounted at `/mcp`) from one -process. Start it before the Claude session so the `mcp__qupick__*` tools register. - -**Docker (Postgres + backend together):** - -```bash -docker compose up -d --build -``` - -This brings up Postgres and the backend (image built from `backend/Dockerfile`), publishing the -server on `http://127.0.0.1:8000`. The container defaults to `MARKET_DATA_SOURCE=synthetic` and -`GUROBI_IN_RACE=0` (SA is the CPU solver — no Gurobi licence or D-Wave token needed). With no -`SMTP_PASSWORD` set, the registration key is logged to the backend container -(`docker compose logs backend` → `[email:console] API key …`). - -**Local (uv), Postgres from compose:** - -```bash -docker compose up -d db -cd backend -MARKET_DATA_SOURCE=synthetic uv run uvicorn backend.api.app:app --workers 1 --port 8000 -``` - -Either way, wait until `GET http://127.0.0.1:8000/healthz` returns `{"ok": true}`. If the server is -down at session start the skill offers to start it backgrounded (allowlisted `synthetic` command) — -but the MCP tools only appear after you **reconnect the server** (run `/mcp` in Claude Code). - -> First-solve cold start: the very first `optimize` call can return -> `503 no feasible solution ... before deadline` while the D-Wave/Gurobi libs warm up. Just retry -> once — subsequent solves are sub-10ms. - -## Connect the MCP server - -Register the server with Claude Code via `.mcp.json` (gitignored; copy the committed example): - -```bash -cp .mcp.json.example .mcp.json -``` - -It points Claude Code at `http://127.0.0.1:8000/mcp` and passes your API key as the Bearer header -from `QUPICK_API_KEY`: - -```json -{ "mcpServers": { "qupick": { "type": "http", "url": "http://127.0.0.1:8000/mcp", - "headers": { "Authorization": "Bearer ${QUPICK_API_KEY}" } } } } -``` - -First run, you have no key yet: leave `QUPICK_API_KEY` unset, ask the agent to proceed, and it calls -`register_agent` (a public tool). The key is **emailed**; in local dev (no `SMTP_PASSWORD` on the -backend) it is printed to the backend console as `[email:console] API key for … : `. Set -`QUPICK_API_KEY` to that value and reconnect (`/mcp`); the per-agent tools then authenticate. - ## Permissions & approvals The skill is designed to interrupt you in **exactly one place** — the purchase approval in step 6. @@ -196,7 +195,7 @@ The skill then runs the flow from `SKILL.md`: methods live. 2. **Check + seed agent (MCP)** — `ping_backend`; if the qupick tools are missing, offer to start the server. Then `get_agent` (success → reuse the basket), or `register_agent` from `config.defaults` - → set `QUPICK_API_KEY` from the emailed/console key, reconnect MCP, then `optimize` for the first solve. + → put the emailed/console key in `.mcp.json`'s `Authorization` header, reconnect MCP, then `optimize` for the first solve. 3. **Pick product (MCP)** — `search-products` → `product-details` for price + accepted `payment_methods`; `denomination.policy` auto-selects the package. 4. **Market (MCP)** — `get_market` for per-asset μ, units, USD value. @@ -232,4 +231,49 @@ Retune: drop BTC, re-optimize over the remaining 10 currencies - The agent **never buys without explicit approval** — it always pauses at step 6. - Codes deliver instantly and are **non-refundable**; treat redemption codes as cash and redeem ASAP. - Use a dedicated, low-balance wallet. Full policy: [`safeguards.md`](skills/bitrefill/references/safeguards.md). -- The step-7 retune is irreversible — the spent asset leaves the basket until you re-add it. \ No newline at end of file +- The step-7 retune is irreversible — the spent asset leaves the basket until you re-add it. + +## Development + +To work on the backend or run fully offline, run the server yourself instead of using the hosted one. +The backend serves both its REST API and the **qupick MCP server** (mounted at `/mcp`) from one +process; start it before the Claude session so the `mcp__qupick__*` tools register. + +**Docker (Postgres + backend together):** + +```bash +docker compose up -d --build +``` + +This brings up Postgres and the backend (image built from `backend/Dockerfile`), publishing the +server on `http://127.0.0.1:8000`. The container defaults to `MARKET_DATA_SOURCE=synthetic` and +`GUROBI_IN_RACE=0` (SA is the CPU solver — no Gurobi licence or D-Wave token needed). + +**Local (uv), Postgres from compose:** + +```bash +docker compose up -d db +cd backend +MARKET_DATA_SOURCE=synthetic uv run uvicorn backend.api.app:app --workers 1 --port 8000 +``` + +Either way, wait until `GET http://127.0.0.1:8000/healthz` returns `{"ok": true}`. If the server is +down at session start the skill offers to start it backgrounded (allowlisted `synthetic` command) — +but the MCP tools only appear after you **reconnect the server** (run `/mcp` in Claude Code). + +> First-solve cold start: the very first `optimize` call can return +> `503 no feasible solution ... before deadline` while the D-Wave/Gurobi libs warm up. Just retry +> once — subsequent solves are sub-10ms. + +**Point `.mcp.json` at the local server** — same as the [hosted setup](#register--set-up-hosted), +but with the local URL (the key still goes directly in the header, not via an environment variable): + +```json +{ "mcpServers": { "qupick": { "type": "http", "url": "http://127.0.0.1:8000/mcp", + "headers": { "Authorization": "Bearer " } } } } +``` + +Registration works the same way, except a local backend with no `SMTP_PASSWORD` set does **not** send +email — instead the key is printed to the backend console as +`[email:console] API key for … : ` (`docker compose logs backend`). Paste that value into +`.mcp.json`'s `Authorization` header and reconnect (`/mcp`). \ No newline at end of file diff --git a/docs/deployment-prep.md b/docs/deployment-prep.md index 39a1388..53fa9ab 100644 --- a/docs/deployment-prep.md +++ b/docs/deployment-prep.md @@ -16,13 +16,13 @@ Locked choices that the rest of this doc assumes: |------|--------|-------------| | **Postgres** | Company **Supabase** | `DATABASE_URL` → Supabase **session pooler, port 5432** (long-running service). Prefer a **dedicated project or schema** — startup `create_all` should not share company tables. | | **Email** | **Resend** (org-owned account) | `SmtpEmailSender` over `smtp.resend.com` (STARTTLS); sender `noreply@quip.network` (domain verified in Resend). Username is the literal `resend`; password is the Resend API key. Plain SMTP, so switching relays later is env-only. **DigitalOcean blocks outbound 25/465/587 → set `SMTP_PORT=2587`** (see §4). Use an **org-owned** account, not a personal one — registration mail carries the API key. | -| **Hosting** | **DigitalOcean droplet + Caddy** | Single instance is fine but **no longer forced** — the background mark-to-market loop was removed, so the server holds no in-process state. Caddy does auto-HTTPS and SSE pass-through for `/mcp`. `create_all` on a shared DB is the only multi-instance concern (see §7). | +| **Hosting** | **DigitalOcean droplet + Caddy** | Single instance is fine but **no longer forced** — the background mark-to-market loop was removed, so the server holds no in-process *application* state. (The mounted MCP server still keeps per-worker session state, which is why `WEB_CONCURRENCY` is pinned to `1` — see the Concurrency row.) Caddy does auto-HTTPS and SSE pass-through for `/mcp`. `create_all` on a shared DB is the only multi-instance concern (see §7). | | **Audience** | **Internal-only** | Per-agent bearer auth is sufficient; rate-limiting public registration is a nice-to-have, not a launch blocker. | | **Schema** | Keep startup `create_all` | Fastest to launch; no migration tooling yet (see §7). Re-evaluate before the first schema change. | | **Subdomain** | e.g. `qupick.quip.network` | Pointed at the droplet; image pushed to `registry.gitlab.com/quip.network/qupick` (amd64). | | **Solvers** | **SA-only for v1 (PoC/MVP)** | `GUROBI_IN_RACE=0` (no Gurobi license in prod) and `DWAVE_API_TOKEN` unset (no QPU cost). Simulated annealing is the entire race field and always runs. Reversible later via env alone — re-add Gurobi or the QPU without a code change or image rebuild. | | **Market data** | **Live assets-api** | `MARKET_DATA_SOURCE=assets-api` pointed at `https://asset-tracker.quip.network`, so the μ/Σ inputs reflect real market conditions. Adds a runtime dependency on that service being reachable; `synthetic` stays the offline fallback if it is down or unbuilt. | -| **Concurrency** | **Scale workers to vCPU** | `WEB_CONCURRENCY` = the droplet's vCPU count. The server is stateless in-process (no background loops), so this is safe; keep `workers × ~15` DB connections under the Supabase pooler cap. | +| **Concurrency** | **Pin `WEB_CONCURRENCY=1`** | The mounted MCP server (`FastApiMCP.mount_http`) keeps its Streamable-HTTP session table **in each worker's memory** (fastapi-mcp 0.4.0 hardcodes `stateless=False`). With >1 uvicorn worker and no shared session store or sticky routing, a session minted on worker A 404s when a follow-up (`tools/list`, tool calls) round-robins to worker B — intermittent MCP failures. The CPU-bound solve already offloads via `asyncio.to_thread`, so one worker handles concurrent requests fine. To scale workers you must first make MCP stateless or add a shared session backend. | --- @@ -33,7 +33,7 @@ Locked choices that the rest of this doc assumes: | **Backend** | FastAPI app (`backend.api.app:app`) serving REST + the `/mcp` transport on port `8000` | Containerized (`backend/Dockerfile`); this is the qupick MCP server | | **PostgreSQL** | Holds agents (the API keys) and jobs | The `db` service in `docker-compose.yml` is dev-grade only | | **assets-api** | External market-data price service (REST, SQLite-backed) | Separate service; only needed when `MARKET_DATA_SOURCE=assets-api` | -| **qupick skill + client** | Claude Code side that calls the MCP server and Bitrefill | Needs `QUPICK_API_KEY` and `BITREFILL_API_KEY` | +| **qupick skill + client** | Claude Code side that calls the MCP server and Bitrefill | Needs the agent's qupick API key (in the `.mcp.json` Bearer header) and `BITREFILL_API_KEY` | The `docker-compose.yml` in the repo is a **dev stack** (weak Postgres password, `MARKET_DATA_SOURCE: synthetic`, console email). Treat it as a reference, not a @@ -68,7 +68,7 @@ backend. | Service | Why it's needed | What to obtain | Cost / gotchas | |---------|-----------------|----------------|----------------| | **Bitrefill** (https://www.bitrefill.com) | The qupick purchase flow buys gift cards / settles invoices via the bitrefill skill | An account API key (`BITREFILL_API_KEY`) and a **funded, low-balance** account/wallet | Real money. Use a dedicated low-balance account. Local-only — never commit, never set server-side. | -| **A registered qupick agent** | Per-agent MCP tools authenticate with its key | `QUPICK_API_KEY` (emailed at registration by the backend) | Stored client-side in `.mcp.json` / env, not on the server. | +| **A registered qupick agent** | Per-agent MCP tools authenticate with its key | The agent's API key (emailed at registration by the backend) | Pasted literally into the `.mcp.json` Bearer header client-side (gitignored), not stored on the server. | --- @@ -89,7 +89,7 @@ secret store, **never** in git or the image. | `ASSETS_API_BASE_URL` | `http://127.0.0.1:8080` | Set to `https://asset-tracker.quip.network` (the live assets-api) | | `GUROBI_IN_RACE` | `1` | Set `0` in production (no Gurobi license there) | | `PORT` | `8000` | Internal listen port behind Caddy (e.g. `8080`) | -| `WEB_CONCURRENCY` | `1` | uvicorn worker count; safe to raise (stateless), keep `workers × ~15` DB conns under the pooler cap | +| `WEB_CONCURRENCY` | `1` | uvicorn worker count; **keep at `1`** — MCP sessions are per-worker in-memory, so >1 worker 404s MCP intermittently (see Concurrency row). Raising it needs stateless MCP or a shared session store first | **Optional / tuning:** @@ -107,7 +107,7 @@ secret store, **never** in git or the image. | Env var | Used by | Notes | |---------|---------|-------| -| `QUPICK_API_KEY` | `.mcp.json` Bearer header → per-agent MCP tools | The agent's key, emailed at registration. Unset → public tools work, per-agent tools 401 | +| `QUPICK_API_KEY` | `backend/scripts/mcp_smoke.py` only (authed-call check) | The agent's key, emailed at registration. **`.mcp.json` does not read this var** — paste the key literally into its Bearer header (Claude Code doesn't expand `${VAR}` in headers). A placeholder/invalid key → public tools work, per-agent tools 401 | | `BITREFILL_API_KEY` | the bitrefill skill (`Authorization: Bearer`) | Funds the purchase flow | --- diff --git a/skills/qupick/SKILL.md b/skills/qupick/SKILL.md index 1ee8606..c0ce436 100644 --- a/skills/qupick/SKILL.md +++ b/skills/qupick/SKILL.md @@ -32,8 +32,9 @@ or a deployed HTTPS endpoint such as `https://qupick.quip.network/mcp`. Drive it | `mcp__qupick__get_leaderboard` | scoreboard (public) | **Auth.** Every per-agent tool (`get_agent`, `optimize`, `get_market`) carries the agent's API -key as `Authorization: Bearer `, configured once in `.mcp.json` -(`"Authorization": "Bearer ${QUPICK_API_KEY}"`). Set `QUPICK_API_KEY` to the key from +key as `Authorization: Bearer `, configured once in `.mcp.json` by pasting the key **literally** +into the header (`"Authorization": "Bearer "`). Claude Code does **not** expand `${VAR}` in +`.mcp.json` headers, so an environment variable won't reach the server — use the literal key from registration (see step 2). The public tools work without it. The only `curl` left in this flow is the read-only Bitrefill balance endpoint (step 5b): @@ -50,8 +51,8 @@ step 6 always fires. MCP server: `qupick` (the backend's `/mcp` transport — local `http://127.0.0.1:8000/mcp` or the deployed URL configured in `.mcp.json`). Tools return JSON; a failed -per-agent call surfaces the backend's `{"detail": "..."}` (e.g. `401` when `QUPICK_API_KEY` is -unset/wrong, `422` on validation, `503` on no feasible solution). +per-agent call surfaces the backend's `{"detail": "..."}` (e.g. `401` when the `.mcp.json` Bearer key +is unset/wrong, `422` on validation, `503` on no feasible solution). ### `register_agent` — create agent (public) @@ -93,7 +94,7 @@ In local dev (no `SMTP_PASSWORD` on the backend) the key is **printed to the bac **Returns** (`AgentConfig`): name, handle, email, `sliders`, and the current `assets` basket. Use it to retrieve the basket when re-using an agent — and as the **"am I registered?" probe**: a -success means the configured key is valid; a 401 means register (or set `QUPICK_API_KEY`) first. +success means the configured key is valid; a 401 means register (or paste the key into `.mcp.json`) first. ### `optimize` — optimise (and optionally retune) (authed) @@ -199,9 +200,9 @@ Load `skills/qupick/config.json` (mirror of the committed `config.example.json`) } ``` -- Identity is **not** in `config.json`. The agent's API key lives in `QUPICK_API_KEY` (read by - `.mcp.json` as the `Authorization: Bearer` header). "Already registered?" is answered by - `get_agent` succeeding, not by a stored id. +- Identity is **not** in `config.json`. The agent's API key lives **directly** in the `.mcp.json` + `Authorization: Bearer` header (Claude Code does not expand `${VAR}` there). "Already registered?" + is answered by `get_agent` succeeding, not by a stored id. - `defaults` — name / email / country / sliders, used only when creating a new agent and for product country. - `funding.priority` — settlement order (see step 5). `funding.fee_buffer_pct` — coverage buffer (default 2). `funding.on_shortfall` — `reject` | `confirm`. - `denomination.policy` — `smallest_gte` auto-picks the smallest package ≥ the requested amount. @@ -249,11 +250,12 @@ The deployed server is remote and always-on — there is nothing to start, so a situation there is a **wiring** problem, not a down-server problem. Deployed wiring: ```jsonc -// .mcp.json — point qupick at the deployed server; the key travels as the Bearer header +// .mcp.json — point qupick at the deployed server; paste the key literally into the Bearer header +// (Claude Code does NOT expand ${VAR} in .mcp.json headers; this file is gitignored, so it is safe) { "mcpServers": { "qupick": { "type": "http", "url": "https://qupick.quip.network/mcp", - "headers": { "Authorization": "Bearer ${QUPICK_API_KEY}" } } } } + "headers": { "Authorization": "Bearer " } } } } ``` **Server up?** The qupick MCP server rides on the backend over HTTP, so it must be reachable @@ -261,8 +263,8 @@ situation there is a **wiring** problem, not a down-server problem. Deployed wir - **Tools present:** call `mcp__qupick__ping_backend` → `{"ok": true}` and proceed. - **Tools missing — deployed target:** the `.mcp.json` `qupick` entry is absent or its URL is wrong. - Wire it to the deployed `/mcp` URL with the `Authorization: Bearer ${QUPICK_API_KEY}` header - (above), then **reconnect the MCP server** (run `/mcp` in Claude Code) — the tools won't appear + Wire it to the deployed `/mcp` URL with the `Authorization: Bearer ` header (above, key pasted + literally), then **reconnect the MCP server** (run `/mcp` in Claude Code) — the tools won't appear mid-session otherwise. No docker compose involved; the box is managed separately. - **Tools missing — local target:** the local stack was down at session start. **Offer to start it**, and on the user's yes bring up the full stack (Postgres + backend) with docker compose from the @@ -282,10 +284,10 @@ situation there is a **wiring** problem, not a down-server problem. Deployed wir **Already registered?** Call `mcp__qupick__get_agent`: -- **Succeeds** → the configured `QUPICK_API_KEY` is valid. Read the returned `assets` basket; skip - creation. -- **401** → no valid key. Either `QUPICK_API_KEY` is unset/stale, or there is no agent yet — - create one (below). +- **Succeeds** → the key in the `.mcp.json` Bearer header is valid. Read the returned `assets` + basket; skip creation. +- **401** → no valid key. Either the `.mcp.json` Bearer key is a placeholder/stale, or there is no + agent yet — create one (below). **Create.** Seed over the available Bitrefill currencies, using `config.defaults`: @@ -301,8 +303,8 @@ mcp__qupick__register_agent({ The API key is **emailed, not returned**. Retrieve it — from the email, or in local dev (no `SMTP_PASSWORD`) from the backend container logs (`docker compose logs backend`), line -`[email:console] API key for <>: ` — then **set `QUPICK_API_KEY` to it and -reconnect the MCP server (`/mcp`)** so the per-agent tools authenticate. +`[email:console] API key for <>: ` — then **paste it into the `.mcp.json` +`Authorization` header and reconnect the MCP server (`/mcp`)** so the per-agent tools authenticate. Once `get_agent` succeeds, optimise immediately for the first solve (no arguments): ``` @@ -347,7 +349,7 @@ The **worst performer** is `min(μ)` across these candidates. If there are no sp | Token | Source | Pays via | Sells it? | Retune? | |-------|--------|----------|--------------|---------| | `account_match` | Bitrefill account balance held in the worst-performing asset — **only possible when the worst performer is BTC** (account sub-accounts are limited to `XBT`/`USD`/`EUR`) | `buy-products(payment_method:"balance", balance_currency:"XBT")` | yes | yes | -| `onchain_match` | Wallet holdings of the worst-performing asset | `buy-products(payment_method:, return_payment_link:true)` → pay link → poll | yes | yes | +| `onchain_match` | Wallet holdings of the worst-performing asset | `buy-products(payment_method:, return_payment_link:true)` → pay link → auto-poll (step 6a) | yes | yes | | `account_fiat` | Bitrefill USD/EUR account balance | `buy-products(payment_method:"balance", balance_currency:"USD"\|"EUR")` | no | no | - `account_match` applies **only when the worst performer is BTC** — `buy-products` can debit a @@ -387,11 +389,40 @@ the entry for multiples; `package_id` is deprecated). Balance payments settle in `auto_pay` flag — pick the sub-account with `balance_currency`: - `account_match`: `buy-products(cart_items=[{product_id, package_value}], payment_method="balance", balance_currency="XBT")` → instant. - `account_fiat`: `buy-products(cart_items=[{product_id, package_value}], payment_method="balance", balance_currency="USD"|"EUR")` → instant. -- `onchain_match`: `buy-products(cart_items=[{product_id, package_value}], payment_method=, return_payment_link=true)` → pay via the returned link → poll `get-invoice-by-id` until `status == "complete"`. +- `onchain_match`: `buy-products(cart_items=[{product_id, package_value}], payment_method=, return_payment_link=true)` → returns an `unpaid` invoice with a pay address / link → **auto-poll** (step 6a). Then read `get-invoice-by-id` for the delivered redemption code / PIN / QR (there is no `get-order-by-id` tool). Log: `invoice_id`, product, amount, chosen funding token, payment method. +### 6a. Auto-poll crypto payments — no user prompts + +Balance settlements (`account_match` / `account_fiat`) clear instantly, so they skip this step. Any +**conventional crypto payment** — `onchain_match`, or any `return_payment_link` path that hands back +an `unpaid` invoice with a pay address — must be **polled automatically until it settles**, with no +"say check to poll" round-trips: + +1. Show the pay address / link / amount once, so the user can broadcast the transaction. +2. Then poll `get-invoice-by-id` on a fixed cadence — roughly **every 30 s**, automatically, without + waiting for the user — until the invoice reaches a terminal status. **Cap the loop at ~60 min** (or + the invoice's own expiry, if it reports one) so the loop always terminates even if no terminal + status ever arrives. If the runtime can schedule a cross-turn wake-up, space the polls with one + rather than blocking the session; otherwise fall back to a blocking wait or polling on the user's + next message. Do **not** prompt the user between polls. +3. **Terminal states** (the Bitrefill reference documents the status enum as + `unpaid → payment_detected → payment_confirmed → complete`): + - `payment_confirmed` → the crypto was actually spent; the worst performer is sold. The retune + gate (step 7) is now satisfied — retune immediately, then keep polling for delivery. + - `complete` → read the redemption code / PIN / QR from `get-invoice-by-id` (this Bitrefill MCP + server does not expose `get-order-by-id` — see the step-6 note) and finish. + - the **time cap (or the invoice's expiry) elapses** with the invoice still `unpaid` — i.e. it + never reached `payment_confirmed` → **stop** and report the invoice expired unpaid (no sale, so + **no retune** — the worst performer was never spent). Offer to re-issue a fresh invoice. + - a **partial / underpaid** invoice (amount received below the total) → stop and surface the + shortfall; do not retune. The user must top up or request a refund. + +A user "check" / "poll" message during the window just triggers an immediate extra poll — it is +never *required* to advance the flow. + ### 7. Retune (MCP) — only if the worst performer was sold Retune **only** when settlement used `account_match` or `onchain_match` (the worst performer was actually sold). If settlement used `account_fiat`, **skip the retune** and report: "paid from fiat balance; portfolio unchanged." @@ -410,7 +441,7 @@ mcp__qupick__optimize({ > "Buy a $20 Steam gift card and dump my worst crypto" -1. **Config** — `config.json` has `funding.priority = ["account_match","onchain_match","account_fiat"]`, `denomination.policy = smallest_gte`; `QUPICK_API_KEY` is set. +1. **Config** — `config.json` has `funding.priority = ["account_match","onchain_match","account_fiat"]`, `denomination.policy = smallest_gte`; the `.mcp.json` Bearer key is set. 2. **Currencies** — static map gives 11 tickers. 3. **Agent** — `get_agent` succeeds → read the basket; skip creation. 4. **Product** — `search-products("Steam", country="US")` → `steam-usa`; `product-details` → `smallest_gte($20)` picks `package_value = "20"`, price $21.60, accepts `bitcoin`/`ethereum`/`solana`/`usdc_base`. @@ -427,7 +458,7 @@ This skill executes real-money purchases. See [`skills/bitrefill/references/safe - Confirm before every purchase — step 6 is the single, non-negotiable approval stop. `buy-products` is deliberately **not** on the Claude Code allowlist, so the harness also prompts. - Stop before `buy-products` unless the user opts into a real purchase (real money). - Treat codes as cash — never log or paste redemption codes in public channels. -- Use a dedicated low-balance account. `config.json` (real email) and `QUPICK_API_KEY` (the agent's secret key) are local-only — `config.json` is gitignored and the key never goes in git. +- Use a dedicated low-balance account. `config.json` (real email) and the API key in `.mcp.json` (the agent's secret key) are local-only — both files are gitignored, so the key never goes in git. - Log every purchase: `invoice_id`, product, amount, funding token, method. The retune in step 7 is irreversible — when it fires, the spent asset is removed from the basket permanently until the user re-adds it manually. It fires only when the worst performer was actually sold (`account_match` / `onchain_match`).