fix(agent-performance): lowercase safe address on Polymarket subgraph queries - #1037
Conversation
The Polymarket squid resolves traderAgentById(id: String!) and the traderAgent id_eq filter by exact string match against ids it stores lowercased. _fetch_trader_agent and _fetch_trader_agent_bets bound the EIP-55 checksummed Safe address verbatim, so both queries returned null/[] for every Polystrat agent and prediction accuracy and total ROI rendered blank. Omen masked this: its Graph node normalises ID! casing. Normalising on the variables binding matches what _fetch_ct_held_position_keys and _fetch_daily_profit_statistics already do in this file, and keeps it out of the generic _fetch_from_subgraph, where blanket lowercasing would corrupt the free-text questionTitles of GET_MECH_REQUESTS_BY_TITLES_QUERY. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Trader may be unstaked." asserted a staking state the code never checked, and sent the OPE-1923 investigation toward staking rather than the query. The reporter's agent was STAKED throughout. State what was actually observed - no bets came back - and name the causes that fit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The existing helper tests asserted only on the return value, which is identical whichever casing went out, so no test could observe OPE-1923. Assert instead on the request body get_http_response receives, via the _recording_gen helper the file already has. The four lowercase assertions fail on the pre-fix code. SAFE_ADDRESS / SAFE_ADDRESS_LOWER are imported from test_behaviours rather than redefined: the lowercase half was declared and never referenced, and this is the assertion it was written for. Also pin the two neighbours the fix deliberately left alone - _fetch_agent_details forwarding verbatim, and the reworded warning asserting nothing about staking - so a later "make it uniform" sweep has to be a conscious edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mechanical output of `autonomy packages lock`. The four edited files re-fingerprint agent_performance_summary_abci; every other entry is the dependency-CID cascade through chatui_abci, check_stop_trading_abci, decision_maker_abci, tx_settlement_multiplexer_abci, trader_abci, the agent and both services. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section 9 of the scope asks for negative guards on the two nearby sites that read safe_contract_address for non-subgraph purposes and must keep EIP-55 casing: the ERC-20 check_balance `account` and the LiFi quote's fromAddress/toAddress. Over-applying the fix to either is the main way this change could go wrong, and nothing asserted against it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| except StopIteration as e: | ||
| assert e.value is None | ||
|
|
||
| def test_none_agent_bets_warning_makes_no_staking_claim(self) -> None: |
There was a problem hiding this comment.
[scope] Section 9 of the scope lists four kinds of test; three landed here (wire-level, containment, over-application guards) but the fourth did not:
Behaviour-level. With a lowercase-keyed fake subgraph, assert
_get_prediction_accuracyandcalculate_roireturn populated values rather thanNone, and that metrics render numerically instead ofNA.
Every existing test for both paths patches the helper wholesale — patch.object(b, "_fetch_trader_agent_bets", …) here, and patch.object(b, "_fetch_trader_agent", …) at the 12 sites in TestCalculateRoi (test_behaviours.py:2180-2327). So nothing in the suite drives calculate_roi / _get_prediction_accuracy through a subgraph keyed on the lowercased address and asserts the metric comes out numeric rather than NA — which is the user-visible symptom OPE-1923 actually reports.
The four *_sends_lowercased_address tests are the regression gate for the casing itself, so this is not a coverage hole in the fix; it is the one Section 9 item that is neither delivered nor listed under "Deliberately not done" in the PR description. Either add it, or say in the description that it was dropped as redundant given the wire-level tests — so the omission is a decision rather than a gap.
| "make it uniform" sweep a conscious edit rather than silent drift. | ||
| """ | ||
| b = _make_behaviour() | ||
| b.context.params.is_running_on_polymarket = True |
There was a problem hiding this comment.
[question] The containment test is the right idea, but it pins the Polymarket branch — and that branch is the one place where "forwards verbatim" is a latent instance of the very bug this PR fixes.
_fetch_agent_details (requests.py:611-617) sends its Polymarket query to self.context.polymarket_agents_subgraph, i.e. the same case-sensitive Subsquid endpoint as _fetch_trader_agent. It only works today because its single production caller lowercases first (behaviours.py:1093, safe_address = …safe_contract_address.lower()). The scope acknowledges exactly this in Section 10 item 3: the five untouched helpers "are correct today only because their subgraph is Graph-node-backed or their caller lowercases first", and calls the residual risk out explicitly.
Setting is_running_on_polymarket = True here therefore asserts that a checksummed address reaching the squid is the intended contract. If someone later reports blank created_at / last_active_at for a Polystrat agent — the same failure mode, one helper over — the fix has to delete a test whose name and docstring both say verbatim is deliberate.
Flipping this to is_running_on_polymarket = False pins the identical property ("this helper does not normalise; its caller does") against the Graph-node endpoint, where verbatim is genuinely harmless. Was the Polymarket branch chosen on purpose, or just to mirror the two tests above?
| if agent_bets_data is None: | ||
| self.context.logger.warning( | ||
| f"Agent bets data not found for {agent_safe_address=}. Trader may be unstaked." | ||
| f"No bets returned for {agent_safe_address=}. The agent may not " |
There was a problem hiding this comment.
[question] Answering Section 10 item 1 of the scope, which this PR restates under "Placement note for the reviewer": keep the .lower() inside the two requests.py helpers. That placement matches the convention the file already carries (_fetch_ct_held_position_keys:453, _fetch_daily_profit_statistics:761), it gives each helper a stated contract instead of leaving the invariant to whoever calls it next, and it keeps one rule in one module. Moving it to behaviours.py:746 / :930 would put the convention in a second file and re-open the "correct only by caller convention" gap that Section 10 item 3 already flags as residual risk.
That leaves the diagnostic cost the PR honestly names — this warning still prints the checksummed address while the query went out lowercased. Since the whole reason OPE-1923 took a subgraph round-trip to diagnose is that a log line did not describe what actually went on the wire, it seems worth closing here rather than accepting:
self.context.logger.warning(
f"No bets returned for {agent_safe_address=} "
f"(queried as {agent_safe_address.lower()!r}). The agent may not "
"have placed a bet yet, or the bets subgraph may be unavailable."
)The existing assertions still hold (SAFE_ADDRESS in warning, "No bets returned" in warning, "unstaked" not in warning), so test_none_agent_bets_warning_makes_no_staking_claim needs no change. Your call on the exact wording — the point is that the reader can see both forms.
| _unwrap_trader_agent, | ||
| to_content, | ||
| ) | ||
| from packages.valory.skills.agent_performance_summary_abci.tests.test_behaviours import ( |
There was a problem hiding this comment.
[nit] This pulls the whole of tests/test_behaviours.py (310 KB, ~3.5k lines) into test_requests.py for two string constants, so pytest tests/graph_tooling/test_requests.py now imports and collects nothing less than the entire behaviours suite's module body as a side effect.
Cross-test-module imports do have precedent here (trader_abci/tests/test_handlers.py:60), so this is not a convention break. But the repo's usual home for shared test fixtures is a package-local conftest.py — see market_manager_abci/tests/conftest.py (raw_bet) and decision_maker_abci/tests/conftest.py (profile_name). agent_performance_summary_abci/tests/ has none yet; moving SAFE_ADDRESS / SAFE_ADDRESS_LOWER into one (re-exported from test_behaviours.py so its 187 existing uses keep working) would keep the two test modules independent.
Not worth blocking on — flagging it while the file is open.
Code Review — OPE-1923Scope: Polystrat: prediction accuracy shows null despite resolved predictions Key findings
Scope coverage
Confirmed unchanged, per Section 8: Not reviewed
🤖 Coding Agent [Beta] · Automated review against OPE-1923 |
Implements: https://linear.app/valory-xyz/issue/OPE-1923
Description
What:
_fetch_trader_agentand_fetch_trader_agent_betsbound the EIP-55 checksummed Safe address straight into their GraphQLvariables. The Polymarket squid resolvestraderAgentById(id: String!)and thetraderAgent: {id_eq: $id}filter by exact string match against ids it stores lowercased, so both queries returnednull/[]for every Polystrat agent. Those two call sites are the only sources of prediction accuracy and total ROI, so both rendered blank whilepredictions_madepopulated from a neighbouring helper whose caller already lowercased.fetch_performance_data_roundthen endedEvent.FAILandlast_updatedfroze.Why it was invisible on Omen: the same un-normalised code runs on Gnosis, but
predict-omenis served by a Graph node whoseID!scalar normalises case. Sameid: Bytes!in both schemas, two runtimes, opposite semantics.Fix:
.lower()on thevariablesbinding inside each of the two helpers, both platform branches.Type of Change
Changes Made
graph_tooling/requests.py- lowercase the Safe address on thevariablesbinding in_fetch_trader_agentand_fetch_trader_agent_bets(Polymarket and Omen branches), with a docstring note on each.behaviours.py- reword the"Trader may be unstaked."warning at:937. It asserted a staking state the code never checked; the reporter's agent wasSTAKEDthroughout, and the claim sent the original investigation away from the query. String only, no behavioural change.tests/graph_tooling/test_requests.py- wire-level assertions on the address actually sent, plus a containment test.tests/test_behaviours.py- the corrected warning text, plus two over-application guards.packages.json,skill.yaml,aea-config.yaml,service.yaml- mechanicalautonomy packages lockoutput.Placement note for the reviewer
Section 10.1 of the technical scope left one question open: the
.lower()calls go inside the tworequests.pyhelpers, not at the caller sitesbehaviours.py:746/:930that the ticket names. This follows the scope's stated preference and the convention the file already carries (_fetch_ct_held_position_keys:453,_fetch_daily_profit_statistics:739), and each helper has exactly one production caller - the broken site itself - so the blast radius is nil.The honest cost: the warnings at
behaviours.py:752and:937still log the checksummed address while the query used the lowercased one. Moving the two calls to the caller sites removes that mismatch and substitutes cleanly with no other change. Say the word if you prefer it.Deliberately not done
Per the reviewer feedback recorded in the scope ("lower case only for the input of the queries, the rest should be unchanged"), the v2 proposal to normalise all eleven address-carrying helpers was dropped. The other five (
_fetch_mech_sender,_fetch_agent_details,_fetch_trader_agent_performance,_fetch_pending_bets,_fetch_all_mech_requests) are correct today only because their subgraph is Graph-node-backed or their caller lowercases first. That residual risk is stated rather than fixed: if another endpoint moves from The Graph to a squid, the same defect can reappear.test_forwards_the_address_verbatimpins that decision so a later "make it uniform" sweep has to be a conscious edit.Normalisation was not pushed into
_fetch_from_subgraph: that is the shared transport, and blanket-lowercasing there would corrupt the free-textquestionTitlesofGET_MECH_REQUESTS_BY_TITLES_QUERY.POLYMARKET_AGENTS_SUBGRAPH_URLwas not repointed at the Graph-node deployment. It accepts checksummed addresses, which makes it look like a zero-code fix, but the ticket measured it returning months-stale counts at the same chain head - that would turn "accuracy hidden" into "accuracy confidently wrong".How to Test
uv sync --all-groups && uv run autonomy packages syncuv run pytest packages/valory/skills/agent_performance_summary_abci/tests/ -q- 1237 pass..lower()calls inrequests.pyand re-run-k "lowercase or verbatim". The four*_sends_lowercased_addresstests fail; the two containment tests still pass.last_updatedresumes advancing. The ticket's external checks: service 166 -> 53.19% (97 bets, 94 resolved, 50 won); service 32 -> 62.56% (897 / 892 / 558).Checklist
Verification run locally:
tomte format-code;tomte tox -p -e black-check -e isort-check -e flake8 -e mypy -e pylint -e darglint;check-abci-docstrings,check-abciapp-specs,check-handlers,check-hash,check-packages,check-dependencies,check-third-party-hashes,check-doc-hashes,analyse-service,gitleaks;uv lock --check. All pass. No FSM spec diff, as the scope predicted.Technical Scope (from Linear, v3)
Technical Scoping — valory-xyz/trader — v3
TL;DR
.lower()calls, placed on the query-variable binding inside_fetch_trader_agentand_fetch_trader_agent_betsingraph_tooling/requests.py(Section 7 justifies that placement over the caller sites).behaviours.py:748and:932, the two broken sites themselves. Blast radius is zero; nothing else in the repo reaches them."Trader may be unstaked."warning atbehaviours.py:937(Section 7).1. Context
Target repository:
valory-xyz/trader. Cloned at3dbc71df89f6bf3cf9f80910c254f46b3e60eb34— the same commit the report cites. Every line number, helper name and call site below was re-derived from that clone rather than carried over from v1 or v2, because v2 had already had to correct one of v1's (:936→:937). Two corrections to v2 are folded in, both in Section 3.Repo-set decision and reasoning. Evidence weighed in order:
valory-xyz/traderin the TL;DR, root-cause section and reference list, with a concrete path (packages/valory/skills/agent_performance_summary_abci/behaviours.py) and commit.trader.Bug+Polystrat-Omenstratare consistent with a Polymarket trading agent. NeitherBackendnorFrontendis set, so no Pearl repo is implicated by label.Repos considered and excluded:
valory-xyz/olas-operate-middleware— the blank values originate in the agent, not the middleware. Not scoped.valory-xyz/olas-operate-app— Pearl renders whatever the agent's HTTP server returns. Once the metrics populate, the existing UI renders them; no contract change (Section 5). Not scoped.predict-polymarketsubgraph repo — the synced-but-stale Graph-node deployment is a real, separate defect and belongs to its own ticket. It matters here only as the reason the "repoint the URL" shortcut is rejected (Section 7).Architecture reference used: the repo publishes no
.claude/commands/explore-*.md, so per the fallback order this scope is grounded intrader/CLAUDE.mdplus the source files read below. That reference defines no bug-section template, so a Reproduction and a Root Cause section are appended after Section 10.Issue type: Bug.
2. Scope Classification
Bug fix — agent skill, subgraph query layer. Backend/agent only.
fsm_specification.yamledits.service.yamlchange, no env-var change.3. Data-Flow Trace
Both broken paths originate in
FetchPerformanceSummaryBehaviourand terminate at the same squid endpoint.Path A — Total ROI
calculate_roi(behaviours.py:746) readsself.synchronized_data.safe_contract_addressunmodified → calls_fetch_trader_agentat:748→requests.py:500→ on Polystrat selectsself.context.polymarket_agents_subgraph, bindingvariables={"id": agent_safe_address}atrequests.py:508→_fetch_from_subgraph(requests.py:253), which builds the payload viato_content(query, variables=variables)and forwardsvariablesverbatim →GET_POLYMARKET_TRADER_AGENT_PERFORMANCE_QUERY(queries.py:307), declaredquery GetPolymarketTraderAgentPerformance($id: String!) { traderAgentById(id: $id) … }. A checksummed$idmisses,_unwrap_trader_agentyieldsNone,calculate_roireturns(None, None).Path B — Prediction accuracy
_get_prediction_accuracy(behaviours.py:930) reads the same field unmodified → calls_fetch_trader_agent_betsat:932→requests.py:556→self.context.polymarket_bets_subgraph, bindingvariables={"id": agent_safe_address}atrequests.py:564→ same verbatim forwarding →GET_POLYMARKET_TRADER_AGENT_BETS_QUERY(queries.py:354), whose filter iswhere: {traderAgent: {id_eq: $id}}— exact equality. Empty result →None.Correction to v2 (i) — the blast radius is one caller each, not many. Searching the whole
packages/tree for callers of these two helpers returns, outside tests, exactlybehaviours.py:748andbehaviours.py:932. Nothing else in the repo calls either. This is what makes the helper-level placement in Section 7 safe.Correction to v2 (ii) —
predictions_helper.pydoes not call these helpers. v2's change list includedpredictions_helper.pypartly on the strength of a_fetch_trader_agent_betscall at:586. That is a different method on a different class —PredictionsFetcher._fetch_trader_agent_bets(self, safe_address, first, skip)defined atpredictions_helper.py:608— not therequests.pyhelper, which takes(self, agent_safe_address)and is a generator. Same name, unrelated code path. The file is out of scope in v3 regardless (Section 10), but the conflation should not be inherited.Both helpers bind the address in both platform branches.
_fetch_trader_agentbinds it for Polymarket and again for Omen (requests.py:515);_fetch_trader_agent_betslikewise (requests.py:577). One normalisation per helper therefore covers both venues.Why the endpoint's casing rule differs by platform (verified against
skill.yaml):skill.yaml)idcasingpolymarket_agents_subgraphsubgraph.autonolas.tech/squid/predict-polymarket/graphql(:225)polymarket_bets_subgraph:240)polymarket_questions_subgraph:255)olas_agents_subgraphapi.subgraph.autonolas.tech/api/proxy/predict-omen(:135)ID!normalisespolygon_mech_subgraph…/api/proxy/marketplace-polygon(:270)ID!normalisesolas_mech_subgraph…/api/proxy/marketplace-gnosis(:165)ID!normalisesThis table is the substance of the bug: identical un-normalised code is correct on Omen and wrong on Polystrat. It is retained because it explains why the Omen branches of the two fixed helpers are unaffected either way — lowercasing is a no-op there today, and insurance if that endpoint ever moves to a squid.
No response-side matching on the address. Nothing in the skill compares a subgraph response back against
safe_contract_address(no equality test on that value exists inagent_performance_summary_abci), and_unwrap_trader_agentkeys off field names only. So changing the casing of the query input cannot desynchronise any downstream match.Downstream effect. Both
Nonevalues becomeNAmetrics in_build_performance_metrics. The round reports failure,_save_agent_performance_summarylogs "Preserving existing values for failed metrics", andlast_updatedstays frozen — matching the reported freeze.4. File-Level Change List
packages/valory/skills/agent_performance_summary_abci/graph_tooling/requests.py— Modify — lowercase the Safe address on thevariablesbinding in_fetch_trader_agentand_fetch_trader_agent_betsonly (both platform branches of each), and note it in each docstring.packages/valory/skills/agent_performance_summary_abci/behaviours.py— Modify — reword the misleading warning at:937only. No other edit to this file; in particular no.lower()is added or removed here.packages/valory/skills/agent_performance_summary_abci/tests/graph_tooling/test_requests.py— Modify — extend the two existing test classes with wire-level assertions on the address actually sent (Section 9).packages/valory/skills/agent_performance_summary_abci/tests/test_behaviours.py— Modify — cover the corrected warning text; the orphanedSAFE_ADDRESS_LOWERbecomes usable as the expected value in the wire assertions.packages/valory/skills/agent_performance_summary_abci/skill.yaml— Modify — content hash only, regenerated by tooling (Section 8). No hand-editing.graph_tooling/queries.pyis not changed — the queries are correct; only the values bound to them were wrong.handlers.py,predictions_helper.pyandpolymarket_predictions_helper.pyare not changed (Sections 5 and 10).4b. Existing Code to Reuse
The normalise-at-the-binding pattern is already established in the target file, which makes this a consistency fix rather than a new convention:
graph_tooling/requests.py:739—_fetch_daily_profit_statistics— lowercases at the binding ("agentId": agent_safe_address.lower()); the closest precedent to copy verbatim.graph_tooling/requests.py:453—_fetch_ct_held_position_keys— lowercases at the binding ("id": agent_safe_address.lower()) — the exact shape needed here, on the sameidvariable name.graph_tooling/requests.py:816—_hydrate_profit_participants— derivesbettor_id = agent_safe_address.lower()once; its docstring already states "The address is lowercased here so the invariant does not depend on the caller" (:808-809) — reuse that sentence as the docstring note for the two fixed helpers.graph_tooling/requests.py:1028—_fetch_mech_requests_by_titles— normalisessenderwhile deliberately leavingquestionTitlesuntouched in the samevariablesdict; the working example of why normalisation belongs per-parameter, not per-transport.graph_tooling/requests.py:144—to_content(query, variables)— the single point wherevariablesis serialised onto the wire. Reuse as-is for the test assertions in Section 9; do not add normalisation to it or to_fetch_from_subgraph(Section 7).tests/test_behaviours.py:83-84—SAFE_ADDRESS = "0xSafeAddress"/SAFE_ADDRESS_LOWER = "0xsafeaddress"— the mixed-case/lowercase pair the tests already define. Verified:SAFE_ADDRESS_LOWERis referenced nowhere inpackages/today; it is exactly the fixture this change needs.tests/graph_tooling/test_requests.py:572TestFetchTraderAgentand:749TestFetchTraderAgentBets— a class already exists for each helper being changed, each withtest_polymarket_path/test_omen_pathmethods, so the new assertions extend existing classes rather than adding scaffolding.5. API Contract Changes
None.
The agent's performance HTTP surface keeps its existing shape.
AgentPerformanceSummary.prediction_accuracystaysOptional[float], and the metric entries keep the names"Prediction accuracy"and"Total ROI". The only difference is behavioural: fields that resolved toNAwill carry real values.Because v3 adds normalisation only at two query bindings and removes none of the existing caller-side
.lower()calls, no served value changes. In particular the five sites that produce a served or persisted value keep their current behaviour untouched and unexamined:handlers.py:452,:505,:634(each emits"agent_id": safe_addressin a JSON response),handlers.py:797, andbehaviours.py:1092(the lowercased address is the fallback inid=agent_details_raw.get("id", safe_address), so it becomesAgentDetails.idwhen the subgraph omitsid). v2 had to reason about these because it proposed removing them; v3 does not touch them, so response payloads are byte-identical before and after. This is whyhandlers.pyappears in no change list.Dependency note (not scoped here): Pearl consumes this via the agent's own HTTP server. Because field names, types and nullability are unchanged, no coordinated change is required in
olas-operate-apporolas-operate-middleware.Optional/NAremains a legitimate runtime state (genuinely new agent, transient subgraph failure), so existing null-handling on the consumer side must stay.6. Persistence & Schema Updates
No schema change and no migration.
The persisted
AgentPerformanceSummarykeeps its current fields. Two points worth stating explicitly:last_updatedresumes advancing. Nothing needs rewriting by hand.profit_over_timewas never affected. It is sourced through_fetch_daily_profit_statistics, which already lowercases at the binding (requests.py:739) — so no rebuild of historical profit data is needed. Worth confirming during verification rather than assuming.7. Implementation Approach
What the reviewer asked for. The feedback is "make it lower case only for the input of the queries, the rest should be unchanged. We fix what's broken, and leave everything as is (unless it's a very low hanging fruit)." v3 reads that as three instructions: normalise only values bound into GraphQL query variables; restrict the change to the two paths that are actually broken; and leave every currently-working site alone even where a broader rule would be tidier. The dropped work is enumerated in Section 10 so the reduction is visible as a decision rather than an omission.
Where the
.lower()goes — inside the two helpers, on thevariablesbinding. Each of_fetch_trader_agentand_fetch_trader_agent_betslowercases the address as it is placed into itsvariablesdict, in both the Polymarket and the Omen branch, matching what_fetch_ct_held_position_keysand_fetch_daily_profit_statisticsalready do a few lines away in the same file. Each helper's docstring gains the one-line note that the address is lower-cased internally, echoing the wording_hydrate_profit_participantsalready uses.Three things make this the right placement rather than the caller sites. It is the most literal reading of "only for the input of the queries" — the query input is precisely the
variablesvalue, whereas lowercasing the local variable inbehaviours.pywould also change what the surrounding log lines print. It is provably contained: each helper has exactly one production caller and that caller is the broken site itself (Section 3), so nothing else can be affected. And it puts the rule where the file already keeps it, so the next reader finds one convention inrequests.pyinstead of a new one split across two modules.The counter-argument is diagnostic, and it is worth stating because it is the honest cost: with the fix in the helper, the warnings at
behaviours.py:752and:937still log the checksummed address while the query used the lowercased one — a small mismatch of exactly the kind that made this bug hard to trace in the first place. The alternative below removes that mismatch; Section 10 item 1 asks the reviewer to confirm the choice.Alternative — the two caller-side lines named in the report. Adding
.lower()atbehaviours.py:746and:930is equally small, equally effective, and is literally the fix the report specifies. It also makes the log lines print the address that was actually queried. It is not preferred only because it lowercases a local variable used for more than the query, leaves the helpers' own contract undefined for any future caller, and adds the convention to a second file rather than reinforcing it in the one that already carries it. If the reviewer prefers the report's literal wording, this substitutes cleanly with no other change to this scope.The misleading warning at
behaviours.py:937— the carve-out, exercised. The line reads "Agent bets data not found for {agent_safe_address=}. Trader may be unstaked." The staking claim is unfounded — the reporter'scli.logshowsstaking_state=<StakingState.STAKED: 1>— and it actively sent the original investigation toward staking rather than the query. Rewording it to state that no bets were returned for the queried address, citing subgraph unavailability or a genuinely new agent as possible causes and asserting nothing about staking state, is a single string with no behavioural effect, in a file already being opened. That is the "very low hanging fruit" the feedback allows for, and it is the one item in the ticket's own "Also:" list. It is called out separately here so it can be dropped on request without disturbing anything else. The neighbouring warning on the ROI path (:752, "Trader agent data not found or incomplete") is accurate and is left alone.Alternative — normalise centrally in
_fetch_from_subgraph. Rejected, and verified rather than assumed: that function is the shared transport for every query in the skill, andGET_MECH_REQUESTS_BY_TITLES_QUERYpasses free-text market titles through the samevariablesdict. Blanket-lowercasing there would corrupt those titles and break mech-request attribution._fetch_mech_requests_by_titlesalready demonstrates the correct granularity (Section 4b).Alternative — repoint
POLYMARKET_AGENTS_SUBGRAPH_URLat the Graph-node deployment. Rejected. It is config-only —polymarket_trader/service.yaml:310already parameterises the URL as${POLYMARKET_AGENTS_SUBGRAPH_URL:str:…}— and that deployment does accept checksummed addresses, which is what makes it tempting. But the report's measurements show it returning materially lower bet counts and months-stalelastActiveat the same chain head: synced but not current. Adopting it would replace a visibly missing metric with a confidently wrong one, the worse failure mode for a number users read as their agent's performance.8. Constraint Checklist
variablesbindings. Every other address-carrying helper inrequests.pystays as it is, including the ones that forward verbatim today and are correct only because their subgraph is Graph-node-backed. That breadth was explicitly declined (Section 10).safe_contract_address. Two sites read the same field and must keep checksummed casing:behaviours.py:1326(LiFi quotefromAddress) andbehaviours.py:1424, consumed asaccount=on an ERC-20check_balancecontract call at:1441. Neither is a subgraph query. This is the main way to over-apply the fix..lower()inbehaviours.py. The existing ones at:1092,:1118,:1169,:1506and:1693stay exactly as they are;:1092in particular feeds a served and persisted value (Section 5). The only edit to this file is the warning string at:937.handlers.py,predictions_helper.pyorpolymarket_predictions_helper.py. They are correct today; the callers that feed them still lowercase, because those.lower()calls are not being removed.autonomy packages lock/make generators) — CI enforces them.# -*- coding: utf-8 -*-preserved on every touched file.:param:note that the address is lower-cased internally — darglint enforces this.tomte tox, never baretox(pertrader/CLAUDE.md), and never parallelise the autonomy-based environments.graph_tooling/requests.pyandbehaviours.py), not justrequests.py.check-abciapp-specsreports a diff, that signals unintended scope creep.9. Test Strategy
The reason this shipped is a testing gap, not a review miss:
TestGetPredictionAccuracypatches_fetch_trader_agent_betswholesale, so the Safe address never reaches a query and no test could observe its casing. The existingTestFetchTraderAgent/TestFetchTraderAgentBetsclasses do exercise the real helpers, but assert only on the return value, which is identical whatever casing went out. The assertion has to be on what goes onto the wire.get_http_response. Capture thecontentkwarg it receives — built byto_content(query, variables=variables)— decode the JSON, and assertvariables["id"]equalsSAFE_ADDRESS_LOWERwhen the helper is invoked with the mixed-caseSAFE_ADDRESS. This is the regression gate: it fails on today's code and passes after the fix. Add it totest_polymarket_pathandtest_omen_pathin each class, so the Omen branch is pinned too._fetch_agent_detailsis the closest neighbour, sharing the same{"id": …}shape — still forwards the address verbatim. This encodes the reviewer's narrow-scope decision as a test, so a later well-meaning "make it uniform" change has to be a conscious edit rather than a silent drift.:1326) and the ERC-20check_balancecall (:1441) still receive the checksummed address._get_prediction_accuracyandcalculate_roireturn populated values rather thanNone, and that metrics render numerically instead ofNA.:937message, and confirm no existing test asserts on the old "may be unstaked" wording.last_updatedresumes advancing. The report's expected values for services 166 and 32 are usable as external checks — treat them as reported, not as fixtures.10. Unresolved Questions
.lower()calls — confirm. This scope puts them on thevariablesbinding inside the tworequests.pyhelpers, as the most literal reading of "only for the input of the queries" and the containable one. The report's own wording specifies the caller sitesbehaviours.py:746and:930instead, which additionally makes the warning logs print the address actually queried. Both are two lines and equally effective; Section 7 sets out the trade-off. Say which you prefer — the swap changes nothing else in this scope.behaviours.py:937— in or out? Included here as the "very low hanging fruit" the feedback allows: one string, no behavioural effect, and it is the item the ticket itself lists under "Also:". If you read the carve-out more tightly, drop it and the change reduces torequests.pyplus its tests.Superseded — v2 decisions reversed by the 2026-09-04 feedback. Recorded so the reduction in scope is auditable and no reviewer has to diff v2 against v3 to find it. None of these are open questions; they are closed against doing the work:
_fetch_mech_sender,_fetch_agent_details,_fetch_trader_agent_performance,_fetch_pending_betsand_fetch_all_mech_requestsas well. All five are correct today because their subgraph is Graph-node-backed or their caller already lowercases. They are now left untouched. The residual risk is stated plainly: the invariant still depends on convention rather than enforcement, so if another endpoint moves from The Graph to a squid, the same defect can reappear at one of those five sites. That is an accepted consequence of fixing only what is broken, not an oversight..lower()calls that feed them (behaviours.py:1506,handlers.py:797) are no longer being removed. This item and the next are coupled: dropping one without the other would have been a regression..lower()calls inbehaviours.py— dropped. The two overlapping conventions remain visible in the code. Tidiness only; no correctness cost given item 4 is also dropped.agent_idcasing in the HTTP responses — moot. v2 raised this because it was removinghandlers.py.lower()calls. v3 removes none, so the served payloads are unchanged and there is nothing to decide (Section 5).predict-polymarketGraph deployment — out of scope, still unfiled as far as this scope can tell. Independent of this bug and belonging to its own ticket; noted here only so it is not lost.Reproduction
Verified structurally against
3dbc71drather than by running an agent:is_running_on_polymarketis true.calculate_roi(:746) and_get_prediction_accuracy(:930) readsafe_contract_addressin EIP-55 checksummed form and pass it unmodified to their helpers.idexactly — the checksummed value matches no row, since ids are stored lowercase.traderAgentByIdreturnsnullandmarketParticipantsreturns[];prediction_accuracyandTotal ROIrender asNA; the log warns "Trader may be unstaked.";last_updatedfreezes at the last good round._fetch_trader_agent_performanceissues a comparable query for the same Safe in the same round and succeeds, because its caller (behaviours.py:1118) lowercases — which is whypredictions_madepopulates while the other two do not. Two nearly-identical calls disagreeing within one round is the fingerprint of this bug.Equivalent on Omen: not reproducible —
predict-omenis Graph-node-served and itsID!scalar normalises case, masking the identical defect.Root Cause
A missing normalisation invariant at the subgraph boundary, exposed by a runtime migration.
_fetch_from_subgraphforwardsvariablesverbatim by design, so responsibility for address casing rests with each_fetch_*helper or its caller. That responsibility was never centralised: some helpers normalise internally, others rely on their callers, and of the callers readingsafe_contract_addressinbehaviours.py, five lowercase and two — the two broken ones — do not. While every Polymarket-facing query was Graph-node-served, theID!scalar normalised case and the inconsistency had no observable effect. Moving the Polymarket queries onto Subsquid — whose generatedtraderAgentById(id: String!)andid_eqfilters are exact string matches — turned those two un-normalised sites from harmless into user-visible.The defect is therefore not the two missing
.lower()calls in isolation; it is that correctness depended on a convention no layer enforced. Two identicalid: Bytes!schemas with opposite case semantics across two runtimes is a migration hazard for anything else moved from The Graph to a squid. v3 fixes the two broken sites as instructed and does not attempt to enforce the invariant more broadly — see Section 10 item 3 for the residual risk that leaves.🤖 Generated with Claude Code