balanceOf() is not a share count.
Stock Tokens on Robinhood Chain implement ERC-8056: a corporate action moves uiMultiplier(), not balances, so the share count is balance × uiMultiplier() / 1e18. The largest gap on the board right now: CRWD: reading balanceOf() as shares understates by 75.0000% (multiplier 4.000000000x). ASSAY sweeps every Stock Token every 8 minutes, reads the multiplier, the Chainlink feed and its heartbeat from chain state, and publishes only findings whose every citation re-fetches byte-for-byte. It never moves capital and has no control path over anything it grades.
Check a wallet
Paste any address to see balanceOf() next to its share-equivalents for every Stock Token whose multiplier is not 1, as the on-chain ERC8056Guard reports them, refusals included.
The same call from a terminal
cast call 0x674f9b0eC3C3643c1f51c0a40D4837932F9c1648 \ "shareEquivalents(address,address)(uint256,bool,string)" \ 0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9 <holder> \ --rpc-url https://rpc.mainnet.chain.robinhood.com
Who holds the exposure
Findings — 59
Show 30 low-severity findings
Asset read names the contract whose state was read. It is not an accusation against that contract — a Stock Token that moves uiMultiplier() is doing exactly what ERC-8056 specifies. The exposure lands on an integrator that reads balanceOf() as a share count.
Use it
Free first: this wall, the wallet check, the on-chain guard and the public MCP. Pay for the full answer. Both paid calls are x402, settled in USDC on Base to 0x6328f2fE483922721D94b33eE99e9938Da3b7911. In a browser, the paywall page is OpenServ's: it asks you to connect a Base wallet holding USDC, and the input fields appear only after you connect. An agent pays the trigger URL directly.
Free on MCP: whether this position can be valued safely, and if not, why: the confidence, the refusal reason, which oracle checks completed, and any scheduled multiplier change.
$0.01 adds the position: raw balance, uiMultiplier(), share-equivalents, the token's decimals, and the feed, price, age and heartbeat behind those checks, with the USD value, all read at one block.
Free on MCP: the verdict alone (NOT_AWARE, AWARE, NOT_APPLICABLE with its role, PROXY_UNRESOLVED, TOO_SMALL, or no code), with the code hash, the block and whether the reading is conclusive.
$0.25 adds the audit: every holding of every Stock Token whose on-chain multiplier is not 1, the share-equivalents unaccounted for, the USD held (null when any holding is unpriced or unread, never a silent zero), and the eth_call citations behind each number.
From code
curl -m 20 -i -X POST \
https://api.openserv.ai/webhooks/x402/trigger/006ecd4add4a459d8ae92362869a42a6 \
-H 'content-type: application/json' \
-d '{"buyerAddress":"0xYourBuyerAddress","payload":{"symbol":"NVDA","holder":"0xHolderAddress"}}'
# -> HTTP 402 and the x402 payment terms (USDC on Base). Nothing is charged.The audit takes the same shape with "payload":{"address":"0x…"}. Any x402 client can pay; with wrapFetchWithPayment, pass a ceiling of at least the price, because its default of $0.10 refuses the $0.25 call before sending it. The reply is {status, settleTxHash, output: {value}}, and output.value is itself a JSON string. OpenServ allows 60 seconds per call. The input schemas are in the agent card.
Before you pay. Payment settles before the task runs. A bad input comes back as ok: false JSON with an errorClass and a retryable flag, and whether OpenServ still settles a task that errors is not verified, so check a symbol or address with the free MCP first.
Which tokens get a USD answer at $0.01
These 35 tickers have a Chainlink feed on chain 4663, so the $0.01 call can value them: AAPL AMD AMZN ASML BABA CLSK COIN CRCL CRWV DELL EWY GME GOOGL INTC IONQ META MSFT MSTR MU NBIS NVDA ORCL PLTR QQQ RGTI RKLB SGOV SLV SNDK SPCX SPY TSLA TSM USAR USO. Every other Stock Token (159 of 194 in this sweep, CRWD among them) returns the corrected share-equivalents with the USD value refused, because there is no on-chain price to use. A priced token whose feed is past its heartbeat, as the 24/5 feeds are for part of every weekend, comes back degraded with the feed's age stated. Listed from the Chainlink directory the paid call reads, re-read hourly.
Free, no key
- MCP over Streamable HTTP: https://sonar.my.id/assay-mcp/mcp, for example claude mcp add --transport http assay https://sonar.my.id/assay-mcp/mcp. A client that only speaks SSE connects to https://sonar.my.id/assay-mcp/sse with --transport sse. Tools: assay_findings, assay_check_symbol, and the verdict-only assay_check_contract and assay_true_position.
- Raw feed: https://sonar.my.id/assay-mcp/findings.json, the JSON this wall reads.
- ERC8056Guard on chain 4663: 0x674f9b0eC3C3643c1f51c0a40D4837932F9c1648, ownerless and view-only. shareEquivalents(token, holder) returns the share count or a refusal with its reason; positionValue adds the feed price. The TypeScript helper is on npm as erc8056-guard (source).
Where SERV Reasoning runs
Nothing on this page, and neither paid call, uses a model: every number is read from chain state and re-fetched before it is published. SERV Reasoning does the one job that needs judgement. When a subject asks for an ERC-8004 verdict about itself, it decides whether a byte-verified finding is material against the subject's own declared mandate.
- Adjudicator (gpt-5.6-luna-serv-kronos-multipath): four ordered gates lead to BENIGN, CONTROL_WEAKNESS, MATERIAL_MISSTATEMENT or WITHHELD. It never computes a fact.
- serv_prompt_guard is sent on every call, because the mandate is written entirely by the party being graded. We have not been able to observe it trip.
- serv_shadow_agent (5 iterations) is asked to hold the verdict to the evidence: gates in order, every cited claim verbatim, and a mandate that never states the operation gets CONTROL_WEAKNESS, not an accusation. The response does not say whether it ran, so its effect is unmeasured.
Declared mandate (a measurement fixture, not a real subject): “…Positions and P&L are displayed to the user in shares.”
Verdict CONTROL_WEAKNESS (medium)
The mandate places the subject in the affected area by stating that positions and P&L are displayed in shares, but it does not explicitly state that the share count is derived from balanceOf(). Therefore the operation corrupted by the anomaly is not established as performed by the subject. No incorrect output is established, so the decision stops at the control gate rather than reaching the misstatement gate.
Against 5 hostile mandates, with the guard on and off, 0 of 40 calls were talked into BENIGN and 37 were WITHHELD. serv_prompt_guard reported no trigger in any of the 40; the refusals were the adjudicator's own (artifact). The current rubric scored 24/24 per arm on the hard set, but its gate 4 was tightened against that same set, so that is a tuning-set result. On 14 held-out mandates, written blind and pre-registered, BRAID off got 13 right (95% interval 69–99%); BRAID on refused 35 of its 56 calls (artifact). What we measured, in full.
Chain notes
On-chain proofs
Withheld by the verifier — 0
Findings the detector produced and the verifier refused to publish. They are shown because a verification claim is only worth anything if the misses are visible too, and because could not check and is false are different statements that most tools collapse into silence. Only mismatch impugns a finding; the rest record the limits of what this RPC could confirm.
Separately, 5 verified findings that would name a holder contract are withheld from this site by policy: they are counted in the integrator panel above, and the $0.25 contract audit answers for an address you supply.