[test] Receipt-bound feedback; polymarket-search reads the keyword index - #1330
Merged
Merged
Conversation
…can write
POST /api/feedback {tx, verdict, reason} $0.001. The credential is the
settlement transaction: the ledger records who paid for each call, so a row can
only be written by that wallet, about that call, once.
Two properties carry the product and both are pinned:
- The receipt is the right to speak. The wallet comes from the signed
EIP-3009 authorization (identity-bound like attest and receipts, so a rail
whose payer we cannot verify never sees the route and is never charged). A
`wallet` in the body changes nothing, and the two refusals - unknown
transaction, not yours - are the same sentence and name no slug, so a
stranger learns nothing about whether a transaction is ours or what it
bought.
- One payment, one verdict. sale_feedback.tx is the primary key and the
write upserts: a buyer may change their mind, they cannot stack five
ratings on one call, which is the cheapest way to distort any review
system.
Two verdicts, not stars. A scale invites an average, an average invites a
ranking, and a ranking built on a handful of self-selected reviews is a number
that looks like a measurement and is not one.
What this has over a payment-only rating: we already record sha256 of the exact
JSON bytes each call served, so a verdict is bound to a specific ANSWER, not
only to money moving. A bad verdict requires a reason and is logged with the
slug and that digest, and the digest rides back on the receipt so the buyer can
hash their own copy and confirm they are rating what we served.
The buyer's words go to the log and to /__operator/sales.json (no payer
address) and are never published. The public tally, GET /api/feedback/summary,
is counts only with `raters` as distinct payers, bad counts printed beside good
ones, and is PoW-eligible: honest ratings should be free to read.
The $0.001 is the facilitator's settlement floor and is there because the
payment is the identity, not as a charge for complaining. Every refusal is a
>= 400, so it cancels settlement and costs nothing.
Mutation-checked: the payer match, divergent refusals, the bad-reason
requirement, the log line, DISTINCT raters, the upsert, and a body-parameter
fallback for the caller.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQbt8oAJjLVbTKY2GqviH6
…ly the busy list The nightly corpus caught it: a paid search tool answering "no active market matched bitcoin" while Polymarket lists many. The match has always been ours - an exact substring over the market's question, slug and description - but the only place candidates came from was the volume-ordered list, which makes findability a function of how deep we happen to look. The 08-29 fix paged to 600 rows and named "bitcoin" as one of the terms it restored. A fortnight later it read 0 again, and a hand sweep of the first 3,000 active markets by 24h volume found not one: Fed and football own the top of that list, and Polymarket's bitcoin markets are numerous and individually small. Gamma does have a keyword index, /public-search?q=, and the comment in our own code saying it does not was simply wrong. It is not usable on its own: it is fuzzy, and answers a gibberish query with a Copa America event, so taking its rows as results would turn "no match" into a wrong match. So it is a candidate source and our exact predicate still decides. The index supplies reach, the predicate keeps precision, and an unmatchable query still returns the honest zero. The volume scan stays as the fallback and the top-up, so an index that changes shape or disappears degrades this tool to what it did yesterday instead of emptying it - the same rule polyList follows for their deprecated list shape. Measured after: "bitcoin" found in 57 rows and one request, where it was 0 in 600 rows and six. Two fields follow so a zero stays readable: searchedKeywordIndex, and an honest searchExhausted, which the old code would have claimed the moment the index filled the page without the scan ever running. Six mutations killed: the predicate, the dedupe, the active filter, the exhausted claim, the index flag, and the whole index block. Also: the corpus case asserting ethereum news in a 48-hour window was my expectation, not a defect. 104 items across the eight feeds in that window, 39 matching bitcoin, zero matching ethereum. The case now asks for the best-covered asset and records why. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LQbt8oAJjLVbTKY2GqviH6
MikeyPetrillo
marked this pull request as ready for review
September 12, 2026 20:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
POST /api/feedback {tx, verdict, reason}$0.001. The credential is the settlement transaction: the ledger records who paid for each call, so a row can only be written by that wallet, about that call, once.The receipt is the right to speak. The wallet comes from the signed EIP-3009 authorization; the route is identity-bound like
attestandreceipts, so a rail whose payer we cannot verify never sees it and is never charged. Awalletin the body changes nothing, and the two refusals (unknown transaction / not yours) are the same sentence and name no slug, so a stranger learns nothing about whether a transaction is ours or what it bought.One payment, one verdict.
sale_feedback.txis the primary key and the write upserts. A buyer may change their mind; they cannot stack five ratings on one call.Two verdicts, not stars. A scale invites an average, an average invites a ranking, and a ranking built on a handful of self-selected reviews is a number that looks like a measurement and is not one.
What this has over a payment-only rating: we already record sha256 of the exact JSON bytes each call served, so a verdict is bound to a specific answer, not only to money moving. A
badverdict requires a reason and is logged with the slug and that digest; the digest rides back on the receipt so the buyer can hash their own copy and confirm they are rating what we served.The buyer's words go to the log and to
/__operator/sales.json(no payer address) and are never published. The public tally,GET /api/feedback/summary, is counts only withratersas distinct payers, bad counts printed beside good ones, and is PoW-eligible: honest ratings should be free to read.The $0.001 is the facilitator's settlement floor and is there because the payment is the identity, not as a charge for complaining. Every refusal is a >= 400, so it cancels settlement and costs nothing.
scripts/test-feedback-kit.js(54, offline, in CI) with seven mutations killed: the payer match, divergent refusals, the bad-reason requirement, the log line, DISTINCT raters, the upsert, and a body-parameter fallback for the caller. Four corpus cases; free-tier egress probe, both catalog sweeps, cross-surface, MCP self-consistency and the ledger suites green locally.🤖 Generated with Claude Code
Also in this batch: polymarket-search could not find "bitcoin"
Caught by the nightly corpus. The match has always been ours (exact substring over question, slug, description), but candidates came only from the volume-ordered list, so findability was a function of search depth. The 08-29 fix paged to 600 rows and named "bitcoin" as one of the terms it restored; a fortnight later it read 0 again, and a hand sweep of the first 3,000 active markets by 24h volume found not one — Fed and football own the top of that list.
Gamma does have a keyword index (
/public-search?q=), and the comment in our own code saying it does not was wrong. It is fuzzy (a gibberish query returns a Copa America event), so it is a candidate source and our exact predicate still decides: the index supplies reach, the predicate keeps precision, and an unmatchable query still returns the honest zero. The volume scan stays as fallback and top-up, so an index that changes shape degrades the tool to yesterday instead of emptying it.Measured after: "bitcoin" found in 57 rows and one request, where it was 0 in 600 rows and six. New
searchedKeywordIndex, andsearchExhaustedis now honest — the old expression would have claimed the whole active list was read the moment the index filled the page without the scan running. Six mutations killed.The corpus case asserting ethereum news in a 48-hour window was my expectation, not a defect (104 items in that window, 39 matching bitcoin, zero matching ethereum); it now asks for the best-covered asset and records why.