Release was the one escrow spend path still hardwired to the synthetic
testTxBody backend, so suppliers had no way to resolve Submitted escrows
past the accept window on mainnet (the sweeper parks them 'for supplier
Release'). Adds buildLiveTxForRelease mirroring the Reclaim builder
(supplier payout, Constr4/tag 125 redeemer, Ogmios UPLC eval) and the
detectCborBackend dispatch in buildReleaseTx, reclaim.ts-style.
validFrom clamps to submitted_at + ACCEPT_WINDOW rather than tip-60s
alone: the back-pad by itself violates the validator whenever the tip is
less than 60s past the window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes 2 critical, 2 important, and 5 minor findings from review of 069edcb:
- C1: add a happy-path test that reaches postOcrEscrow.ts's escrowLockFloor
call and asserts the locked amount; verified it fails against a bare
economicTotal and passes against the real code.
- C2: gate /v1/ocr/extract (auth + rate limits) before the 16mb body parser
so an unauthenticated caller can no longer force a full buffer/JSON.parse.
- I2: hoist result/used/escrowRef in the OCR one-shot handler so the failure
path records escrow_ref/supplier/cost when an escrow was actually posted,
and emit an ocr_failed structured log line mirroring ocr_settled.
- I3: export venueAttribution and cover its sanitization directly; add a
stubbed happy-path route test asserting the first-party-brokered
disclosure label reaches the response.
- Minor: drop unused hfWeightsSource end to end, rename a misleading test,
make the OCR capability template's pinning language conditional on the
self-hosted upstream, tighten the /v1/ocr path-prefix guard, and make
preflight's required_lovelace use the same escrowLockFloor the builder
locks (exported from the shared tx barrel for gateway use).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the ocr.page.extract.chandra-ocr-2.v1 capability end to end: the
postOcrEscrow builder (locking the min-ada floor for the Submitted-state
datum, required since the min-ada fix), the gateway OCR route that selects
an OCR supplier by capability, posts a per-page escrow, calls the supplier,
verifies and settles, and returns content plus the signed receipt labelled
first-party-brokered. Supplier OCR handler with a hosted document-API
upstream and buyer-SDK OCR support included.
Squashed from the 2026-08-06 working subset; the deploy pack is
intentionally excluded (it lives in a private operator repo).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The escrow locks payment + both bonds and the validator requires exact
value preservation (value_equal) on Claim and Submit. Submit grows the
inline datum ~43 bytes (submitted_at None->Some, result_receipt_hash
None->Some 32 bytes). When capability_id is long enough, the
Submitted-state output's min-ada exceeds the locked total; lucid bumps
the continuing output, value_equal fails, and the validator errors with
no traces (opaque evaluateTransaction 3010/3012). Measured on mainnet at
utxoCostPerByte 4310: a 20-byte chat capability clears a 2.2M lock by
49,310, a 33-byte OCR capability is over by 11,030 - so short-capability
escrows settle and long ones never can, and because Claim's datum is
smaller it always passes, making a Submit-only failure look chain-side.
escrowLockFloor() computes min-ada for the Submitted state plus a margin
and locks max(economic total, that floor) in the post builders. Buyer-
side only, no contract or supplier changes; lucid never bumps once the
lock is sufficient, and the surplus returns through the >= terminal
distribution. postOcrEscrow's identical change ships with the OCR
feature branch. New unit test covers the floor across capability lengths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
planConsolidate returned already-healthy only for wallets with EXACTLY
2 UTxOs, but buildConsolidateWalletTx lands 3 (collateral + working +
the fee-reserve remainder as change). The shape consolidation produces
can never satisfy its own health check, so every wallet-health tick
re-consolidates and pays the ~0.5 AP3X min fee again. Measured in a
production deployment: a 45s ticker submitted 232 consolidations and
burned ~116 AP3X in about 2.5 idle hours; at the default 600s interval
the same loop leaks ~3 AP3X per hour whenever the wallet is not exactly
2 UTxOs.
Healthy is now structural: a pure-lovelace UTxO >= collateralLovelace
exists, at least 1 AP3X sits outside it, and fragmentation is bounded
(<= 6 UTxOs, new optional maxUtxos param). The health check also runs
BEFORE the low-balance guard, so a wallet that can already run script
transactions is never reshaped or refused. walletHealth gains two
independent circuit breakers - halt after 3 consecutive consolidations
that never reach healthy, halt past 6 consolidations per 24h - so any
future non-convergence costs at most ~1.5 AP3X instead of the whole
wallet, plus a docstring that matches reality.
Tests: the exact post-consolidation-shape regression, one-step
idempotence against the builder output model, candidate boundaries
(native assets, 1,628-lovelace-short), healthy-below-reshape-threshold,
and breaker behavior including error and reset paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The demo endpoint lost its session on nearly every second message: affinity
required clients to re-echo the assistant reply byte-identically, so clients
that strip <think> blocks or reshape whitespace (Open WebUI, litellm) missed
every turn, opened a fresh escrow session on the next free single-slot
supplier, and left the old one pinned. Open WebUI's background title/tag-gen
requests each pinned another supplier — one message could exhaust all three
kimi replicas ("no available chat supplier").
- affinity: lenient assistant-content matching (think-strip + whitespace
collapse); user/system/tool messages and tool_calls stay byte-strict;
strict prefixes outrank lenient ones. The mirror is never rewritten with
client text, so receipt hashes stay valid.
- demoChat: trim trailing assistant/system messages (regenerate semantics)
instead of 400 "the last message must have role user or tool".
- supplier: roll the transcript back when the LLM call fails so retries
don't duplicate the delta (ChatSessionStore.truncateTranscript).
- demoChat: LRU-evict a demo session when no supplier is available (supplier
/v1/chat/end frees the slot, sweeper Accepts the escrow) and retry the
open with selectCandidates ignoreStatusFor to bypass the ~20s-stale
indexer status poll. Max 2 evictions per request.
- sweeper: idle janitor closes demo sessions unused for DEMO_SESSION_IDLE_MS
(default 3 min, below the supplier's 5 min CHAT_IDLE_TIMEOUT_MS) so
abandoned one-off sessions release their suppliers quickly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MVP demo: one operator-funded API key anyone can plug into an IDE/harness,
running real escrow-per-session marketplace conversations (llm.chat.v1)
behind the standard OpenAI /chat/completions wire shape.
- shared: ChatMessage gains tool role + tool_calls; normalizeChatMessage/
chatMessagesEquivalent keep supplier transcript and gateway mirror
hash-identical for receipts (field presence is part of the identity)
- supplier: /v1/chat/message accepts {messages,tools,tool_choice} alongside
legacy {content}; callOpenAiStream forwards tools, accumulates fragmented
delta.tool_calls, allows content-less tool turns; done frame carries the
assistant message verbatim + finish_reason + usage
- gateway: demo flag on api_keys (additive migration), create-demo-key CLI,
withdraw 403 + hidden deposit address for demo keys, per-IP demo rate
limit (demo keys skip the shared per-key limit), models filtered to chat
capability
- gateway executor (demoChat.ts): transcript-prefix affinity maps stateless
calls onto open sessions, forwards only unseen deltas, transparently
reopens on supplier idle-close; never falls back to one-shot (its settle
path assumes <=1 in-flight escrow per wallet)
- mutex scope: key mutex held only for chain work (open/close); off-chain
turns serialize on per-session locks so replicas serve users concurrently
- sweeper: chat-aware reclaim guard (35min vs 11min) and abandoned-session
cleanup; terminal state confirmed via on-chain queryUtxo (indexer keeps
lineage rows at their old state forever)
- deploy: kimi-chat replicas 2+3 compose files; chat idle timeout 2.5min
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildLiveTxForEscrow called .complete() with no presetWalletInputs, so lucid was
free to spend the dedicated 5-ADA collateral UTxO as an ordinary input. On
back-to-back requests this left the wallet with no collateral candidate and the
subsequent Accept failed with collateral_required, stranding the escrow.
Reserve the smallest pure-ADA UTxO >= 5 ADA and fund PostEscrow from the rest
(falls back to default selection for single/large-UTxO cases, which keep an
ample pure-ADA change UTxO anyway). Benefits the buyer too (same shared path).
Adds reserveCollateralInputs unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
Adds a conversational supplier capability and a free demo, both streaming.
New paid `llm.chat.v1` type: one escrow per conversation (flat fee). "Start
chat" posts the escrow with a session-init placeholder prompt_hash and the
supplier Claims (reserving its single slot); turns then stream off-chain via
SSE with zero chain interaction; "End chat" has the supplier Submit a receipt
over the full transcript and the buyer Accept — the moment the user is charged.
The on-chain validator is content-agnostic (pins but never verifies the
hashes), so this reuses the existing contract unchanged. A supplier-side idle
watchdog auto-ends abandoned sessions before deliver_by.
Free Kimi K2.6 chat demo: a synthetic "demo:kimi-chat" tile that uses the same
chat UI but proxies straight to OpenRouter (no escrow, no payment), mirroring
the existing Piper demo.
Shared:
- postChatEscrow.ts + chatSessionPromptHash() (session-init prompt_hash helper
shared by the buyer builder and the supplier start gate).
Supplier:
- CAPABILITY_KIND="chat-session" (+ CHAT_IDLE_TIMEOUT_MS); callOpenAiStream()
streaming client; ChatSessionStore + endChatSession(); /v1/chat/{start,
message,end} handlers with idle + hard-cap watchdog timers.
Buyer:
- Marketplace.startChat()/endChat() with relaxed (session-init) receipt
verification + crypto verifyReceipt; /v1/chat/{start,message,end} routes
(SSE passthrough) and a free /v1/chat-demo/message OpenRouter proxy;
OPENROUTER_API_KEY/OPENROUTER_BASE_URL config; ChatForm.tsx + Dashboard
KIMI_DEMO_SUPPLIER tile and llm.chat.v1 dispatch.
Deploy:
- docker-compose.supplier-kimi-chat.yml + supplier/.env.kimi-chat.example.
Typecheck passes across all workspaces; full vitest suite green except 15
pre-existing time-bomb/RED failures (tx-claim/accept/post-advert live-CBOR
fixtures with hardcoded past deadlines + the M0-C golden-datum test), confirmed
identical on a clean tree.
Co-Authored-By: Claude Opus 4.8 (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 comment at every setMinFee call site said "500K is a comfortable
floor" but the actual value was 200K — the floor wasn't high enough to
cover lucid 0.4.30's Conway-era fee underestimation. Smoke-tested
mainnet Accept tx hit Ogmios code 3122: lucid declared 343,132 lovelace
fee but Ogmios required 347,726 (off by ~5K). With the floor at 200K
the floor was inert (343K > 200K), so the underestimated fee stood.
Bumping all six builder paths (PostEscrow, Claim/Submit, Accept,
Reclaim, PostAdvert, UpdateAdvert/RetireAdvert) to 500K — comfortably
above the empirical minimum across operations, ~0.15 AP3X surcharge
per tx, excess becomes change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lucidContext.ts captured VECTOR_ZERO_TIME at module-load time, so CLIs
that read a .env file at runtime (rather than letting Docker env_file
populate process.env before the runtime starts) got the testnet default
and built txs with a wrong slot origin -- Ogmios rejected with code
3118 "outside validity interval" on mainnet.
Two fixes:
- lucidContext.ts: read process.env.VECTOR_ZERO_TIME_MS inside
applyVectorSlotConfig() so the defensive re-apply in createLucidContext
picks up env vars set after module-load.
- run-cycle.ts / reclaim-orphans.ts: inject VECTOR_ZERO_TIME_MS,
OGMIOS_URL, INDEXER_URL, NETWORK_ID into process.env right after
reading the --env file, before any tx work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lucid's auto-fee calculation under-estimates the publish-refs tx by
~5K lovelace on Vector L2 mainnet (observed: required 372,658, lucid
computed 367,197 → ogmios rejected with code 3122 InsufficientFee).
Bumping the floor to 500K guarantees lucid pays enough. Cost: 0.13 AP3X
overhead, one-shot. Doesn't affect ongoing script-spending txs (those
have their own setMinFee already at 200K, unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the single `lovelacePerOutput` param in publishReferenceScripts.ts
into `escrowLovelace` and `advertLovelace`, exposed via env vars
ESCROW_PUBLISH_LOVELACE / ADVERT_PUBLISH_LOVELACE in the CLI. Defaults
unchanged at 30 AP3X each (60 AP3X total).
Motivation: mainnet rollout where the publishing wallet has only ~57 AP3X
(buyer wallet) — needs to publish at calculated min-utxo (~17 AP3X escrow
+ ~7 AP3X advert = 24 AP3X total) instead of the safe 60 AP3X default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 500K lovelace floor was set higher than the natural fee for both
inlined (~340K) and ref-script (~242K) Plutus V3 spend txs. With it
in place, every script-spending tx pays the same 0.5 AP3X regardless
of whether scripts are inlined or referenced — masking the on-chain
size reduction of ~3.4 KB / -82% per Claim/Submit/Accept measured
end-to-end on Vector L2 testnet.
200K is below both natural fees (so lucid's auto-estimate wins) and
still ~5x above the lucid-evolution 0.4.30 ~5K under-estimate the
floor was originally guarding against.
Expected per-cycle saving on Vector L2 testnet: ~0.32 AP3X
(Claim: 0.34 → 0.24, Submit: 0.34 → 0.25, Accept: 0.35 → 0.23).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ogmiosUtxoToLucidUtxo expected `o.script` to be a hex string, but
Ogmios v6 returns it as `{ language: "plutus:v3", cbor: "<hex>" }`
(or "native"). The typeof === "string" guard never matched, so
scriptRef was always null on returned UTxOs — making
.readFrom([refUtxo]) silently produce a tx with no script witness
and the supplier reject the resulting Claim with 503.
Wired ogmiosScriptToLucidScript() to translate the Ogmios shape into
the lucid-evolution shape across all four script languages.
Verified end-to-end: tmp-repro now reports
"scriptRef: present type=PlutusV3 cborlen=6884" against the published
ref UTxO (250928de…#0).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Static re-exports from tx/index.ts caused vite to trace
loadEscrowScript/loadAdvertScript/buildLiveTxForPublishReferenceScripts
into the buyer UI bundle, hitting the CML WASM "ESM integration proposal"
limitation. Sibling builders avoid this with /* @vite-ignore */ dynamic
imports; we keep symmetry by parking the helpers on a separate
@marketplace/shared/tx/server entry that the buyer never imports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When ESCROW_REF_UTXO / ADVERT_REF_UTXO env vars are set (format
<txhash>#<idx>), Claim/Submit/Accept/Reclaim/RetireAdvert call
.readFrom([refUtxo]) instead of inlining the validator CBOR via
.attach.SpendingValidator(). Lucid-evolution 0.4.30 auto-extracts
utxo.scriptRef into the script-witness map (dist/index.js:1903–1913),
so no separate attach call is needed. Default (env unset) is unchanged.
Adds buildLiveTxForPublishReferenceScripts (a one-shot tx that
publishes both validators as scriptRef-bearing UTxOs at a caller-
supplied address — typically the all-zero-pkh enterprise burn address)
and the supplier tx:publish-reference-scripts CLI that wraps it.
Targeted at testnet trial; mainnet rollout is a separate flip after
verification.
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>
Closes 2/4 builders from the M1-F-D-defer list (postAdvert already live in
7ea7328; updateAdvert and release remain).
Reclaim:
- buildLiveTxForReclaim in liveCbor.ts (mirrors buildLiveTxForAccept):
spends the escrow UTxO with REDEEMER_RECLAIM (Constr3), pays
payment + buyer_bond + supplier_bond to buyer, delegates UPLC eval
to Ogmios via localUPLCEval:false. Validity range = tipMs ± 60s.
- reclaim.ts: replace the unconditional mockSlotToWallclockMs tipMs
computation with `isLive ? Date.now() : mockSlotToWallclockMs(tipSlot)`,
matching accept.ts:77. Adds the live-CBOR dispatch branch.
Funds-at-risk before this fix: with the broken tipMs path, every live
Reclaim was rejected client-side ("tip < deliver_by" using a Nov-1970
tip). If a supplier never Claimed, buyer's 6-7 AP3X stayed locked
indefinitely. Verified recovery of 4 stuck escrows on Vector L2 testnet
(txs f6660e52, 769f16fd, 79970d86, 9f4db14b — 14 AP3X total recovered).
RetireAdvert:
- buildLiveTxForRetireAdvert in liveCbor.ts: spends advert UTxO with
REDEEMER_RETIRE_ADVERT (Constr2), advert script address.
- retireAdvert.ts: live-CBOR dispatch.
- supplier CLI: retire-advert.ts + retireAdvertFlow.ts.
- supplier/package.json: tx:retire-advert script.
Indexer:
- follower/blockProcessor + worker: handle RetireAdvert events,
transition advert row state to Retired.
- db/cache.ts: markAdvertTerminal helper.
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.
buildLiveTxForAccept has its own .complete() call separate from
buildSpendOpenOrClaimedTx (which already routed eval to Ogmios in
7ea7328). The same lucid 0.4.30 CML WASM false-rejection bit Accept
on every escrow — confirmed by 502s on three different pending
receipts in the SPA after the chat/TTS Submit fix landed.
Adding `localUPLCEval: false` to Accept's .complete() options closes
the gap; verified end-to-end against Vector testnet (Accept tx
0385442fd7ef72743194f8fa0b468c2c56bacb90b023f86ec2c3df22b18b7ca8
confirmed under the prior workaround; subsequent Accepts on the
patched build also confirm cleanly).
Two production-essential fixes that surfaced during the path-A TTS
deployment to Vector testnet:
1. localUPLCEval: false on every Plutus V3 spend
──────────────────────────────────────────────
lucid-evolution 0.4.30's CML WASM UPLC evaluator has a false-rejection
bug for our V3 spend pattern: it rejects perfectly valid txs with
"failed script execution Spend[0] the validator crashed / exited
prematurely" while the chain itself accepts them without complaint.
Routing eval to Ogmios's evaluateTransaction (cardano-node's
evaluator) sidesteps the WASM bug and returns correct ex-units.
OgmiosLucidProvider.evaluateTx already implements this path.
This also unblocks the 5 tx-submit-live tests previously skipped
with the comment "UPLC wasm crashes" (M1-F-D-defer / task #28).
Without this fix, every Claim/Submit/Accept tx from our supplier
was failing locally before reaching the chain. The third-party
summitstak.ing supplier worked because it uses a different
toolchain that does not run client-side phase-2 evaluation.
2. AdvertDatum BigInt coercion
────────────────────────────
cbor-x emits float64 (`fb` prefix) for JS Number values that can't
fit CBOR's compact int forms (e.g. POSIX-ms timestamps). Plutus
Data has no float type, so CML.PlutusData.from_cbor_hex panics
with "RuntimeError: unreachable" when it sees one. Coerce
max_output_tokens, max_processing_ms, advertised_at to BigInt
before encoding so cbor-x always emits major-type-0/1 integers.
Mirrors the intAsBig pattern already used in EscrowDatum.
3. Live-CBOR PostAdvert builder
─────────────────────────────
M1-F-4 deferred postAdvert/updateAdvert/retireAdvert/reclaim/
release on the live path. With (1) and (2) in place, posting an
advert via tx:post-advert against Vector testnet now works.
Added buildLiveTxForAdvert in liveCbor.ts (mirrors PostEscrow)
and routed buildPostAdvertTx through it when chain is live.
40 advert/post-advert tests still pass. Verified end-to-end on
Vector testnet: TTS advert posted at
5df793ac12dc426fae7a7a01d56d3e923d3985103d62f0182d9ba63dda871f96#0,
full chat + TTS marketplace lifecycle (PostEscrow → Claim → Submit →
Accept) confirmed completing successfully on chain.
Path-A buyer side: PostEscrow → supplier /v1/audio/synthesize → 202+poll
→ on-chain Submit → receipt verify, returning the audio bytes + signed
receipt to the SPA. Pairs with the supplier-side scaffolding in 3c3e009.
Files
- packages/shared/src/tx/escrow/postTtsEscrow.ts (new):
buildPostTtsEscrowTx() mirrors postEscrow.ts but commits a TTS-shaped
prompt_hash = sha256(canonical({text, voice, format, speed})). The
same hash is exposed as ttsPromptHash() so the buyer SDK and supplier
validation can pin the canonicalisation; any drift fails the
supplier's prompt_mismatch gate. Validates body shape (text non-empty,
voice/format in allow-list, speed ∈ [0.5,1.5]); reuses the chat live-
cbor path which is already capability-agnostic.
- packages/shared/src/tx/index.ts: export the new TTS surface.
- buyer/src/sdk/Marketplace.ts: + submitTts() — full lifecycle mirror of
submitPrompt with TTS-shaped POST body, /v1/audio/synthesize endpoint,
and audio_b64 in the polled response. Receipt verification gates on
prompt_hash via ttsPromptHash() instead of canonicalize(messages).
- buyer/src/sdk/types.ts: + SubmitTtsOptions / SubmitTtsResult.
- buyer/src/server.ts: + POST /v1/submit-tts (mirrors /v1/submit-prompt
shape; routes to marketplace.submitTts).
- buyer/src/ui/components/PiperTTSForm.tsx: two-mode rendering. When
advertRef + payment_lovelace are passed, posts to /v1/submit-tts and
decodes base64 audio into a blob URL. When neither is passed, falls
back to /v1/synth-speech (the path-B direct demo proxy).
- buyer/src/ui/pages/Dashboard.tsx: capability dispatch routes a real
on-chain TTS supplier to the marketplace mode and the synthetic demo
card to the direct mode (its utxo_ref="demo:piper-tts" fails the ref
parser → advertRef is null → demo mode). Same component for both.
Tests still passing: 31 buyer + 29 supplier; both typechecks clean.
Chat path behaviourally untouched.