runOcrOneShot posted the escrow before calling the supplier, so a busy
supplier's 409 stranded an already-paid escrow until reclaim - and the
buyer-side busy-retry multiplies this into several stranded escrows per
page. Add a live /status check per candidate, immediately before its
escrow posts, mirroring sessions.ts's startChat pre-check: "working"
skips to the next candidate without posting, anything else (free,
unreachable, malformed, slow) is treated as usable so the check is an
optimisation, never a gate. Bounded to a 2s timeout so a stalled
supplier cannot meaningfully delay the request.
Existing candidate-fallback, escrow-ref capture, usage recording, and
receipt labels are unchanged.
escrowRefStr was only assigned after submitOcr resolved, but submitOcr posts
the escrow internally and THEN calls the supplier over HTTP — a SupplierError
there (timeout, network failure, non-2xx, malformed response, receipt
mismatch) meant the escrow was already on chain while the failure path still
recorded escrow_ref: null. Given the OCR upstream's 120s timeout budget this
is likely the most common paid-but-failed shape, and the exact one the
reconciliation spine exists for.
Subscribe to the SDK's "progress" event (type "escrow_posted") around each
submitOcr attempt so the ref is captured the instant it lands, regardless of
whether submitOcr goes on to resolve or reject. Listener is scoped per
candidate and removed in a finally so it cannot accumulate.
Adds a regression test proving the failure-path usage row and the ocr_failed
structured log line both carry the real ref instead of null; verified it
fails without the fix.
Co-Authored-By: Claude Opus 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>