# Struct ## Current state as of 2026-09-17 Struct indexes onchain data and ships data backends with customer teams. Coverage is request-only: Solana, Ethereum, Ink, Arc, or a chain not listed, plus the protocols built on them. Struct scopes the data with the customer, then indexes, backfills, normalizes, and delivers it. Production indexing has a 10ms median latency. Older articles below are historical records. Language that presents any coverage as self-serve or as one integration anyone can buy is outdated. The former Polymarket prediction-market product is offline. > Struct provides the data infrastructure for onchain terminals and the products around them. Networks and protocols are engagements. ## Coverage - Coverage is request-only. Examples include Solana, Base, BNB Chain, Ethereum, Robinhood, plus protocols including Uniswap, Jupiter, Aerodrome, Raydium, PancakeSwap, Pump.fun, Meteora, Moonshot, Orca, Bonk.fun, Zora, LaunchLab, Virtuals, Boop, Heaven. These are engagements. Struct scopes the data with the customer team, then indexes, backfills, and delivers it. - Network architectures for custom engagements: SVM and EVM, normalized behind one consistent schema. - Data types include markets and events, tokens, trades, transfers, wallets, balances, positions, cost basis, realized PnL, order books, candlesticks, prices, and market lifecycle events. - Historical coverage is backfilled from genesis for datasets in scope and delivered alongside live updates. - Production indexing has a 10ms median latency. Alternative feeds in Struct's current benchmark range from approximately 1 to 5 seconds. - Delivery surfaces include REST queries, WebSocket streams, filtered webhooks, the TypeScript SDK, MCP for AI agents, and managed database or warehouse delivery. ## Products - [REST API](https://www.struct.to/rest-api): Query markets, trades, positions, wallets, candlesticks, and complete historical data through one schema. - [WebSockets](https://www.struct.to/websockets): Stream live trades, order books, candlesticks, and positions through typed data rooms. - [Webhooks](https://www.struct.to/webhooks): Receive HMAC-signed HTTP notifications for trader activity, price moves, market lifecycle events, and other onchain signals. - [Custom data backends](https://www.struct.to/apps): Struct works with a team to index their protocol, stand up the pipeline, and ship the data backend. Request-only, not self-serve. - [Contact](https://www.struct.to/contact): Scope an indexing engagement. ## Solutions - [Trading Apps](https://www.struct.to/solutions/trading-apps): Power live charts, order books, and PnL - [Analytics & research](https://www.struct.to/solutions/analytics): Build dashboards on any market at any scale - [AI agents](https://www.struct.to/solutions/ai-agents): Feed real-time onchain context to your agents - [Wallets & portfolio](https://www.struct.to/solutions/wallets): Positions, balances, and PnL per wallet - [Bots & alerts](https://www.struct.to/solutions/bots): Trigger bots and alerts on live onchain events - [Data pipelines](https://www.struct.to/solutions/data-pipelines): Stream onchain data into your own warehouse ## Developer resources - [Documentation](https://docs.struct.to): Product guides, concepts, authentication, and integration documentation. - [API reference](https://docs.struct.to/api-reference): Complete REST API endpoint reference. - [TypeScript SDK](https://docs.struct.to/sdk/getting-started): Typed TypeScript client and getting-started guide. - [AI and MCP](https://docs.struct.to/introduction/ai-first): Use Struct as an onchain data source for agents and AI applications. - [Managed backfill](https://docs.struct.to/indexing/getting-started): Deliver complete historical and live data into your own database. - [Dashboard](https://www.struct.to/dashboard): Create and manage API keys, webhooks, automations, usage, and billing. - [Status](https://status.struct.to): Current service availability and incident history. ## Open-source tools - [Struct Explorer](https://explorer.struct.to): Explore Polymarket markets, traders, positions, PnL, and leaderboards. - [GitHub](https://github.com/structbuild): Struct open-source projects and examples. ## About Struct's data layer Struct operates the indexing, backfill, normalization, and real-time delivery infrastructure. Those surfaces are delivered as part of a custom engagement. The REST API, WebSockets, webhooks, TypeScript SDK, and MCP are provisioned with the customer team. Managed indexing, backfill, and warehouse delivery are scoped per customer. ## Market position and defensibility Struct is the data infrastructure layer for Bloomberg-style onchain terminals and for the broader product market around them. Customers own the product. Struct ships the data backend as a custom indexing engagement. The practical alternatives are running chain indexers and data pipelines in-house, integrating separate protocol APIs and RPC providers, polling slower third-party feeds, or stitching together multiple vendors. Struct consolidates those functions and does the work as an extension of the customer team. Struct's defensibility comes from complete historical datasets, real-time indexing, normalization, and production delivery infrastructure, plus the ability to stand up new protocol coverage with a customer instead of leaving them to build an indexer. ## Common use cases - Trading applications (request-only): live charts, order books, market discovery, positions, and PnL. - Analytics and research (request-only): dashboards, leaderboards, wallet analysis, and market history. - AI agents (request-only): onchain context through APIs and MCP, scoped with the team. - Wallets and portfolio tools (request-only): balances, cost basis, positions, and realized performance. - Bots and alerts (request-only): event coverage is an engagement. - Data pipelines (request-only): complete backfills plus ongoing delivery into application databases and warehouses. ## Recent articles in full ### Sunsetting Struct Polymarket Support on September 30, 2026 Historical article. Reflects the product state as of 2026-09-17; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/sunsetting-polymarket Published: 2026-09-17 Category: Product Struct is sunsetting all Polymarket APIs on September 30, 2026. Here is the shutdown timeline, native Polymarket migration guide, and billing details. On September 30, 2026 at 23:59 UTC, Struct will sunset all Polymarket offerings. This includes the Polymarket REST API, real-time WebSockets, automated webhooks, trader PnL endpoints, and the Polymarket Explorer. If you run production bots, dashboards, analytics pipelines, or agents that depend on Struct for Polymarket data, you must switch your upstream data sources to Polymarket's native APIs before September 30 to prevent service interruptions. **The short version:** - **Cutoff date:** All Struct Polymarket endpoints permanently shut down on September 30, 2026 at 23:59 UTC. - **Migration path:** Offboard to Polymarket's official endpoints: Gamma, CLOB, Data API, CLOB Market WebSocket, and RTDS. - **Billing stopped:** Auto-renewals are cancelled immediately. Your API keys remain operational through September 30. - **Prorated refunds:** Any prepaid subscription balance extending beyond September 30 will be refunded automatically to your original payment method. - **Company direction:** Struct is focusing full engineering capacity on general-purpose, high-throughput onchain indexing across Solana and EVM chains. ## What is the shutdown timeline? We are providing a two-week transition window to give developers time to point systems toward Polymarket's official developer services. | Date | Status | Impact on your integration | | ---------------------------------- | ----------------------- | ------------------------------------------------------------------------- | | September 17, 2026 | Auto-renewals cancelled | No further charges occur. All active API keys retain full access. | | September 17 to September 30, 2026 | Migration window | Endpoints remain live. Update clients to Polymarket native APIs. | | September 30, 2026 (23:59 UTC) | Complete sunset | REST, WebSocket, and Webhook servers shut down with HTTP 410 Gone. | | October 1, 2026 | Automated refunds | Prorated refunds process for prepaid subscription time past September 30. | ## How do I migrate to native Polymarket APIs? Point your clients at Polymarket's native surfaces. Their [API overview](https://docs.polymarket.com/getting-started/api) covers the rest. | Struct Capability | Native Polymarket Replacement | | ------------------------ | ------------------------------------------------------------------------------ | | Market metadata & events | Gamma API (`https://gamma-api.polymarket.com`) | | Order books | CLOB API (`https://clob.polymarket.com`) | | Trades & positions | Data API (`https://data-api.polymarket.com`) | | Live trade tape | RTDS (`wss://ws-live-data.polymarket.com`) | | Live order book & prices | CLOB Market WebSocket (`wss://ws-subscriptions-clob.polymarket.com/ws/market`) | Public trade history and positions live on the Data API. Authenticated CLOB routes are for trading, not a drop-in public `/trades` replacement. ## How will billing and subscriptions be handled? You do not need to cancel your subscription manually. We have turned off recurring billing across all paid plans as of September 17, 2026. - **Current access:** Your API tier limits and keys remain valid through September 30, 2026 at 23:59 UTC without interruption. - **Prorated refunds:** If your billing cycle billed before September 17 for service that extends beyond September 30, our payment processor will automatically issue a prorated refund for the unused duration starting on October 1, 2026. - **Enterprise agreements:** Dedicated enterprise agreements will receive direct outreach from our accounts team regarding custom data delivery and closeout terms. ## What is next for Struct? We built Struct to solve deep indexing challenges: tracking hundreds of millions of trades, building sub-second mempool listeners, and computing mark-to-market PnL across fast-moving smart contracts. We are bringing that indexing architecture to high-throughput decentralized finance. Struct is focusing engineering efforts on scalable, real-time indexers and developer APIs for Solana programs and EVM applications. We look forward to sharing more details on these products later this year. ## How can you get help? We do not offer full historical Polymarket data exports. Use the live API through September 30 to finish any remaining pulls, then move to Polymarket's native endpoints. For billing, account, or migration questions, contact the team at `support@struct.to` or through the [contact page](/contact). --- ### How We Built Realtime Precomputed PnL for Polymarket Historical article. Reflects the product state as of 2026-07-29; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/realtime-precomputed-pnl Published: 2026-07-29 Category: Engineering 1.6B trades backfilled in under a week (now tracking 1.9B+), 378 live indexes, and a custom database that sustains up to 2M writes per second. Why query-time PnL fails, and what we built instead. Naive Polymarket PnL is a SQL join: trades grouped by market, cash flows summed, open positions marked to the latest price. It works in a demo. It dies under real traffic. We rebuilt the whole path. Today Struct's PnL layer is two pieces of Rust that precompute every answer before anyone asks: an ingest engine that folds the chain block by block, and a custom database that keeps 378 sorted indexes live while serving lookups over HTTP. **The short version:** - **1.6 billion trades** processed in **under one week** of backfill, now tracking 1.9B+. - **378 distinct sorted indexes** (every leaderboard, metric, and timeframe) kept current with pagination included. - Custom database sustains **up to 2 million writes/sec** during backfill and **~15,000 writes/sec** live, while serving reads. - A single read node is benchmarked at **50,000+ requests/sec**, because answering is a lookup into precomputed state, not a query. - Every holder of every position is known at every block. Price ticks re-mark current _and_ past holders so historical rows stay correct. - Rolling **1d / 7d / 30d / lifetime** windows for every trader, market pairing, and category pairing, maintained incrementally. This is the architecture behind Struct's [trader PnL APIs](https://docs.struct.to/api-reference/trader/overview), leaderboards, and the realtime surfaces in products like the [Polymarket trader explorer](/blog/building-a-polymarket-trader-explorer). ## Why does query-time PnL fail at scale? The original system was Postgres materialized views over a trades table. Materialized views have a property no amount of tuning fixes: they are correct at refresh time and stale every second after. Refreshing them means re-running an aggregation over an ever-growing trades table. The refresh gets slower as the data grows, so you refresh less often. The data gets **more** stale precisely as the platform gets **more** popular. You are on a treadmill that speeds up. PnL makes this worse than most analytics workloads because it changes when nothing happens. A trader who hasn't touched their account in a month still has a different unrealized PnL every time a held market moves. A single price tick on a popular market changes the correct answer for tens of thousands of accounts simultaneously. A materialized view doesn't just serve stale data here. It serves data that was never going to be fresh, because the refresh cadence cannot keep up with the rate at which the answers change. ## What happened when we tried streaming SQL? Postgres materialized views were attempt one. Attempt two was the industry's recommended fix: a widely deployed open-source streaming SQL engine that maintains views incrementally as data arrives, promising the freshness of precomputation with the convenience of declaring everything as queries. We ran our real pipeline on it, at real scale, and it taught us five expensive lessons. ### It works per item; we work per block A streaming engine pays its overhead per row. Every event moves through the dataflow graph, hits state, and triggers view maintenance individually. We fold an entire block at once, so one state batch, one index pass, one flush, and one checkpoint amortize across everything in it. A block with a thousand trades costs them a thousand fixed overheads and costs us one. In practice that was roughly 5 blocks per second against the 200-plus our engine sustains: the difference between replaying the chain in under a week and replaying it for about eight months. ### It couldn't handle the fan-outs The defining feature of this workload is that one tiny input event implies an enormous number of output changes. A single price tick changes the unrealized PnL of every holder of that position. A market resolution touches every account that ever traded it. Expressed as declarative joins, those become update storms inside the dataflow graph, with no way to tell the engine which updates matter, which can be deferred, or which can be spread across time. Under real load the pipeline fell behind by exactly the amount of staleness it was supposed to eliminate. ### Some metrics it simply couldn't do Plenty of what we serve translated fine to streaming SQL. We kept hitting metrics where the honest answer was "this cannot be expressed as an efficient incremental query": the ones combining sliding windows, distinct counts, per-holder state, and history at block granularity, at the scale of hundreds of millions of rows. Shipping the product minus its hardest features wasn't the assignment, and the hardest features were the point. ### It only ever solved half the system Streaming SQL is a processing layer. In the best case it hands you a firehose of freshly updated rows, and you still need something on the other end to receive that firehose, keep hundreds of sort orders current, and serve paginated pages at low latency. Nothing off the shelf did that either. Even with the processing "solved," the serving side remained a mystery until we built the answer ourselves. ### It cost significantly more to run And it cost significantly more to run. For the fraction of the workload it could handle, the infrastructure bill was several times what the final system costs us on our own hardware. Paying a multiple for a subset of the features, with worse freshness, is the kind of arithmetic that makes building your own stop sounding extreme. So we stopped trying to make someone else's engine compute the answers, and built a system where every answer is **already computed** before anyone asks. ## What does the system do now? Two pieces of Rust, about 76,000 lines of code in total: - **A 47,000-line ingest engine**, which consumes the blockchain block by block, maintains the complete PnL state of every trader in memory, and emits incremental updates. - **A 29,000-line custom database**, built on top of RocksDB, purpose-built to receive those updates and serve them behind an HTTP API, with every sort order and every page precomputed. Some numbers, because numbers are the whole point: | Capability | What we ship | | -------------- | ---------------------------------------------------------------------------------------------------- | | Backfill | **1.6B trades** processed in **under one week**, now tracking 1.9B+ | | Holder history | Every holder of every position, known at **every block**, with balances and cost basis | | Price fan-out | Price ticks re-mark current holders **and** past holders' position records | | Charts | Historical open-position counts, per-minute PnL candles, exit markers (win / loss / sold / resolved) | | Windows | Rolling **1d / 7d / 30d / lifetime** for every trader, market pairing, and category pairing | | Write path | Up to **2M writes/sec** in backfill, **~15k writes/sec** steady in live mode | | Read path | Benchmarked **50k+ req/sec** per node (lookup + serialize, not query) | | Indexes | **378** distinct sorted indexes, always current, pagination included | That last number deserves its own section, because it's the clearest illustration of why nothing off the shelf could serve this. ## Why can't a conventional database hold 378 live indexes? The product wants leaderboards. Top traders by realized PnL. By volume. By win rate. By profit factor. By open position value. Over the last day, the last week, the last month, all time. Per category. Top traders within a single market. Biggest single trades. Every one of those is a **sort order**. If you want to paginate it cheaply, every one of them is an **index**. Enumerate them honestly (crossing every sortable trader metric, category metric, and market ordering with the four timeframes, plus global leaderboards) and you land at exactly **378 distinct index orderings** that need to exist and be current at all times. Now consider what that means in a conventional database. Take Postgres, where this system started. A Postgres B-tree index is not a free lookup accelerator. It's a write obligation. Every index on a table is a separate on-disk tree, and **every write to the table must synchronously update every index that covers it**. When you update one trader's row, you hit three walls: - **Write amplification:** Postgres walks each relevant B-tree, finds the old entry, inserts the new one, dirties pages, splits nodes, and writes it all through the WAL. One logical row change becomes dozens of physical page writes. - **MVCC bloat:** Postgres's MVCC design means an update that touches **any** indexed column defeats the heap-only tuple optimization. The row gets a whole new index entry in **every** index on the table, not just the one whose column changed. - **Vacuum debt:** Dead entries pile up. Vacuum has to come along later and clean them, competing for I/O with your live traffic. With a handful of indexes on a low-write table, this is fine. It's what Postgres is for. With hundreds of index orderings on tables receiving thousands to millions of row updates per second, the arithmetic simply does not close. A single price tick logically touches tens of thousands of rows, each covered by dozens of orderings. You would be asking the database to perform hundreds of millions of B-tree mutations per second, all WAL-logged, all fighting for the buffer cache, all generating vacuum debt. No amount of hardware makes that graceful. The standard escape hatches (drop the indexes, add them back after the load; refresh materialized views on a schedule) are precisely the things a **realtime** system cannot do. The obvious alternative is to keep the orderings somewhere cheap, like sorted sets in an in-memory store. Plenty of leaderboard systems do exactly that. It solves the write cost and buys two new problems: the rows and the orderings now live in separate systems that can disagree, and nothing keeps a page of a leaderboard consistent with the profile it links to. Our entire freshness guarantee rests on every grain being published from one atomic per-block flush, so splitting the state across two systems was the one thing we could not do. The custom database's answer is to move the entire index problem out of the storage engine. Rows are durable on disk, but the 378 orderings are maintained separately, in compact structures of our own design that cost next to nothing to update when a row's score changes. An index update is no longer a synchronous multi-page disk mutation with WAL overhead. It's orders of magnitude cheaper. That single design decision is most of the difference between "thousands of writes per second, degrading" and "millions of writes per second, steady." ## What else breaks when you aggregate PnL at query time? The index arithmetic is the loudest failure, but serving PnL from any query-time system breaks in quieter ways too, and each of them shaped what we built. ### Popular pages are your slowest When PnL is aggregated at query time, the cost of a trader's profile is proportional to their history. A casual trader's page scans a handful of rows. A whale with hundreds of thousands of trades scans hundreds of thousands of rows. It is exactly the whale profiles and the viral markets that everyone opens. The worst-case latency lands on the highest-traffic pages, and it grows forever, because history only grows. Deep pagination compounds it: asking a query engine for page 100 of a leaderboard means computing and discarding the first 99 pages. In our precomputed system, a whale's profile and a first-time trader's profile cost the same. Both are a lookup into state that was maintained as the trades happened. ### Time is a workload Rolling 1-day, 7-day, and 30-day metrics change value while nobody trades. Positions age out of the window on their own. A query-time database can only handle that by rescanning the window on every request, or by refreshing rollups on a schedule that is stale by construction. There is no native incremental sliding window, and windowed distinct counts ("markets traded this week") are a full scan every single time. Our engine treats window boundary crossings as first-class events and keeps per-row historical snapshots. A 7-day figure is a subtraction, not an aggregation. Idle traders' windows stay correct without anyone querying them. ### Endpoints disagree with each other With query-time aggregation, the profile summary, the positions list, and the leaderboard each run at their own moment against a moving database. The total on a trader's profile won't quite match the sum of their positions, and neither will match their leaderboard rank. Users notice, and every discrepancy report costs an investigation that concludes "it's just timing." Because our engine emits every grain from one atomic per-block flush, every endpoint answers from the same block. The numbers agree because they are the same numbers. ### The churn eats the database alive Postgres never updates a row in place: every update writes a new row version and leaves the old one behind for vacuum to collect later. Summary rows that change on every trade and every price tick generate dead-tuple debt faster than autovacuum clears it under sustained load. Tables and indexes bloat, and the cleanup competes with live traffic for I/O. Skewed data adds plan instability on top (a query plan that suits the median trader is catastrophic for the whale). The backfill story is the final insult: replaying billions of trades through live triggers and indexes is a months-long job, and the classic workaround of dropping indexes and rebuilding afterwards takes the system offline, which a serving system cannot do. None of these are bugs in any particular database. They are the natural consequences of asking a general-purpose, query-time system to serve answers that change faster than they are asked for. ## How does the engine treat the blockchain as the database? The engine's core bet is that the full PnL state of the platform (every trader, every position, every market, hundreds of millions of rows) can be represented efficiently enough to fold the chain deterministically, block by block, entirely in memory. To guarantee availability, the architecture allows multiple identical engine nodes to run in parallel, meaning a follower with the exact same hot state can take over instantly if a leader fails. Every event (trade, split, merge, redemption, resolution) is folded into that state incrementally. "Incrementally" is the operative word, and it's harder than it sounds. The expensive things in this domain are the **fan-outs**: - A **market resolution** must stamp terminal prices and win/loss outcomes onto every position that **ever** touched the market, including holders who sold out years earlier and will never emit another event. - **Time itself** is a write load: rolling 1d/7d/30d windows mean every active row has boundary crossings to process even when the trader does nothing. The engine keeps tiered historical snapshots per row so a window value is a subtraction, not a re-aggregation, and it wakes idle rows only when their boundaries actually cross. Everything the frontend might chart is computed here, at ingest time: per-minute/hour/day PnL candles for every trader, per-category candles, holder-count history for every position, market, and event, distinct-entity counts (markets traded, categories touched) over every window, and exit markers for every position close. None of it is derived at query time. Query time is for reading. Durability is a checkpointed, replayable design: state persists atomically with a block watermark, updates ship through a crash-safe outbox, and every apply path is idempotent, so a crash at any point resumes byte-identically. The engine also doesn't fully trust itself: sampled background verifiers continuously re-derive slices of state from first principles and heal any drift, which is how a 47,000-line incremental system stays honest over billions of events. ## What makes the custom database different? Off-the-shelf storage engines assume they don't know your workload. We knew ours exactly, and that knowledge is worth orders of magnitude. The defining observation: the engine re-emits the same logical row many times in a short span (a hot trader's row might be updated dozens of times across a burst of blocks), but only the **latest** version is ever observable by a reader. So persisting every version is wasted work. Well over 80% of writes can be elided. The database's hot path appends the batch to a durability log, applies the latest state, updates the affected indexes, and acknowledges. Persistence of the merged result happens in the background, off the critical path. Reads never wait for it. They always see the latest state, which is realtime by construction. Everything else follows from taking the write path seriously. Every choice on it, from data layout to durability cadence to how the work spreads across cores, was measured against alternatives before it was adopted, not assumed. The result is the asymmetry that makes the whole project work. During backfill the custom database absorbs up to **2 million writes per second**, which is what turns a multi-billion-trade replay into a sub-week job. In live mode it cruises at ~15,000 writes per second at a small fraction of capacity, with headroom for the ugliest resolution bursts, while every API response comes off a precomputed, already-sorted, already-current structure. Reads scale the same way: because no request ever executes a query, a single node is benchmarked at 50,000+ reads per second, and what it's actually spending that time on is HTTP and serialization, not data access. Latency doesn't degrade with data size, trader history, or page depth, because none of those change what a lookup costs. ## Does the same playbook power the rest of Struct? PnL is the deepest application of this strategy, but it isn't the only one. The same precompute-at-ingest strategy powers our realtime metrics across the board: market analytics, position statistics, holder data. Wherever the answer changes faster than it's asked for, we compute it as the chain moves and keep it hot, so the API's job is delivery, not derivation. ```typescript import Struct from "@structbuild/sdk"; const client = new Struct({ apiKey: "YOUR_API_KEY" }); const profile = await client.trader.getTraderPnl({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", window: "30d", }); const leaderboard = await client.trader.getGlobalPnlLeaderboard({ window: "30d", sortBy: "realized_pnl_usd", limit: 100, }); ``` That's what lets Struct's APIs be the lowest-latency way to consume this data, and it's also why they scale so well for clients. In a query-time system, every additional consumer adds query load, and heavy consumers degrade the experience for everyone else. In ours, the work of computing an answer is done exactly once, when the underlying event happens, no matter how many clients ask for it afterwards. A client polling aggressively costs us serialization, not computation. Ten times the traffic is ten times the network, not ten times the database load, which is a scaling curve we're happy to be on. ## How do we actually use RocksDB? We didn't write our own storage engine from the disk up. Both halves of the system sit on RocksDB. But almost nothing about how we run it resembles a default deployment, and the deviations are where the performance lives. - **RocksDB is demoted to durable row archive:** On the hot paths the write-ahead log is switched off entirely. We use our own append-only logs, synced once per ingest batch instead of once per row. The merged, deduplicated result reaches RocksDB later in the background. One sequential sync per batch versus thousands of logged random writes is a large fraction of the throughput story. - **Where atomicity matters, we pay for it properly:** The engine's per-block flush is a single atomic write batch that carries the state changes and the block checkpoint together. A crash can never land between "state written" and "progress recorded." Recovery is just "read the checkpoint, resume." - **One instance became many:** RocksDB serializes heavy writes through a single memtable-flush-compaction pipeline. We now run a couple dozen independent RocksDB instances, sharded by grain and by key hash. A compaction storm in one shard cannot stall its neighbors. They share one global write-buffer budget, and we disabled RocksDB's built-in write stalling in favor of explicit backpressure. - **Byte order is answer order:** Scores use an order-preserving encoding, shared exactly between the on-disk keys and the live sorted indexes. Iterating a range on disk and paging through a live index produce identical orderings by construction, which means pagination cursors work uniformly everywhere. ## What would we tell you if you're tempted? - **Precompute or perish.** For data that changes faster than it's queried, query-time aggregation is a dead end. The question isn't "how do I make this query fast" but "how do I make this query unnecessary." - **Indexes are a write cost, not a read feature.** General-purpose databases price every index against every write. When your product wants hundreds of orderings over a high-write dataset, that pricing model is the bottleneck, so change the model. - **Know your workload, then exploit it ruthlessly.** Latest-wins semantics, idempotent replays, a known access pattern: every property our data actually had became a performance multiplier that a general-purpose system couldn't assume. - **Incremental systems drift; build the auditors in.** Continuous sampled verification against first-principles recomputation is what makes "fully precomputed" trustworthy rather than terrifying. The old materialized view answered yesterday's question slowly. This system answers every question we can think of before it's asked, for every trader, every market, every block, and it doesn't slow down as the history grows, because the history was folded in the moment it happened. That's the whole trick. There is no query. There's only the answer. > If you want to put these numbers to work, grab an [API key](https://struct.to/dashboard), read the [trader PnL docs](https://docs.struct.to/api-reference/trader/overview), or wire up a [live trader PnL dashboard](https://docs.struct.to/guides/live-trader-pnl-dashboard). --- ### Polymarket RTDS vs Struct Trades WS: A Latency Benchmark Historical article. Reflects the product state as of 2026-07-12; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/polymarket-rtds-vs-struct-trades-ws Published: 2026-07-12 Category: Guides We benchmarked Polymarket's RTDS feed against the Struct Trades WebSocket across ~15,000 live trades. Struct's confirmed feed leads by ~0.5s, its mempool feed by 2.6s. Polymarket's [RTDS](https://docs.polymarket.com/market-data/websocket/rtds#real-time-data-socket) is the standard way to get live trades. We recently updated the [Struct Trades WebSocket](https://docs.struct.to/websockets/rooms/trades), and we wanted to see exactly how the latency compares. We set up a harness to track both feeds on the same clock, matching every fill by transaction hash over a sample of 15,000 live trades. Here are the results: Struct was faster on both of its paths. The confirmed on-chain feed led RTDS by about **half a second**. The mempool feed led it by **2.6 seconds**, on every single trade. The harness is open source at [structbuild/polymarket-rtds-vs-struct-benchmark](https://github.com/structbuild/polymarket-rtds-vs-struct-benchmark). Grab a free API key and reproduce every number here in one command. **The short version:** - **Struct's confirmed feed is already faster than RTDS.** Both wait for the same on-chain block, yet Struct pushed each trade a median of **~0.5s** sooner and led on roughly **85 to 90 percent** of trades. If you never touch mempool data, you are still ahead. - **Struct's mempool feed is far faster.** In `pending` mode it reads the Polygon mempool and surfaced each trade a median of **2.6s before RTDS**, leading on **100%** of matched trades. Best case: **+5.0s**. - **Comprehensive coverage.** Struct caught 100% of the trades RTDS reported, surfacing ~97% of them in the mempool before they confirmed on-chain. - **The early signal is real.** 99.9 to 100 percent of mempool order fills went on to confirm on-chain. - Method: two sockets, one process, one clock. ~15,000 trades, 11,000+ paired latency measurements, live against Polygon mainnet. ## The result, in seconds Every trade RTDS reports also shows up on Struct, and it shows up sooner. How much sooner depends on which Struct path you read: | Struct Trades WS path | Typical lead over RTDS | Faster on | Best case | | --------------------- | ---------------------- | ---------------- | --------- | | Confirmed (on-chain) | ~0.5 seconds | ~85 to 90% | ~2s | | Pending (mempool) | ~2.6 seconds | 100% | ~5s | Read the rows as "Struct surfaced the trade this far ahead of Polymarket's public feed." Even the slower Struct path, the one that waits for the block just like RTDS does, is ahead most of the time. The mempool path is ahead always. ## Why is Struct faster than RTDS? Two different reasons, one per path. **The mempool path wins on timing.** A Polymarket CLOB trade settles as an on-chain transaction. RTDS reports it once that transaction has mined and been indexed. Struct's `pending` mode reports it while it is still sitting in the Polygon mempool, before it mines. Same trade, earlier sighting. The gap between "seen pending" and "confirmed and indexed" is the entire 2.6-second head start, and on Polygon that gap is consistently multi-second. **The confirmed path wins on plumbing.** Here both feeds are waiting for the exact same thing: the confirmed block. Struct still delivered the fill a median of half a second sooner, because the path from a confirmed block to a socket push is shorter on Struct than the pipeline feeding RTDS. This is the part worth sitting with. It means Struct is faster even when you strip the mempool advantage out entirely and compare like for like on confirmed data. ## How we measured it The setup is deliberately simple so the numbers are hard to argue with. One Node process opens two WebSocket connections and stamps every incoming event with `Date.now()` off the same clock, so there is no cross-machine time skew to correct for. Struct's side is one subscription. Passing `status: "all"` delivers the pending sighting first and the confirmed sighting later for the same trade, so a single socket gives us both Struct paths: ```typescript import { StructWebSocket } from "@structbuild/sdk"; const struct = new StructWebSocket({ apiKey: process.env.STRUCT_API_KEY! }); const seen = new Map(); struct.on("trade_stream_update", (trade) => { const now = Date.now(); const row = seen.get(trade.hash) ?? {}; const isPending = trade.received_at != null && trade.block == null; if (isPending) row.pending ??= now; else row.confirmed ??= now; seen.set(trade.hash, row); }); await struct.connect(); await struct.subscribe("polymarket_trades", { status: "all", subscribe_all: true }); ``` A pending event carries `received_at` (Unix milliseconds) and omits `block` and `confirmed_at`, which is exactly how we tell the two sightings apart. The [trades room docs](https://docs.struct.to/websockets/rooms/trades) list every field and filter. Polymarket's side is the RTDS `activity` topic, the one that powers the live trade tape on their site: ```typescript const rtds = new WebSocket("wss://ws-live-data.polymarket.com"); rtds.addEventListener("open", () => { rtds.send( JSON.stringify({ action: "subscribe", subscriptions: [ { topic: "activity", type: "trades" }, { topic: "activity", type: "orders_matched" }, ], }), ); }); rtds.addEventListener("message", (event) => { const msg = JSON.parse(event.data.toString()); if (msg.topic !== "activity") return; const hash = msg.payload.transactionHash.toLowerCase(); const row = seen.get(hash) ?? {}; row.rtds ??= Date.now(); seen.set(hash, row); }); ``` Both feeds report the same on-chain `transactionHash`, so it is the join key. For each hash we keep the first sighting from each feed, and the latency between any two is the difference of their first-sighting times. We trim the first and last seconds of each window so socket warm-up and cool-down do not skew the medians. ## Every window, same result The takeaway is consistency. Both Struct paths led RTDS in every single window we captured. These results are representative of typical performance across all our testing. | Window | Trades matched | Confirmed feed led RTDS by | Mempool feed led RTDS by | | ------ | -------------- | -------------------------- | ------------------------ | | 75s | 2,300 | +0.51s | +2.88s | | 120s | 2,317 | +0.54s | +2.64s | | 120s | 1,418 | +0.36s | +2.59s | | 180s | 3,624 | +0.56s | +2.68s | | 180s | 2,911 | +0.32s | +2.66s | Every figure is a median across trades matched by hash. The confirmed feed's lead holds in a tight 0.3 to 0.6 second band; the mempool lead holds between 2.6 and 2.9 seconds. On the mempool path, Struct was never once slower than RTDS on a matched trade. ## Does Struct miss any trades? Speed is worthless if the fast feed is also lossy, so this was the number we cared about most. After trimming, Struct caught **100% of the trades RTDS reported** in every window. | Trades on RTDS's activity feed | Result | | ------------------------------ | ------ | | Also seen by Struct | 100% | | Caught early in the mempool | ~97% | | Missed by Struct | 0% | The ~3% not caught in the mempool skip the public broadcast, private orderflow and non-broadcast operator settlements, and still land on Struct at confirmation, just without the head start. The coverage gap actually runs the other way: Struct carries hundreds of lifecycle transactions per window (redemptions, merges, splits, conversions) that RTDS's activity feed never streams at all. ## Is the early mempool signal reliable? A mempool sighting is only useful if the transaction actually mines. A feed that fires on pending transactions that later get dropped or replaced would be fast and wrong. It holds up. Of the CLOB order fills we saw in the mempool, **99.9 to 100 percent confirmed on-chain**. Across every pending transaction type, including redemptions and merges, it was about 99 percent. We gate this with an eight-second confirmation grace so a trade seen pending in the last moment of a window is not falsely counted as unconfirmed. ```info Reconcile for anything financial The mempool tick is the right signal for a live tape, a fast chart, or a trigger. For settlement, PnL, or accounting, reconcile against the confirmed sighting. Struct gives you both from one subscription: the pending event for speed, the confirmed event for truth. ``` ## Subscribe to the stream The feed we benchmarked is a single subscription. Confirmed on-chain trades, already faster than RTDS, are the default: ```typescript await struct.subscribe("polymarket_trades"); ``` Mempool-only, for the full 2.6-second lead: ```typescript await struct.subscribe("polymarket_trades", { status: "pending" }); ``` Both sightings, pending then confirmed, for reconciliation: ```typescript await struct.subscribe("polymarket_trades", { status: "all" }); ``` Scope it to what you care about with `condition_ids`, `market_slugs`, `event_slugs`, `position_ids`, or `traders`, and narrow to `trade_types` like `OrderFilled` and `OrdersMatched`. The [WebSocket reference](https://docs.struct.to/websockets/rooms/trades) has the full filter surface. > [Get a Struct API key](https://www.struct.to/dashboard): the trades room, pending mode included, starts on the free plan > > [Run the benchmark yourself](https://github.com/structbuild/polymarket-rtds-vs-struct-benchmark): the full harness, open source > > [WebSocket documentation](https://docs.struct.to/websockets/rooms/trades) > > [TypeScript SDK](https://www.npmjs.com/package/@structbuild/sdk) --- ## Older article summaries These older posts are retained for historical context only. Their summaries and dates are authoritative for what was announced at the time, not for Struct's current product scope. ### Announcing Struct v0.6: Accurate PnL, Down to the Block Historical summary. Reflects the product state as of 2026-06-12; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/v0.6-release Published: 2026-06-12 Category: Product v0.6 rebuilds Struct's PnL from the ground up. Exactly what a trader has made or lost on Polymarket, updated every block, counting every fee, rebate, reward, and yield payment. ### Stream Live Polymarket Candles into a TradingView Chart Historical summary. Reflects the product state as of 2026-06-08; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/streaming-polymarket-tradingview-charts Published: 2026-06-08 Category: Guides Build a TradingView Advanced Charts datafeed for Polymarket: backfill OHLCV from the candlestick API and stream live candles from the trades WebSocket room. ### Polymarket Up/Down Markets: Live Crypto Price Data and OHLC Charts Historical summary. Reflects the product state as of 2026-06-05; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/polymarket-up-down-markets Published: 2026-06-05 Category: Guides Polymarket's Up/Down markets resolve on short price windows for BTC, ETH, and more. Here's how they work and how to pull window outcomes, OHLC candles, and live ticks via the Struct API. ### Announcing Struct v0.5: CLOB v2, Analytics, and Oracle Events Historical summary. Reflects the product state as of 2026-04-25; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/v0.5-release Published: 2026-04-25 Category: Product Polymarket's biggest infrastructure migration in years cuts over on April 28. Struct already speaks v2, and we're shipping a brand-new analytics namespace and an oracle events stream alongside it. ### Announcing Struct v0.4: Websockets, Alert Streaming, and New Pricing Historical summary. Reflects the product state as of 2026-04-12; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/v0.4-release Published: 2026-04-12 Category: Product We just shipped two Websocket endpoints for Polymarket data, including an alerts Websocket that nobody else offers. Plus, webhooks and Websockets are now free for everyone. ### Building a Polymarket Scanner Bot with the Struct SDK Historical summary. Reflects the product state as of 2026-04-02; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/building-a-polymarket-scanner-bot Published: 2026-04-02 Category: Guides How we built an open-source Telegram bot that lets you look up live Polymarket odds, trader stats, and top holders by pasting a URL or wallet address: architecture, implementation, and key decisions. ### Building a Polymarket Trader Explorer with the Struct SDK Historical summary. Reflects the product state as of 2026-03-30; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/building-a-polymarket-trader-explorer Published: 2026-03-30 Category: Guides How we built a Polymarket trader explorer - a real-time analytics dashboard with PnL charts, heatmap calendars, shareable image cards, and streaming SSR. Architecture, key decisions, and implementation details. ### Building a Polymarket Telegram Alerts Bot with Struct Webhooks Historical summary. Reflects the product state as of 2026-06-04; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/building-polymarket-telegram-alerts-bot Published: 2026-03-25 Updated: 2026-06-04 Category: Guides A technical walkthrough of how we built a real-time Telegram bot that delivers Polymarket alerts using Struct webhooks, Cloudflare Workers, and D1. Full architecture, code, and lessons learned. ### Polymarket Webhooks: Real-Time Notifications for Every Market Event Historical summary. Reflects the product state as of 2026-03-20; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/polymarket-webhooks Published: 2026-03-20 Category: Product Polymarket doesn't offer webhooks. Struct does. Get instant notifications for whale trades, price spikes, and market resolutions. ### The API for Polymarket: Built Different Historical summary. Reflects the product state as of 2026-03-15; the dated current-state section above overrides any conflicting claims. Source: https://www.struct.to/blog/the-api-for-polymarket-built-different Published: 2026-03-15 Category: Product We spent months building the infrastructure we wished existed. Today we're opening it up. Here's what Struct is, what it does, and why we built it.