Skip to content
4 changes: 2 additions & 2 deletions .mcp.json.example
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`:

Expand All @@ -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`

Expand Down
164 changes: 104 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <key-from-email>" }
```

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,
Expand Down Expand Up @@ -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 |
|-------|---------|
Expand All @@ -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 … : <key>`. 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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
- 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 <your-key>" } } } }
```

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 … : <key>` (`docker compose logs backend`). Paste that value into
`.mcp.json`'s `Authorization` header and reconnect (`/mcp`).
Loading