Restore the canonical mainnet docker-compose.supplier.yml to mirror
testnet's ollama-backed supplier (LLM_BACKEND=ollama, OLLAMA_URL points
at marketplace-mainnet-ollama, Traefik routes mp-suppliers.vector.apex-
fusion.org). The ChatGPT/Codex variant introduced in 965fabd moves to
docker-compose.supplier-gpt.yml with project name + container name
suffixed -gpt so both can coexist on the same host.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The storagetime/chatmock image has neither wget nor curl, so the
previous wget-based healthcheck always failed with "executable file
not found" and the container sat in "starting" forever. The image
does carry a Python interpreter (chatmock is a Flask app), so swap
to a 1-liner urllib probe of /health. /health returns 200 even
before OAuth login, so a red healthcheck now means the Flask server
itself is down.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
storagetime/chatmock on Docker Hub publishes only {latest, 1.36, v1.36}
(all the same digest sha256:757c65c8b28941...). The :v1.35 pin from
the initial commit resolves to "not found". The compose's own comment
forbids :latest, so bump to :v1.36.
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>
The mainnet host (vector-marketplace, 91.98.147.172) has no local
apex-dashboard-mainnet stack — the buyer + indexer already reach the
chain via the public HTTPS endpoint
https://ogmios.vector.mainnet.apexfusion.org. Pin the supplier to the
same pattern: drop the ws://ogmios:1337 env override (let supplier/.env
supply OGMIOS_URL) and drop the apex-net binding so compose-up doesn't
fail on a missing external network.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the "DEFERRED — Do NOT compose up" banner from
docker-compose.ollama.yml and docker-compose.supplier.yml (mirrors
cd0edd7 for buyer.yml + indexer.yml) and adds the missing LIVE_CHAIN=1
inline override that the testnet supplier compose has at line 45 —
without it the mainnet supplier silently falls back to
ReadOnlyOgmiosProvider and Claim/Submit never broadcast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
buyer and indexer mainnet stacks have been running on vector-marketplace
(marketplace-mainnet-buyer + marketplace-mainnet-indexer healthy, 4+ days
uptime for indexer, buyer redeployed today with the auth gate). The
"Wired but not yet exercised" header contradicts reality and would spook
the next operator who reads it.
Note in the new headers points at marketplace.vector.apexfusion.org and
mp-indexer.vector.apexfusion.org so the connection between the compose
file and the live URL is explicit. The genuinely-still-open mainnet-
safety follow-up (ARCHITECTURE.md §9 #5: env-gate / wallet allowlist /
MAINNET=1 flag) is kept as a one-line reference in the buyer header.
supplier and ollama mainnet composes are intentionally left alone — I
don't have evidence of those being deployed via these compose files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removing the hardcoded OGMIOS_URL=ws://ogmios:1337 (apex-dashboard-mainnet
internal hostname) lets indexer/.env supply the real value — hosted WSS for
deployments that don't run a co-located ogmios container.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy dashboard_default name leaked from apex-dashboard on the
testnet host. On a fresh mainnet box with no apex-dashboard, traefik-net
is more accurate. Mainnet-only — testnet compose files keep
dashboard_default since they depend on the existing apex-dashboard host.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shared: VECTOR_ZERO_TIME now reads VECTOR_ZERO_TIME_MS from env, defaults
to testnet genesis (1_752_057_484_000 ms). Mainnet deploys set
VECTOR_ZERO_TIME_MS=1756485600000. Existing testnet deployments unaffected
since env unset → testnet default. Patched in both lucidContext.ts (lucid
slot config override) and liveCbor.ts buildLiveTxForSubmit (submitted_at
slot-alignment).
deploy/mainnet: buyer host changed mp.vector.* → marketplace.vector.* per
operator-owned DNS. Indexer compose drops apex-net network (we point at
the hosted mainnet ogmios over public WSS, not via apex-dashboard-mainnet
which is not deployed on this box).
Co-Authored-By: Claude Opus 4.7 (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.
Second supplier instance on apex-dashboard, dedicated to the new
audio.synthesize.piper.v1 capability. Reuses supplier/Dockerfile but
sets CAPABILITY_KIND=tts so the boot dispatch mounts /v1/audio/
synthesize routes instead of /v1/chat/completions.
PIPER_URL points at the public CF hostname for the openedai-speech-min
host on apex-simulation (no shared docker network with this host).
Wallet, advert UTxO, .env.tts, and Cloudflare DNS+Skip rule are
managed out-of-band; this compose just brings up the container that
serves the marketplace-side of the TTS lifecycle.
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)
Stand up a second deploy target on apex-simulation dedicated to
"simulation"-style supplier capabilities, starting with CPU-only PiperTTS
served via openedai-speech-min behind Traefik on :80.
- deploy/simulation/docker-compose.traefik.yml: Traefik latest with
introspection API, settles on Docker daemon 29.x API negotiation
- deploy/simulation/docker-compose.tts-piper.yml: openedai-speech-min
with curl-based healthcheck (image lacks wget, which Docker would have
marked unhealthy and Traefik would filter from the docker provider)
- deploy/simulation/deploy.sh: rsync-based deploy mirroring the
apex-dashboard pattern; selective --traefik-only / --tts-only flags
- deploy/simulation/install-voice-extras.sh + voice-mappings.py:
idempotent post-deploy step that downloads en_US-lessac-high voice
files into the bind-mount and registers a "lessac" alias in
voice_to_speaker.yaml; survives a host wipe without manual steps
- deploy/simulation/README.md: full host runbook
- .gitignore: exclude data/ recursively, *.log, .vitest-cache, .DS_Store,
__pycache__/, *.pyc, docs/*.zip