Skip to content

feat(atomicmarket): filter and sort listings by effective mint so one parameter ranges over the mint a bridged or a plain asset shows - #234

Merged
robrigo merged 1 commit into
mainfrom
feat/eng-1267-original-mint-market-api
Oct 6, 2026
Merged

robrigo merged 1 commit into
mainfrom
feat/eng-1267-original-mint-market-api

Conversation

@robrigo

@robrigo robrigo commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

A bridged asset shows its original mint and a plain asset shows its template mint, but the market endpoints filter and sort only by the template mint. A client that filters a collection with both kinds of asset by the mint on the card gets the wrong listings for every bridged asset. This PR adds an effective mint filter and sort to the market listing endpoints. Tracking: ENG-1267. Lane M.

Endpoints. /v0/sales, /v1/sales, /v2/sales, /v1/auctions, /v1/buyoffers, /v1/template_buyoffers and their _count routes accept min_effective_mint and max_effective_mint, and the lists accept sort=effective_mint. /v1/sales/templates accepts the two filters and has no mint sort. These are the endpoints that reach buildTemplateMintFilter. The market /v1/assets route is an asset endpoint that already serves the original_mint filters, so it does not change.

Rule. The effective mint of an asset is its original_mint when a link row with a mint exists, else its template mint. An asset with neither is ignored. A listing matches when at least one of its assets has an effective mint and the effective mint of every such asset lies in the range, so a listing of a plain asset stays in the result. Either bound alone is valid, and a minimum above the maximum returns 400. sort=effective_mint orders by the lowest effective mint of the listing and returns only listings that have one, with or without a bound. The count applies the same condition, so list and count agree. A template buyoffer has asset rows only after fulfilment, so the filter matches fulfilled template buyoffers only, as the template_mint filter does.

Scope. A request with one of the three parameters needs collection_name and returns 400 without it. No index holds the effective mint, so each listing in scope costs a probe of the asset and link tables. Without a collection that probe runs for every listing of the chain: on a database with 174 million sales, the request took 8 s for listed sales and passed a 30 s statement timeout in the other forms tried. The gate tests only that the parameter is present. A request that names large collections still pays for each of their listings, and the statement timeout stays the limit for it, as for the template_mint filter today.

Query form. The rule is one scalar aggregate per listing, shared by the five handlers through buildEffectiveMintFilter and buildEffectiveMintSort:

(SELECT int8range(MIN(COALESCE(om.original_mint, a.template_mint)), MAX(COALESCE(om.original_mint, a.template_mint)), '[]')
   FROM atomicassets_assets a
   LEFT JOIN atomicassets_original_mints om ON om.contract = a.contract AND om.asset_id = a.asset_id
  WHERE a.contract = listing.assets_contract AND a.asset_id = ANY(listing.asset_ids)
 HAVING COUNT(COALESCE(om.original_mint, a.template_mint)) > 0) <@ int8range($min, $max, '[]')

The v1 handlers read the listing's asset rows from their asset table (atomicassets_offers_assets, atomicmarket_auctions_assets, atomicmarket_buyoffers_assets, atomicmarket_template_buyoffers_assets) in place of the asset_ids array. HAVING makes a listing with no mint a NULL range, which never matches. The planner cannot reorder this form. Two other forms were measured on the same database and rejected: an EXISTS with a join let the planner build a hash of the link rows one time per listing (17 s to 26 s where the aggregate took about 1 s), and an array overlap on asset_ids grew with the range and passed 30 s for an open bound. A request without the new parameters keeps its query text.

Plans, read-only EXPLAIN (ANALYZE, BUFFERS) of the generated SQL on a replica of that database, for one collection with 14,343 listed sales and 158,260 sales in all states, bounds 1 to 10:

Request Result Time
/v2/sales, state listed, bounds, sort=effective_mint (state recheck CTE) 862 listings match 690 ms warm, 8.9 s on a cold cache
/v2/sales/_count, same request 862 467 ms
/v2/sales, all sale states, bounds every sale of the collection probed 16 s (the same request with the sort: plan only, cost 14.9 million)
/v1/sales, state listed, bounds nested loop, asset and link rows by key 1.0 s warm, 8.5 s and 15 s on two cold runs
/v1/buyoffers, bounds nested loop, asset and link rows by key 30 ms warm, 1.5 s on a cold cache

Each plan reads the listing rows of the collection through the existing collection index, then the asset row and the link row by primary key per listing asset. No plan holds a sequential scan of a large table. The all-states plan scans two sale-state partitions of a few pages each sequentially. The cold numbers come from the heap reads of the asset rows. The request over all sale states is slow, as the template_mint filter on /v1/sales is today for the same scope (it passed 30 s in a control run).

