trufnetwork / trufnetwork/node
Goal: SDK call for market volume over a time period
@vinarmani is already working on this.
Since Sep 17, 2026.
- Dominant language
- Go
- Stars
- 7
- Forks
- 3
- Avg merge
- 3h 2m
- Merged PRs (30d)
- 13
Description
[!WARNING]
@vinarmani this Goal has no ETA. Add an estimated completion date before work begins.
See the Wizard docs for guidance.
@vinarmani
Framework only. The definitions and caveats are the parts worth holding. The
action signature and response shape are a starting point, not a decision.
Why this was created
The order book query surface covers the book, depth, and best prices, but not
activity. get_order_book, get_market_depth, and get_best_prices all answer
"what is resting right now". There is no action that answers "how much traded".
The data is already on the node. ob_order_events records every fill with
price, amount, outcome, participant, and block timestamp, so volume and unique
traders are computable without new state. What is missing is an action, and
therefore an SDK call.
Outcome
An SDK call returns volume and unique traders for a single order book over a
time period, in the same shape and at the same level as get_market_depth.
Current state
Nothing exists. The metrics are reachable today only by querying Postgres
directly, which requires node access and re-deriving the rule by hand each time.
Scope
Suggested action shape
Per bucket, matching get_market_depth. Callers that want a whole ladder
aggregate across its buckets, rather than the action taking a stream and a
settle time and baking the ladder structure into its signature.
CREATE OR REPLACE ACTION get_market_activity(
$query_id INT,
$from_ts INT8,
$to_ts INT8
) PUBLIC VIEW RETURNS TABLE(
bridge TEXT, -- units. volume is meaningless without it
volume_cents NUMERIC(78,0), -- the headline figure
direct_cents NUMERIC(78,0), -- counterparty trading
mint_burn_cents NUMERIC(78,0), -- boundary arbitrage
unique_traders INT,
fill_count INT, -- all fills, one row per economic event
direct_fill_count INT, -- subset: counterparty matches only
shares_traded INT8,
first_event_ts INT8, -- activity window actually observed
last_event_ts INT8,
coverage_from_block INT8, -- earliest retained event for this market
coverage_complete BOOL -- false if the market predates retention
)
PUBLIC VIEW, so read only, with no consensus or fee implications.
Why one object rather than a volume scalar
Every field above falls out of the same single pass over the event rows, so
returning them together costs nothing beyond the scan that volume already
requires. Splitting them into separate actions would multiply the scans.
Three of them earn their place for reasons worth stating:
bridgebecause volume is per collateral token and never summable across
tokens. A bare number invites exactly the addition the definition forbids.direct_centsandmint_burn_centsbecause the split is diagnostic. Two
markets with identical volume behave very differently if one is counterparty
trading and the other is a single participant working the mint and burn
boundary. They respond to different stimuli, and a consumer charting volume
will want to know which it is looking at.coverage_from_blockandcoverage_completebecause they turn the
retention problem below from a silent wrong answer into a visible one.
A count is worth having next to unique_traders. A market with seventy small
matches and one with five large ones report the same volume and the same trader
count, and are not the same market.
Count fills on the same allowlist that defines volume. If a mint or a burn
counts as volume, it counts as a fill, for the same reason: it is an economic
decision with collateral moving. Excluding it from the count while including it
in the value would be inconsistent.
De-duplicate it the same way as well. Both patterns write two rows per
economic event, direct_buy_fill alongside direct_sell_fill for a match and
a YES row alongside a NO row for a mint or burn, so one row per event is the
count in both cases. direct_fill_count is then a subset of fill_count, and
the pair mirrors the direct_cents and mint_burn_cents split.
Definition to hold fixed
Volume is filled orders, counted once per trade, in cents of that market's
own collateral token.
| Event | Volume | Counts as a trader |
|---|---|---|
direct_buy_fill |
price * amount |
yes |
direct_sell_fill |
no, it is the same trade | yes |
mint_fill, burn_fill |
100 * amount, YES row only |
yes |
split_placed |
no, it is a deposit | no |
placements, cancels, amends, settled |
no | no |
Three points that are easy to get wrong:
- Mint and burn are volume. They are two-party matches, not protocol
operations. Both sides agreed complementary prices, and the match settles by
creating or destroying a pair rather than transferring existing shares. They
are also the arbitrage boundaries that hold YES + NO near 100. split_placedis not volume. It is unilateral: collateral in, pair out,
no counterparty and no agreed price. It moves open interest.- The line is mechanically checkable. Every fill carries a
counterparty_idand nothing else does, so the allowlist can be validated
against the data rather than trusted. - Use an allowlist over event types, never a denylist. A denylist silently
admits any event type added later.
Volume is always per bridge and never summed across bridges. Base units
differ between collateral tokens by orders of magnitude, and this layer holds no
price feed to reconcile them. Unique traders, unlike volume, are comparable
across bridges.
Retention is the hard part
ob_order_events is ephemeral. trim_order_events() deletes rows once they
have been indexed, so the node holds a rolling window rather than full history.
A naive implementation returns 0 for a market whose events were trimmed, which
is indistinguishable from a market that genuinely had no activity. That silent
wrong answer is worse than a refusal.
Options worth weighing: return the coverage boundary alongside the figure so the
caller can tell truncated from empty, or ERROR when the requested range
precedes the earliest retained block. Either way the action should be explicit
that it answers for the retained window, not for all time.
Indexing
ob_order_events is indexed on id and block_height, but not on query_id.
A per-market aggregate is therefore a sequential scan today.
This is a public read that any caller can invoke and that every node executes,
so it should not be shipped without an index on (query_id, block_timestamp).
That index is cheap and helps the existing event queries as well.
Consistency with other implementations
The same metric is defined elsewhere in the stack. Wherever it is restated, the
implementations should be tested in lockstep against a shared fixture, meaning a
pinned market with a known figure, rather than each being retyped and trusted.
Out of scope
- Historical metrics beyond the retained window. That belongs to a service that
keeps its own copy. - Protocol wide totals across bridges.
- Charting endpoints and bucketed series.
Question
Is per bucket the right granularity, or should the action take a stream and a
settle time and aggregate the ladder itself? Per bucket matches the existing
query actions and keeps the signature stable if the ladder shape changes, at the
cost of callers making several calls.
Deadline
ETA: undefined
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.