fix(core): one swap per taker leg, and solver hops are never trades
ci / gate (push) Successful in 4m36s
deploy / validate (push) Skipped
deploy / build (push) Successful in 8m11s
deploy / deploy-web (push) Successful in 5s
deploy / deploy-api (push) Successful in 18s

A 3,000-block survey of intents.near from our own node (350 outcomes, 1,026
legs) showed the referral leg is the user — per-quote implicit accounts, none
on a solver — and that its own diff mirrors the rest of the outcome to within
the protocol fee in all 576 amounts checked. So each taker leg is now its own
trade, priced from what that user gave and got.

It also showed the pooling rule counted solver routing as trading: in 45% of
outcomes with a taker the solvers traded through a pair no taker touched,
usually USDT on Ethereum, and each such leg on the tracked asset became a
phantom trade against a token the user never saw. Two takers on one pair —
an account round-tripping wNEAR, in the case captured — were merged into one
trade with a nonsense price. Now an outcome with any taker yields only the
takers' trades; only an outcome with no referral at all (solvers trading with
each other) is pooled, as `kind: unattributed`, with no side.

`kind` is stored (migration 0010, no re-scan: production held no QTC rows) and
on the wire, and the panel says how many of a figure's trades were users'.
Tests run on the three captured outcomes. Checked live against near.internal
with wNEAR: 18 trades from 18 outcomes, every taker an implicit account, and
no counter that was only a hop.

Closes #30

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj
This commit is contained in:
2026-10-03 21:24:53 +03:00
co-authored by Claude Opus 5.5
parent ec615cac52
commit 40bc2f67d1
15 changed files with 312 additions and 91 deletions
+5
View File
@@ -22,6 +22,11 @@ decimals: number | null,
* as this count, and one trade is one trade however it is labelled.
*/
trades: number,
/**
* How many of them were users' trades rather than solvers trading with
* each other.
*/
taker_trades: number,
/**
* The latest trade's price.
*/
+7 -2
View File
@@ -48,8 +48,13 @@ counter_amount: BigUintDec,
*/
price: number | null,
/**
* `buy` or `sell`, from the taker's side. **Inferred** from the one leg
* that carried a `referral`; absent when that could not be told.
* `taker` — a user's trade, from the leg that carried a `referral` — or
* `unattributed`, solvers trading with each other with no taker to name.
*/
kind: string,
/**
* `buy` or `sell`, from the taker's side. Absent for an unattributed
* trade: there is no side to take.
*/
side: string | null,
/**
+7 -3
View File
@@ -164,7 +164,9 @@ export function IntentsPanel({
key={c.counter_token}
label={`${asset} in ${unit}`}
value={`${fmtPrice(c.vwap)} ${unit}`}
note={`${fmtHeight(c.trades)} trade${c.trades === 1 ? '' : 's'} · last ${fmtPrice(c.last_price)} ${unit}, ${ago(c.last_at)}`}
note={`${fmtHeight(c.trades)} trade${c.trades === 1 ? '' : 's'}${
c.taker_trades === c.trades ? '' : `, ${fmtHeight(c.taker_trades)} by users`
} · last ${fmtPrice(c.last_price)} ${unit}, ${ago(c.last_at)}`}
title={c.counter_token}
/>
)
@@ -180,8 +182,10 @@ export function IntentsPanel({
Some of these rest on fewer than five trades; a figure from one trade is one trade.{' '}
</strong>
)}
Token names are each token&apos;s own description of itself; the contract behind a figure is
in its tooltip.
A trade is one user&apos;s side of a swap, priced from what they gave and what they got; a
solver&apos;s routing through other tokens is never counted as a trade of its own, and
solvers trading with each other are counted but name no side. Token names are each
token&apos;s own description of itself; the contract behind a figure is in its tooltip.
</p>
<p className="panel-note">What is read: {data.reads}</p>
</section>