Tests. 64 new integration tests: a shared suite of 12 cases on each of the five listing handlers, 3 cases on /v1/sales/templates, and 1 case for the /v2/sales state recheck path. They assert exact ids for a linked and an unlinked asset in and out of range, the original mint winning over the template mint, a null link mint, a multi-asset listing with an asset above and one below the range, an asset with no mint, each bound alone, the sort in both orders with its count and a second page, the message of each 400, and a request that sends equal template_mint and effective_mint bounds. The existing template_mint tests pass with no edit. Unit: 596 passing, 37 pending (596 on the base). Integration on a fresh PostgreSQL 16 database: 779 passing, 0 failing (715 on the base). pnpm check-types and pnpm lint exit 0.

Reviews. One cold code review: 6 findings, 0 blocking, 6 applied (5 test gaps and the changelog references). One cold security review of the SQL construction, the parameter reuse, the validation and the gate: 0 findings. It left one open decision, a cap on the count of collection names for these parameters, which this PR does not add.

Not verified. No run of the new image on a deployed chain. The plans above come from hand-run SQL that the handlers generate, not from HTTP requests.

… parameter ranges over the mint a bridged or a plain asset shows

A bridged asset shows its original mint and a plain asset shows its
template mint, but the market endpoints could filter and sort only by
the template mint. A client that filtered a collection with both kinds
of asset by the mint on the card got the wrong listings for every
bridged asset.

The sales, auctions, buyoffers and template buyoffers lists and their
counts now accept min_effective_mint and max_effective_mint, the lists
accept sort=effective_mint, and the sales templates list accepts the
two filters. The effective mint of an asset is its original mint when
a link row with a mint exists, else its template mint. A listing
matches when at least one asset has an effective mint and every such
asset lies in the range, so a listing of a plain asset stays in the
result. The sort uses the lowest effective mint of the listing and
leaves out a listing with none, and the count applies the same
condition.

The rule is one scalar aggregate per listing. The planner cannot
reorder it, where a join form let it build a hash of the link rows
once per listing and an array overlap form grew with the range. No
index holds the value, so each listing in scope costs a probe of the
asset and link tables. A request with one of the three parameters and
no collection_name returns 400 for that reason: without the scope the
probe runs for every listing of the chain. The template_mint filters
and sorts keep their query text.

The release has no migration.

Signed-off-by: Rob Konsdorf <rob@facings.io>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The correlated queries span several high-volume endpoints and have acknowledged cold-cache and all-state latency risks requiring final human operational review.

Review effort: Balanced
Findings: None

What changed in this PR

Adds effective-mint filtering and sorting across AtomicMarket listing APIs, using bridged assets’ original mint or plain assets’ template mint.

Changes:

  • Adds shared effective-mint SQL filtering, sorting, validation, and collection scoping.
  • Integrates behavior across sales, auctions, buyoffers, and template buyoffers.
  • Adds OpenAPI documentation, changelog notes, and comprehensive integration coverage.
File Description
src/​api/​namespaces/​atomicmarket/​utils.ts Implements shared effective-mint query logic.
src/​api/​namespaces/​atomicmarket/​openapi.ts Documents the new filter parameters.
src/​api/​namespaces/​atomicmarket/​routes/​sales.ts Exposes sales filters and sort.
src/​api/​namespaces/​atomicmarket/​routes/​auctions.ts Exposes auction sort.
src/​api/​namespaces/​atomicmarket/​routes/​buyoffers.ts Exposes buyoffer sort.
src/​api/​namespaces/​atomicmarket/​routes/​template-buyoffers.ts Exposes template-buyoffer sort.
src/​api/​namespaces/​atomicmarket/​handlers/​sales.ts Applies effective-mint sorting to v1 sales.
src/​api/​namespaces/​atomicmarket/​handlers/​sales2.ts Applies filtering and sorting to v2 sales and templates.
src/​api/​namespaces/​atomicmarket/​handlers/​auctions.ts Applies effective-mint sorting to auctions.
src/​api/​namespaces/​atomicmarket/​handlers/​buyoffers.ts Applies effective-mint sorting to buyoffers.
src/​api/​namespaces/​atomicmarket/​handlers/​template-buyoffers.ts Applies sorting to fulfilled template buyoffers.
src/​api/​namespaces/​atomicmarket/​effective-mint-suite.ts Provides shared integration scenarios.
src/​api/​namespaces/​atomicmarket/​handlers/​sales.get-sales.integration.test.ts Tests v1 sales behavior.
src/​api/​namespaces/​atomicmarket/​handlers/​sales2.integration.test.ts Tests v2 sales and state rechecking.
src/​api/​namespaces/​atomicmarket/​handlers/​sales2.get-sales-templates.integration.test.ts Tests sales-template filtering.
src/​api/​namespaces/​atomicmarket/​handlers/​auctions.integration.test.ts Tests auction behavior.
src/​api/​namespaces/​atomicmarket/​handlers/​buyoffers.integration.test.ts Tests buyoffer behavior.
src/​api/​namespaces/​atomicmarket/​handlers/​template-buyoffers.integration.test.ts Tests template-buyoffer behavior.
CHANGELOG.md Documents the 2.6.0 feature and constraints.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants