Adds an authenticated upload flow to the buyer web app: an operator
uploads a PDF, the server extracts + paragraph-aligned-chunks it
(~2.2k tokens), then runs a hierarchical map-reduce summarization by
paying marketplace suppliers per chunk — one on-chain escrow per map
call and per reduce node. One book upload fans out into dozens of real
mainnet escrows (the intended marketplace traffic) while producing a
genuine full-book summary.
Server module (buyer/src/pdf/): PDF extraction (text-only, rejects
scanned), token-aware chunker, supplier discovery/round-robin filtered
to capable models (kimi/deepseek/gpt; excludes qwen2.5:0.5b), pre-spend
cost estimate + wallet-floor guard, and a JobStore orchestrator that
reuses submitPrompt → findSubmittedEscrowRef → runAccept per call.
v1 serializes PostEscrow under a mutex (the shared tx builder can't pin
wallet inputs, so parallel escrows would double-spend); retries failed
calls on a different supplier then continues with gaps. 6 /v1/pdf-*
routes (multipart upload, estimate, confirm-gated start, SSE progress,
result, .md download) behind the existing session auth + page/chunk/
balance caps. SPA adds a 4-step BookSummarizer wizard.
Tests: chunk/estimate/supplier-pool/extract unit tests + orchestrator
(happy/gap/failover) + routes integration. Full buyer suite green
(181 tests); SPA build + typecheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Periodic setInterval ticker + on-failure backstop from the buildClaimTx
catch in server.ts. Both reuse the existing single-slot lock and call
runConsolidateWallet under the hood — planConsolidate short-circuits to
already-healthy in the steady state so no tx, no fee on a clean wallet.
Driven by deepseek-lane outage 2026-05-28 (supplier wallet drifted to 14
UTxOs with 9 dust 2-AP3X outputs; Claim tx coin-selector picked dust as
working input → chain_submit_failed; lane stalled until manual
tx:consolidate-wallet).
buyer/scripts/monitor-wallets: surface deepseek + kimi as operator wallets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional OPENAI_API_KEY env var that supplier/src/openai.ts forwards as
`Authorization: Bearer <key>`. Empty/unset preserves the ChatMock behaviour
(no auth header). Unlocks pointing the openai backend at DeepSeek directly
(https://api.deepseek.com) — no local proxy needed.
New compose: deploy/mainnet/docker-compose.supplier-deepseek.yml (third mainnet
supplier, model deepseek-v4-pro, mp-suppliers-deepseek.vector.apexfusion.org).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a one-shot operational tx that re-shapes any operator wallet to
{5 AP3X collateral candidate, working remainder} so subsequent Plutus
script-spends (Claim/Submit/Accept/Reclaim/RetireAdvert) find a UTxO
that satisfies lucid's setCollateral=5_000_000n default coin selector.
Symptom this addresses: open-webui mainnet supplier wallet held 8 UTxOs
max 2.40 AP3X, every Accept tx failed at lucid .complete() with the
"wallet must hold a pure-ADA UTxO ≥ 5 ADA" error path. Same fragmentation
risk on the other 4 mainnet operator wallets (one big UTxO surrounded by
dust — first script-spend consumes the big one and leaves nothing usable
as collateral).
Phase 1 — new tx:consolidate-wallet CLI (supplier + buyer):
- planConsolidate.ts — pure planner; supports consolidate (N>1 → 2),
split (1 → 2), and already-healthy skip.
- consolidateWallet.ts — live builder; presetWalletInputs pins all
UTxOs, two pay.ToAddress outputs (collateral + working).
- runConsolidateWallet.ts — orchestration (build + submit + awaitTx).
- supplier/src/cli/consolidate-wallet.ts + buyer mirror.
- monitor-wallets.ts — add supplier-tts@vector-marketplace (was
misclassified as 3rd-party).
Phase 2 — defensive pre-check in the 5 script-spend builders:
- assertCollateralCandidate(utxos) fires at the top of
buildSpendOpenOrClaimedTx (Claim/Submit), buildLiveTxForAccept,
buildLiveTxForReclaim, buildLiveTxForRetireAdvert. Throws a
TxConstructionError("collateral_required") pointing at
tx:consolidate-wallet instead of waiting for lucid's deep failure.
Tests: 12 new passing (planConsolidate edge cases + assert helper).
Pre-existing lucid 0.4.30 WASM "unreachable" failures in tx-*-live.test.ts
fixtures are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mainnet marketplace-indexer was stuck in an event-replay loop since
2026-05-14. Diagnosis: OgmiosSource.intersectAt is captured ONCE in
start() from the cursor at process startup. On every WebSocket reconnect
(Cloudflare-fronted WS times out at ~100s idle, plus periodic Ogmios
server restarts), scheduleReconnect calls sendFindIntersection with the
stale start-time intersect, never the latest persisted cursor. Ogmios
seeks to that old point, sends nextBlock forward from there, the worker
re-processes the same blocks and writes saveCursor over the advanced
cursor with the old slot — DB cursor effectively never moves past the
startup point, and events get duplicated each iteration.
Fix:
- Add ChainSyncSource.updateIntersect({ slot, id }) to the interface.
- OgmiosSource.updateIntersect mutates this.intersectAt, so the next
reconnect's findIntersection picks up from the latest saved cursor.
- worker.handleBlock calls source.updateIntersect after every saveCursor.
- worker.handleRollback also calls it so a WS drop right after a rollback
doesn't resume past the rollback point.
- MockChainSyncSource implements as no-op (no reconnect path in tests).
Verified: same shape as the testnet projection-gap bug (memory 95fe6946),
manifesting differently. After this fix the cursor will only ever advance,
and reconnects resume from the freshest persisted point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Codex/ChatGPT backend path for the supplier alongside the
existing Ollama one. The mainnet supplier is repointed at a localhost
ChatMock proxy (RayBytes/ChatMock, pinned :v1.35) that authenticates
against a ChatGPT Pro subscription via OAuth.
- supplier/src/openai.ts: HTTP client for OpenAI-compatible
/v1/chat/completions. Same return shape as ollama.callOllama so
jobRunner can branch cleanly.
- supplier/src/jobRunner.ts: branch on config.llmBackend.
- supplier/src/config.ts: LLM_BACKEND ("ollama" default | "openai"),
OPENAI_BASE_URL, OPENAI_TIMEOUT_MS (default 180_000 — GPT first-token
latency).
- supplier/src/cli/gen-keypair.ts: standalone Ed25519 keypair generator
+ bech32 address derivation. Used to bootstrap a fresh supplier
wallet without round-tripping through tx:post-advert --dry-run.
- deploy/mainnet/docker-compose.chatmock.yml: long-running chatmock
serve + one-shot --profile login flow.
- deploy/mainnet/docker-compose.supplier.yml: rewritten for openai
backend; Traefik routes at mp-suppliers-gpt.vector.apexfusion.org so
the unsuffixed slot stays free for a future supplier with a
different backend.
- tests/unit/supplier-openai.test.ts: unit coverage for callOpenAi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an Express requireAuth middleware on /v1/* + the SPA shell and a
Vite/React login screen so anyone who reaches mp-buyers.* must authenticate
before driving the operator's wallet via /v1/submit-prompt or /v1/accept.
Why: the buyer-app sits on a public HTTP port and signs txs with a
server-held private key. Until now the only gate was network reachability.
Mechanics:
- BUYER_PASSWORD + SESSION_SECRET (>=32 chars) loaded by config.ts and
surfaced as createApp() deps. Production refuses to start without both.
- buyer/src/auth.ts: HMAC-SHA256 signed buyer_session cookie (HttpOnly,
SameSite=Lax, Path=/, optional Secure), 7-day rolling expiry,
timing-safe password compare, in-memory 5/15-min per-IP rate limiter.
- /v1/auth/{login,logout,whoami}. Login is rate-limited; 6th wrong
attempt returns 429 + Retry-After. Success resets the bucket.
- SPA boot block (__BUYER_BOOT__ injected by server.ts) is gated by the
same cookie so the buyer's address never reaches an unauthenticated
browser.
- New <AuthProvider>/<RequireAuth> in buyer/src/ui/state/AuthContext.tsx
probes /v1/auth/whoami on mount and a scoped window.fetch patch drops
to the login screen on any mid-session 401 from /v1/* (excluding
/v1/auth/* so wrong-password 401s stay local to <Login>).
- Tailwind login card at buyer/src/ui/pages/Login.tsx. App.tsx wraps in
<RequireAuth> and adds a "Sign out" button in the nav.
Tests: tests/unit/buyer-server-auth.test.ts covers sign/verify, rate
limiter, cookie helpers, login flow (400/401/204/429), whoami
(no-cookie/valid/tampered/expired), rolling renewal, logout, gated
/v1/*, /healthz still open, and the no-auth-deps construction shape.
Updated buyer-server-config + buyer-ui-smoke for the new required env
vars and the AuthProvider wrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
While checking whether last night's localUPLCEval: false fix unblocks
the 5 tx-submit-live tests skipped under TODO(M1-F-D-defer), found
that those tests still crash — but at a DIFFERENT spot than before.
Original UPLC issue (now fixed in production via 7ea7328 + fd24598):
- lucid's CML WASM UPLC interpreter falsely rejected our Plutus V3
Submit/Claim/Accept txs. End-to-end chat + TTS lifecycle now works
on Vector testnet by routing eval to Ogmios.
New / remaining test-side issue:
- Even with a proper evaluateTransaction mock response, lucid's
applyUPLCEvalProvider → CML TransactionBuilder.set_exunits still
panics with wasm "unreachable" inside the test fixture. The
production path doesn't hit this (real Ogmios + real wallet UTxOs).
Whatever the test-mock + CML interaction trips, it's separate from
the original UPLC bug.
Refinements landed
- liveCbor.ts: opt-in LIVECBOR_DEBUG=1 env that prints the full inner
err + WASM stack from rethrowAsTxError, so the next investigation
doesn't need to re-add this scaffolding.
- tx-submit-live.test.ts:
- setupSubmitHappyPath() now mocks "evaluateTransaction" with a
proper EvalRedeemer-shaped response (useful baseline for any
future un-skip attempt; not enough on its own to make the tests
pass).
- The first skipped test's TODO comment updated to record what's
fixed in production vs. what remains as a test-infra bug.
Test counts unchanged (3 pass, 5 still skipped); production path
still verified end-to-end on testnet. Task M1-F-D-defer stays pending
but the original UPLC-interpreter blocker is gone.
The on-chain Submit tx anchors sha256(canonicalize(receipt)) so the
receipt's signature is provably tied to a specific Cardano block — but
without persisting the receipt content + the actual response bytes
nowhere can buyer or supplier produce the artefacts a dispute would
need to argue about. This commit closes that gap on the buyer side.
What's new
- buyer/src/db/schema.ts (new):
`responses` table keyed by escrow_ref, indexes on completed_at /
capability_id / supplier_pkh. Stores metadata + receipt JSON +
Ed25519 signature; large blobs (request envelope, response bytes)
live on disk per-escrow.
- buyer/src/db/archive.ts (new):
ResponseArchive class — persistChat / persistTts / list / get /
readRequest / readResponse. Filesystem layout under ARCHIVE_DIR:
<txhash>_<index>/request.json (always JSON)
<txhash>_<index>/response.{ext} (txt/json for chat, mp3/wav/… for TTS)
Synchronous (better-sqlite3 + fs.writeFileSync) — volume is low and
the simplicity is worth more than async overhead.
- buyer/src/server.ts:
- AppDeps.archive plumbed through createApp.
- /v1/submit-prompt + /v1/submit-tts persist BEFORE returning to
the SPA, using canonicalize() for chat (same bytes the supplier
hashed; JSON.stringify produced different bytes → wrong sha256).
Persist failures are logged but never abort the response — the
on-chain receipt is still authoritative.
- New read-only endpoints:
GET /v1/responses?limit=… list metadata + parsed receipts
GET /v1/responses/:ref single-row detail
GET /v1/responses/:ref/request request artefact (application/json)
GET /v1/responses/:ref/response response artefact (verbatim bytes)
- URL path uses "<txhash>_<index>" (the dir-safe form) since "#"
can't appear unencoded in URLs (fragment delimiter); server
normalises back to canonical "<txhash>#<index>" for SQLite lookup.
- 503 service_unavailable on /v1/responses* when boot has no
archive (ARCHIVE_DIR unset or DB unopenable) — the rest of
the buyer-app still works.
- buyer/src/ui/pages/TaskHistory.tsx:
Joins indexer escrows (chain state) + /v1/responses (archived
artefacts) by escrow_ref. Each completed lifecycle gets an
archive panel showing:
- Request panel (above, chronological order): human-readable
summary — first user message for chat, `text + voice + format +
speed` for TTS, raw JSON fallback otherwise.
- Response panel: assistant text inline for chat, <audio controls>
for TTS; Download response / Download request anchors.
- "Verify hash" button — fetches the response bytes, computes
sha256 via crypto.subtle, compares to receipt.response_hash;
green ✓ on match, red ✗ with truncated computed/expected on
mismatch.
- Collapsible "Receipt + signature" with the full canonical
receipt JSON + the supplier's Ed25519 hex sig.
- buyer/src/config.ts: + ARCHIVE_DIR env (default "./data/archive").
- buyer/Dockerfile: builder gains python3/make/g++ + the
better-sqlite3 native-binding fetch step (mirrors indexer/Dockerfile);
runtime image stays toolchain-free.
- deploy/testnet/docker-compose.buyer.yml: bind-mounts
../../data/buyer/archive (repo-root-relative, not file-relative)
into /data/archive. ARCHIVE_DIR env points at that path.
Tests
- tests/unit/buyer-archive.test.ts (new, 14 tests): persistChat /
persistTts unit coverage (filesystem layout, INSERT-OR-REPLACE
semantics, content-type → extension mapping for mp3/wav/opus/aac/
flac, list ordering + limit, single-row get/read miss-vs-hit) +
HTTP integration (503 when disabled, happy paths returning
metadata / request / response artefact bytes with correct
content-types, 404 on unknown ref).
- tests/unit/buyer-ui-smoke.test.tsx: TaskHistory fetch mock now
also handles /v1/responses (returns empty list — same code path as
"archive disabled" production case).
45 buyer tests passing; chat + TTS lifecycle verified end-to-end on
Vector testnet; "Verify hash" button confirms sha256(response bytes)
matches receipt.response_hash for both capabilities.
Three buyer-app changes that all landed during the same browser-debug
session and were verified end-to-end on Vector testnet:
UX-3-history — /tasks page is now indexer-driven (M1-F-5 followup)
- TaskHistory.tsx fetches /v1/indexer/escrows?buyer=<pkh> from the
buyer-app proxy, groups rows by (posted_at, supplier_pkh) into
lifecycle cards, and renders newest-first with state pill,
capability/model/price, and the tx-hash chain
- main.tsx drops the LocalStorageTaskHistoryStore (chain is the source
of truth; SDK uses MemoryTaskHistoryStore for in-flight only)
Path-B PiperTTS demo — capability-routed UI without escrow yet
- buyer/src/server.ts: POST /v1/synth-speech proxies to the openedai-
speech-min host; validates {text, voice, format, speed} and returns
audio bytes with the upstream Content-Type
- buyer/src/config.ts: TTS_PIPER_BASE_URL env (optional; absence keeps
/v1/synth-speech disabled with a clean 503)
- buyer/src/ui/components/PiperTTSForm.tsx: capability-specific form
(text + voice + format + speed slider) with inline <audio controls>
and a Download .mp3 anchor; blob URLs revoked on each submit + unmount
- buyer/src/ui/pages/Dashboard.tsx: capability registry — switch on
selected.capability_id to PromptForm vs PiperTTSForm; appends a
synthetic "PiperTTS (apex-simulation)" demo card so the capability
routing is exercisable before the on-chain TTS supplier ships
- deploy/testnet/docker-compose.buyer.yml: TTS_PIPER_BASE_URL points
at https://tts-piper.vector.testnet.apexfusion.org
Server-side error logging on /v1/submit-prompt
- buyer/src/server.ts: catch block now logs reason + message + stack
(the demo session lost ~30 minutes to a "<!DOCTYPE..." JSON-parse
error caused by Cloudflare replacing origin's clean 502+JSON with
its own HTML page; the actual upstream cause was a "wallet must
hold a pure-ADA UTxO >=5 ADA" lucid-evolution error that was
invisible because the Express handler had no logger)
Tests: 10 new for /v1/synth-speech (validation, defaults, success
shape, upstream 5xx, fetch-throw); existing buyer-ui-smoke supplier
count expectation updated for the synthetic Piper card (N+1)