feat(qupick): auto-poll crypto payments; pin WEB_CONCURRENCY=1; hosted-first README - #6
Conversation
Add step 6a: conventional crypto payments (onchain_match and any return_payment_link invoice that comes back unpaid) are polled automatically every ~30s until a terminal state, capped at the invoice's expiration_minutes, instead of requiring a manual check. Retune stays gated: retune on payment_confirmed; no retune on expiry or partial_payment.
The MCP mount keeps its Streamable-HTTP session table in per-worker memory (fastapi-mcp hardcodes stateless=False), so >1 uvicorn worker 404s MCP follow-up calls (tools/list, tool invocations) ~half the time. Correct the prior claim that the server is stateless and that WEB_CONCURRENCY is safe to scale to vCPU count.
🤖 Augment PR SummarySummary: This PR updates the Qupick operator docs/skill spec to reduce manual intervention during Bitrefill crypto checkouts and to harden production deployment guidance. Changes:
Technical Notes: The guidance relies on fastapi-mcp dispatching tool calls through the FastAPI app (with forwarded 🤖 Was this summary useful? React with 👍 or 👎 |
| 3. **Terminal states:** | ||
| - `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` / `all_delivered` → fetch the redemption code / PIN / QR and finish. |
There was a problem hiding this comment.
complete / all_delivered is listed as a terminal state, but the Bitrefill MCP reference docs in this repo only document invoice statuses unpaid → payment_detected → payment_confirmed → complete; if all_delivered isn’t a real status, this could cause the polling loop to never terminate on delivery.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Fixed in 0d23e39. Removed all_delivered — the repo's Bitrefill reference (mcp.md, api.md) documents the status enum as unpaid → payment_detected → payment_confirmed → complete, so complete is the sole delivery terminal. The loop also has a hard ~60 min time cap as a status-independent backstop.
| 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 state. **Cap the loop at the | ||
| invoice's own `expiration_minutes`** (≈60 min; fall back to 60 min if the field is absent). Space |
There was a problem hiding this comment.
This step depends on an invoice field named expiration_minutes, but I don’t see that field documented elsewhere in the repo’s Bitrefill MCP/REST references; if it’s absent, implementations may incorrectly cap (or fail to cap) polling.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Fixed in 0d23e39. The spec no longer names expiration_minutes (not present in any Bitrefill reference here). It now caps at ~60 min, or the invoice's reported expiry if it exposes one, so termination no longer depends on a specific field name.
| 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 state. **Cap the loop at the | ||
| invoice's own `expiration_minutes`** (≈60 min; fall back to 60 min if the field is absent). Space | ||
| the polls with a wait that survives across turns (schedule the next re-check) rather than blocking |
There was a problem hiding this comment.
“Schedule the next re-check … rather than blocking the session” reads like it requires a background/heartbeat scheduler; if that capability isn’t available in some runtimes, this spec may be hard to implement as written without falling back to blocking waits or user-driven polling.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Fixed in 0d23e39. Reworded to: use a cross-turn wake-up if the runtime offers one, otherwise fall back to a blocking wait or polling on the user's next message — no scheduler is required.
| | **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. | |
There was a problem hiding this comment.
This update correctly calls out per-worker MCP session state, but it now conflicts with the earlier statement in this doc that the server “holds no in-process state”; consider reconciling that wording so operators don’t conclude multi-worker is safe elsewhere in the checklist.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Fixed in 0d23e39. Scoped the line-19 claim to in-process application state (the removed mark-to-market loop) and added a parenthetical pointing at the per-worker MCP session state and the WEB_CONCURRENCY=1 pin, so the two rows no longer conflict.
Add a 5-step hosted register & set up section (qupick.quip.network) as the primary path, and relocate the local backend instructions (docker compose / uv, console-key registration) to a Development section. Fix the Configure anchor that pointed at the removed Connect section.
Claude Code does not expand ${VAR} in .mcp.json headers, so the
Authorization: Bearer ${QUPICK_API_KEY} pattern left the per-agent MCP
tools unauthenticated (401). Paste the key literally into the header
instead; .mcp.json is gitignored, so the secret stays local. The
QUPICK_API_KEY env var now only documents its real consumer,
backend/scripts/mcp_smoke.py.
Address review feedback on PR #6: - drop the undocumented `all_delivered` terminal status; the reference enum is unpaid -> payment_detected -> payment_confirmed -> complete - stop keying the poll cap on the unverified `expiration_minutes` field; cap at ~60 min (or the invoice's reported expiry) so the loop always terminates - give the cross-turn scheduler a fallback (blocking wait / next-message poll) for runtimes without one - reconcile deployment-prep: scope the 'no in-process state' claim to application state and point at the per-worker MCP session caveat
|
augment review |
| `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` → fetch the redemption code / PIN / QR and finish. |
There was a problem hiding this comment.
complete says to “fetch the redemption code / PIN / QR”, but the Bitrefill MCP reference in this repo indicates redemption info is retrieved via get-order-by-id rather than get-invoice-by-id; without clarifying which tool/field to use here, an implementation may not be able to reliably retrieve the delivered code after settlement.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Clarified in 8ef4362, but using get-invoice-by-id, not get-order-by-id. The Bitrefill MCP server mounted here does not expose get-order-by-id — the live tool set is buy-products / get-invoice-by-id / get-product-details / list-invoices / search-products / submit-prepayment-step / update-order. SKILL.md already records this at the end of step 6 ("there is no get-order-by-id tool"); get-order-by-id only appears in the bitrefill reference docs, which describe the fuller upstream MCP. Step 6a now names get-invoice-by-id explicitly so there's no ambiguity. (The reference/live discrepancy in the bitrefill skill is real but separate from this PR.)
| - `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` → fetch the redemption code / PIN / QR and finish. | ||
| - `expired`, or the cap elapses with the invoice still `unpaid` → **stop** and report that the |
There was a problem hiding this comment.
This treats expired as an invoice status, but the Bitrefill MCP reference in this repo only documents unpaid → payment_detected → payment_confirmed → complete; if expired isn’t a real status value, the loop termination should be described purely in terms of the time cap / invoice expiry timestamp to avoid implementations waiting for a state that never arrives.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Fixed in 8ef4362. Termination on a stuck-unpaid invoice now rests on the time cap (or the invoice's expiry), not an expired status — the bitrefill references treat expiry as a 180-min time concept, not a documented status value, so the loop no longer waits on a state that may never appear.
Second review pass on PR #6: - step 6a 'complete' now names get-invoice-by-id for the redemption code (the mounted Bitrefill MCP server does not expose get-order-by-id; the step-6 note already records this) - loop termination on a stuck-unpaid invoice now rests on the time cap / expiry rather than an 'expired' status string, which the Bitrefill reference does not document
|
augment review |
| 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 name or email returns `409 already |
There was a problem hiding this comment.
README.md:45 — This says duplicates return 409 already registered, but the backend error detail is "email or name already taken" (see backend/src/backend/api/routes.py); the mismatch could confuse operators who see a different message.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Fixed in 583ef0b. README now quotes the actual 409 details — email already registered / name already taken / email or name already taken — matching routes.py instead of paraphrasing as 'already registered'.
Third review pass on PR #6: the backend returns 409 with detail 'email already registered' / 'name already taken' (or 'email or name already taken'), not the paraphrased 'already registered' (backend/src/backend/api/routes.py).
Three changes to the qupick skill, deployment docs, and README.
feat(qupick): auto-poll crypto payments until settledAdds step 6a to
skills/qupick/SKILL.md. Conventional crypto payments(
onchain_match, and anyreturn_payment_linkinvoice that comes backunpaid) are now polled automatically every ~30s until a terminal state,capped at the invoice's
expiration_minutes(fallback 60 min), instead ofrequiring a manual "check to poll" round-trip. Balance settlements
(
account_match/account_fiat) clear instantly and skip the step.Retune gating is unchanged and explicit per terminal state:
payment_confirmed→ worst performer was sold → retune, keep polling for deliverycomplete/all_delivered→ fetch redemption code / PIN / QR, finishexpiredor cap elapsed whileunpaid→ stop, no retune, offer re-issuepartial_payment→ stop, no retune, surface the shortfalldocs: pin WEB_CONCURRENCY=1 to keep MCP sessions intactCorrects two rows in
docs/deployment-prep.mdthat claimed the server isstateless and
WEB_CONCURRENCYis safe to scale to vCPU count. The mountedMCP server (
FastApiMCP.mount_http) keeps its Streamable-HTTP session tablein per-worker memory (fastapi-mcp 0.4.0 hardcodes
stateless=False), so withmore than one uvicorn worker and no shared session store or sticky routing, a
session minted on one worker 404s when a follow-up call (
tools/list, toolinvocations) lands on another. On the live box this produced ~17% 404s on
POST /mcp; pinning to one worker eliminated them.docs: lead README with hosted register/setup, move local to DevelopmentRestructures
README.mdto lead with the hosted server. A newRegister & set up (hosted) section gives a 5-step path against
https://qupick.quip.network(install → configure defaults → point.mcp.jsonat the hosted URL →register_agent, key emailed → setQUPICK_API_KEYand reconnect). The local backend instructions (dockercompose / uv, console-key registration, cold-start note) move to a new
Development section, and the Configure anchor that pointed at the removed
Connect section is fixed.