CI has been red since the pnpm 10 migration. Four independent causes:
- better-sqlite3: pnpm 10 blocks dependency build scripts by default, so the
native binding was never compiled on CI. Allowlist it via
onlyBuiltDependencies in pnpm-workspace.yaml.
- libsodium-wrappers-sumo 0.7.16: the published ESM wrapper does
`import "./libsodium-sumo.mjs"` but the tarball does not ship that file
(it lives in the libsodium-sumo package). Patch the import to the bare
specifier; local runs only worked because of a hand-copied file in
node_modules.
- tx-accept/claim [live] tests: the live path checks deadlines against
Date.now(), and the fixed fixture timestamps expired in June. Freeze the
clock inside the fixture windows with vi.useFakeTimers.
- CML set_exunits panic ("RuntimeError: unreachable"): with
localUPLCEval: false the provider asks Ogmios for ex-units; the test mocks
answered `{}`, so the provider fell back to a spend redeemer at index 0
while the escrow input sorts to index 1 — CML panics on the unknown
redeemer key. Mock evaluateTransaction with the true index like real
Ogmios.
Also regenerate the advert-datum golden: the fixture builders passed plain JS
numbers to cbor-x (minimal ints, float64 for the timestamp) while the
production codec — whose output is live on mainnet — emits full-width uint64.
Builders now encode integer fields as BigInt so builders, codec, and golden
agree byte-for-byte.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New multi-tenant gateway package: external developers point any OpenAI SDK at
the marketplace. Each API key is its own custodial buyer wallet (AES-256-GCM
sealed under GATEWAY_MASTER_KEY); every request posts an on-chain escrow from
that user's wallet.
- POST /openai/v1/chat/completions (one escrow/call, llm.text.generate.v1,
buffered + pseudo-stream), chat sessions (one escrow/session, llm.chat.v1,
real SSE), GET /openai/v1/models, /signup, /account, /account/withdraw.
- Settles by resolving the Submitted ref via the indexer + awaited Accept;
state-aware reclaim sweeper + per-wallet auto-consolidation for collateral.
- Reuses the buyer Marketplace SDK via new buyer exports ./sdk; new shared
buildWithdrawTx (custodial exit). Runs via tsx; Dockerfile + mainnet compose.
Design in docs/gateway.md. Typecheck passes for all packages; 20 vitest tests
pass. Not yet exercised against a live chain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A one-shot monitor for the vector-marketplace box: checks each wallet in
wallets.json against a threshold (default 10 APEX) via Ogmios
(ReadOnlyOgmiosProvider) and posts a combined Slack message when any wallet is
below it. Re-reminds every 6h while a wallet stays low and posts a recovery
message when it climbs back above, deduped through a persisted state file.
Runs hourly under host cron via `docker compose run --rm` — no long-running
server, no ports/healthcheck. Reuses the buyer/supplier tsx Docker pattern and
the existing balance-query client; addresses come from a static, gitignored
wallets.json so the monitor never reads a private key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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.