Between June and September 2026 we ran a collector against the Polymarket market WebSocket. At full width it held about three hundred thousand subscriptions across two machines, wrote every frame to disk as the bytes arrived, and kept a record of each interval in which a market was not being received. This page describes the venue's feed first, because the collector's shape follows from it, then the collector piece by piece, then the placement mathematics that let two machines cover for each other, and finally the numbers.
Polymarket publishes its order books over a plain WebSocketa. You open the connection, send one JSON frame naming the outcome tokens you want and the channel type, and the venue starts sending. There is no authentication for the market channel and no cost. The subscribe frame is small:
{"assets_ids": ["<token id>", "<token id>"], "type": "market"}
Tokens can be added to a live connection later with an operation: "subscribe" frame, and removed with "unsubscribe". What comes back is one of four standard events, each a JSON object with an event_type.
A book is a full snapshot for one token: every bid and ask level as a price and a size, both strings, plus the market id, the token id, a millisecond timestamp, the tick size, the last trade price, and a hashc. You get one when you subscribe and again whenever the venue decides to reseed you. A price_change is a delta: a list of entries, each naming a token, a price, the new size at that price (zero means the level is gone), the side, the new best bid and ask, and again a hash. A last_trade_price is a trade print: price, size, side, a fee rate, and the on-chain transaction hash. A tick_size_change says the market's minimum price increment moved, usually from 0.01 to 0.001 as a price pins near an extreme.
The hash on a book and on every price_change entry is the venue's own digest of the book as it stands after that updateb. If you keep a local copy of the book by applying deltas, you can recompute the digest after each one and compare. It is the only in-band way to know your copy still matches theirs, and it is the reason we kept every frame verbatim rather than a parsed view: a parsed view throws the hash away. How the hash is computed and what a mismatch does and does not mean is a page of its own: How Polymarket's order-book hash works.
The venue reaps a connection that sends nothing at the application level, even one that is receiving thousands of frames a second. The documented rule is to send the text frame PING every 10 seconds; the server answers PONG. The protocol-level ping that a WebSocket library sends for you does not satisfy it. We archived the PONGs too, which is how we later knew how many connections each listener held in any given window: the count is exactly the number of connections times the pings per window.
Two more events exist behind a flag. Add "custom_feature_enabled": true to the subscribe frame and the venue also sends new_market, a broadcast every time a market is listed, and market_resolved, sent once when a market settles, naming the winning token. The first needs no subscription at all: a flagged connection with an empty token list receives every listing on the venue, which is what we used as a birth detector. We measured the listing frame arriving about 49 seconds after the market's own activation timestamp, across 51 births with a spread of 48.4 to 50.6 seconds, so it is a fixed upstream cadence rather than network delay.
The second is delivered only for tokens you are subscribed to, and only if the flag was on the frame that established that subscription. A token added later by a plain subscribe operation gets the standard events and never a resolution, until a flagged frame re-establishes it. That rule cost us a probe campaign to establish, because the obvious test, a flagged connection with no tokens, sees every listing and no resolutions, which looks exactly like the venue not sending them. On five-minute crypto markets the resolution frame arrived about 2.5 minutes after the market's end.
One more property matters for anyone building on this feed. The round trip from a London machine to the venue's edge is about 5 ms, and yet the age of a frame when it arrives, measured against the venue's own timestamp, sits around 488 ms at the median. Most of that is publish batching on the venue's side. It puts a floor under how fresh any consumer's view can be, and no choice of region moves it.
The collector is one process per machine, called a listener, plus a small control process called the Manager that decides which markets each listener carries. A listener holds a few hundred sockets, each with about 400 markets on it. Every socket pushes its frames into one bounded channel. One writer thread drains the channel to a file on disk, the file rotates every 2 minutes, and a separate loop compresses each closed window, checks it, and uploads it to object storage. A heartbeat goes out every 60 seconds. The drawing below is the whole thing; the sections after it take the pieces in order.
The product was a promise, written in the Contract section of the repository's README and never softened:
Every frame we received, with per-market coverage-lifecycle markers (c:"start"/c:"disconnect"/c:"reconnect", keyed by asset id) bracketing every window we did and did not receive. No silent loss. No synthesised filler.
Auditable completeness boundaries are the product. A downstream consumer reconstructs any market's coverage timeline by filtering the markers for that asset and ordering by time. A gap is the distance from a disconnect marker to the next reconnect, computed by the reader, never patched by the writer. Only a graceful shutdown self-reports a disconnect; a kill, an out-of-memory, or a power loss is reconstructed from the next boot's start markers plus the frame-timestamp gap.
Everything else in the design follows from a ranking we wrote down early: reliability beats ease of use beats extensibility. Any mechanism that would compromise the contract, a voluntary unsubscribe, drop-oldest backpressure, a silent retry that swallows a gap, was treated as a design failure rather than a trade-off.
The listener does no parsing. From the second envelope version (July 2026) each line it writes is a small envelope around the frame exactly as the socket delivered it:
{"t":1733123456789012,"w":1733123456789482,"s":3,"r":"{\"event_type\":\"price_change\",…}"}
t is a monotonic sort key in microsecondsd. w is the wall clock in microseconds. Both are stamped in the same call so they can never drift apart or be sourced from the wrong clock.s is the shard, which socket carried the bytes. It is transport metadata and appears only on frames, never on coverage markers.r is the raw frame, byte for byte. If the venue sent a field we did not understand, it is in the archive anyway.Keeping the capture path free of interpretation meant any parsing or structured-view layer could fail, be rewritten, or be redeployed without touching capture. It also meant we kept things that other collectors discard. When we measured the largest free Polymarket archive (see the research page), it deduplicated repeated book states within a ten-second window, dropping about 7% of entries on busy assets, it stripped the venue's own book-continuity hash, and its completeness was unaudited. Each of those is a reasonable storage optimisation and each is fatal for microstructure work, where a level added, removed, and re-added is the signal.
The two clocks are there because of the day the sort key went backwards. The first envelope converted stopwatch ticks to microseconds by multiplying first and dividing second; on Linux the ticks are nanoseconds, so the multiply overflowed a 64-bit integer after 2.562 hours of process uptime and the result wrapped, often negative, every 5.124 hours after that. An earlier diagnosis had blamed the box's clock discipline. What separated the two was a sign test: a frame's sort key preceded its own process-start anchor by 63 minutes, which no clock step can produce. No frame was lost, and every affected window was still orderable by its wall-clock field. The paired stamp, taken in one call, is the design that came out of it, and it is why the manifest for every window records the process anchor and whether the window spans a restart.
At boot a keyset walke of the venue's market catalogue subscribes the entire active universe, liquidity-ordered. Births are caught three ways. The firehose socket described above hears every listing about 49 seconds after activation. A 10-second delta walk over the catalogue ordered by activation time, walked backwards until it reaches a market it already knows, often beats the firehose to a birth. An hourly full sweep is the backstop for anything both missed, and it is also where a market that has left the active set is noticed and, after two consecutive absences, closed with a c:"closed" marker. The catalogue stays the authority for resolution and closure; the venue's market_resolved frames were added to the archive late in the project as provenance, not as a trigger.
The cadence numbers were set by the catalogue's rate limiter. The first design polled the whole catalogue every few minutes from both listeners and the Manager, about 10 requests a second sustained with bursts to 45, and on 17 July the venue's edge started answering with 429s. Demoting the sweep to hourly and making the 10-second walk stop at the first page it already knew cut the steady load to about 1.6 requests a second.
Subscriptions are spread over many WebSocket connections of roughly 400 markets each. A connection count by itself was never the hazard: in one experiment a single listener ran 1,252 simultaneous connections cleanly. A burst of near-simultaneous connects was. In June about 500 connections redialling in lockstep after a single network blip cascaded into a 55-minute storm of failed connects, 127 of them logged, with shards down for tens of minutesf. So every socket the process opens, first connect or reconnect, passes through one process-global gate paced at 50 connects a second, and reconnects back off exponentially with jitter. At 50 a second a full-fleet ramp across a listener's ~470 sockets takes about 9.4 seconds. The gate paces off the machine's tick counter rather than the wall clock, because a backward time correction must not stall every queued connect while the markers that promised coverage are already on disk.
The frame channel holds 262,144 frames and is not allowed to overflow: an overflow throws and takes the process down, which is the contract's preferred failure. That rule was tested the first time Manager-fed placement was switched on. The boot bulk-load subscribed the whole universe essentially at once, each subscribe drew a full book snapshot, and inflow exceeded the writer's drain rate until the channel filled and the host faulted. Restarting re-ran the same bulk load, so it could never settle. Four measurements from the box turned the fix from a guess into a design. The queue climbed at about 3,200 frames a second for about 100 seconds regardless of channel size, so a bigger buffer buys a minute and then fails the same way. The drain ceiling was CPU, with the disk about 5% busy and both cores saturated servicing some 1,200 sockets: ~16.1k frames/s on the cx23, with two listeners sharing it. Doubling the cores on a cx33 moved the overflow point from ~126k subscriptions to the full universe and still faulted. And the connect gate could not bound it, because one connection's first subscribe carries ~400 markets' snapshots, so 50 connects a second admits about 20,000 snapshots a second against a 16,000 drain.
The only lever the process controls is when it asks the venue to subscribe. Every bulk subscribe path now passes through one adaptive gate whose control law is additive-increase, multiplicative-decreaseg, keyed to the channel's fill percentage rather than to CPU. The box runs near 95% CPU per listener while perfectly healthy, so CPU cannot tell busy from drowning; the channel watermark measures the thing being defended. Below 10% fill the rate probes up; at 25% it halves; at 50% a breaker admits nothing until fill drops below 10% again. The breaker also gates the connect path, so a mass reconnect's resubscribe burst is held while the channel is squeezed, with the firehose and newborn sockets exempt so a birth is never paced.
Two days after the gate shipped, a second lesson arrived. Each listener's gate watches only its own channel. A freshly restarted listener with an empty channel probed its rate up to the 50,000-a-second ceiling and starved the other listener's writer of CPU; the full one overflowed with its own breaker already open. The fix was a shared cap of 4,000 subscriptions a second, the joint rate both processes had demonstrably sustained, and, later, one listener per machine. Breaker transitions are written into the frame stream as c:"backpressure" records, so the archive itself says when admission was held back. They ride the channel with a non-blocking write, because a disclosure record must never be the thing that faults capture.
A single thread drains the channel to a hot file, syncing on a 100 ms timerh and rotating every 2 minutes. The first version held one lock across both the per-frame write and the periodic fsync. When a rotation's background gzip saturated the disk, the fsync went from milliseconds to seconds, every write queued behind it, and the writer stopped: measured on a laptop, 41,000 frames a second fell to 17 for the length of the stall, and the channel, then 65,536 frames, filled in about a second and a half. The lock now protects exactly one thing, the file handle and its append offset, and no fsync, close or rename is ever entered while it is held. That did not remove every stall: a plain write() can itself block for seconds when the kernel's dirty-page throttling engages, 2.9 seconds in one measured case at 36,000 frames a second, which is about 104,000 frames arriving into a stalled writer. The channel was raised to 262,144 to absorb that, and the memory cost was written down next to it: a mass reconnect delivers a 4 KB book snapshot per market, so a full channel is about 526 MB for one listener.
A failed write is a different animal from a slow one. After an exception from the write call an unknown number of bytes are on disk and the writer's idea of the offset is stale; writing again would overwrite committed frames with a file that still parses. So a failed write poisons the writer, which never writes again, skips its shutdown drain, and refuses to rotate. The process then exits with code 70 so the service manager marks the unit failed rather than logging a clean stop, and the next boot's c:"start" markers disclose the gap.
Closed windows are gzippedi, uploaded with a checksum-verified PUT, and described by a manifest sidecar carrying frame and coverage-marker counts, shard count, first and last timestamps, the wall-clock span, and, from the third manifest schema, a per-event-type histogram. The local file is deleted only after the upload verifies. Object storage is the sole retention store.
It was not always the sole store. The first design kept a local copy of every uploaded window for seven days as a disaster-recovery tier, with the retention rule expressed in days rather than bytes. When discovery took the subscription set from ten hand-picked assets to about two thousand, the listener produced roughly 29 GB a day against a 40 GB disk, the local tier filled it in thirteen hours, the hot write hit ENOSPC, the process faulted rather than drop a frame, restarted, resubscribed, and faulted again for five days until someone looked. The tier was deleted outright. Every byte in it was already in the store.
The rotator's boundary loop does nothing but rotate, and the reason is a second disk-full. The loop used to compress each window inline before scheduling the next boundary, and the next boundary is the next period-aligned instant strictly after now. Overrun a 2-minute boundary by a second and the cadence becomes 4 minutes; the next window is twice the size, takes twice as long to compress, and the cadence steps again. Reproduced live from an empty disk, windows went from two to four to six minutes in 34 minutes, and the run ended at 12:40:48 UTC with both listeners out of disk at once, their writers poisoned, and every shutdown disconnect marker dropped on each cycle of the crash loop. Compression now runs on a separate loop for which the closed directory itself is the work list: a window that is visible there is already durable, because rotation syncs before it renames, so the packaging step can fall behind visibly, in the heartbeat's pending-window count, without ever putting a frame at risk. Measured, compression outran capture by 15 to 35%, which is a thin margin and the reason the backlog is disclosed rather than hidden.
Two listeners subscribing the same universe give you two copies of every frame, and that is only useful if the two copies fail differently. A socket drop takes out about 400 markets at once. If the same 400 markets sit on one socket on both instances, one venue-side reset darkens them on both, and the second copy bought nothingj. The question is how to place markets so that the markets which share a socket on instance A are spread across many sockets on instance B, and the answer turns out to be a piece of mathematics from 1782.
Give every market coordinates. In production each market's token id, a large integer, is reduced to a cell (x, y) in a 601 × 601 grid: x is the id modulo 601 and y is the id divided by 601, modulo 601 again, and the Manager keeps the grid at most one live market per cell. Now instead of putting a market on socket y, put it on socket (x + g·y) mod k for some constant g. For g = 1 on a 5 × 5 grid that is the picture below, and it has a property you can check by eye: every row contains each socket number exactly once, and so does every column. That is the definition of a Latin squarek. Stepping g through 1, 2, 3, 4 gives four different squares, each with the same property; the colour bands just change slope.
Now take two of these squares, say g = 1 for instance A and g = 2 for instance B, and lay one on top of the other. Each cell now carries a pair: its socket on A and its socket on B. Count the pairs. There are 25 cells and 25 possible pairs, and every pair appears exactly once. That is the definition of two Latin squares being mutually orthogonall, and it is exactly the property we wanted: pick any market, find its four socket-mates on A (same colour on the left grid), and look at where those five markets sit on B. They are on five different sockets, one each, because if two of them shared a socket on B the pair would appear twice.
Here is why it works, and why k has to be prime. Two distinct cells (x₁, y₁) and (x₂, y₂) share a socket on the instance with constant g exactly when x₁ + g·y₁ ≡ x₂ + g·y₂ (mod k), that is when g·(y₁ − y₂) ≡ x₂ − x₁. If y₁ = y₂ the cells are in the same row, the left side is zero for every g, and they collide on no instance at all, since x₁ ≠ x₂. Otherwise y₁ − y₂ is non-zero, and because k is prime it has a multiplicative inverse modulo km, so the equation has exactly one solution: g ≡ (x₂ − x₁) · (y₁ − y₂)⁻¹. One value of g in the whole range 0 to k − 1. Any two markets that share a socket on one instance therefore share a socket on at most one other value of g, and on every instance with a different constant they are apart. With a composite k the inverse can fail to exist and the guarantee with it; 601 was chosen prime for this reason and for no other.
A word on g = 0, because production used it. With g = 0 the socket is just x mod k: a market's column. Every cell in a column has the same symbol, so that is a coordinate projection rather than a Latin square. It is nonetheless orthogonal to every square with g ≠ 0: superimpose the column colouring on any Lg and each (column, symbol) pair still appears once, since within a column the symbols x + g·y run through every value as y does. The argument above never used the fact that gA was non-zero, only that gA ≠ gB. So instance A ran the projection and instance B ran the square with g = 1, and the guarantee held. The figures use g = 1 and g = 2 so that both squares shown are genuine Latin squares. Readers who met these objects elsewhere will know them under older namesn; the family Lg for g = 1 … k − 1 is the classical construction of a complete set of k − 1 mutually orthogonal Latin squares for prime k, and any two members of the family, or one member and a projection, will do for two instances.
Now replay the outage on orthogonal grids. Kill one socket on A: its five markets go dark there, and on B they are spread across five different sockets, all still up, so every one of them is still being recorded. Kill one socket on each instance at the same moment and exactly one market is dark on both, the single cell where the two lines cross. That cell is the residual, and the residual is what the design earned in production. On the last day of July an external disconnect storm ran for fifty minutes against both instances. Roughly 45 to 48% of each instance's ~603 sockets dropped, and about 55,000 of the ~174,000 assets touched were affected on both instances at some point in the hour. Only 187 of them, 0.11% of those touched, were ever dark on both at the same instant, for 156 seconds combined, a median overlap of 0.83 s and a longest of 1.64 s, and none was left down at the end.
The construction does not stop at two instances. Each instance needs only a distinct constant, and there are k of them, so a k = 601 grid supports up to 601 instances with the pairwise guarantee intact, and an instance can be added by choosing an unused g without moving a single existing market. The mathematics is not the scaling limit; sockets and CPU are. What the grid does not do is help when the venue drops a market on both connections at once, or when the two instances share a machine and the machine goes; those are the reasons for the second box in a second data centre.
The whole fleet was two small Hetzner cloud serverso. A cx23 has 2 vCPU, 4 GB of memory and a 40 GB disk and costs a few euros a month; a cx33 has 4 vCPU, 8 GB and 80 GB. The first box was a cx23 and for two months it ran both listener instances plus the Manager. On 31 July it became a cx33, after the co-tenancy overflow described under backpressure showed that two full-universe listeners with one gate each could starve one another on two cores. When a second pair was built in August for the storage move, the primary was first tried as a cx23 again, faulted once on the warmup burst, and was rebuilt as a cx33, while the secondary, with no Manager beside it, ran clean on a cx23. Compute for the whole project was on the order of €15 to €25 a month.
Storage was the money. The first archive went to AWS S3 in us-east-1, Standard class with a lifecycle rule to Glacier Instant Retrieval at 90 days, because ingress from the boxes was free and the buckets were cheap to stand up. At full width the archive grew by about 672 GB a day, both listeners' copies combined, and S3 Standard costs about $23 per terabyte-month before anyone reads a byte, with egress billed at $0.09 a gigabyte at the first tier. A three-month projection at the measured growth came to $2,974 for storage alone and up to $13,912 if we pulled a full copy out each month. IDrive e2 stores a terabyte-month for $6, with egress free up to three times the stored volume, and the same three months came to $776. We moved. The pilot upload from the two boxes to IDrive e2's Frankfurt region ran at 504.4 MB/s aggregate from one data centre and 151.6 MB/s from the other, against a combined ingest of about 0.68 TB a day, so throughput was never in question. This site is on Cloudflare.
Until mid-August both instances shared one box in one data centre. That was deliberate. Two instances on one machine with separate sockets to the venue can tell per-connection luck from server-side correlation, because they share everything else: the same kernel, the same disk, the same egress address, the same network path. It is the right setup for measuring the venue's behaviour, and every cross-listener measurement on this page was made that way. It is the wrong setup for surviving a box failure, so in August the second instance moved to a machine in a second data centre, reaching the Manager over a private tunnelp, and the storm analysis below was written as the hypothesis test for that move: whatever residual survives physical separation is venue-side and out of reach.
Two independent observations of the same upstream reality let us measure things the venue never documents. The first question was whether the venue's own timestamp field could be trusted downstream, since the design had labelled it untrusted as a precaution. Across twelve matched window pairs from 15 June to 10 July, calm and stormy, on both subscription universes, the same frame never carried a different stamp on the two connections: 0 mismatches in 8,493,778 content-matched frames and 0 in 14,933,753 entries matched by the venue's own book hash. The book stream was delivered in one canonical order to both connections; only trade prints wander, by up to 9 positions or 216 ms. The difference between our chrony-disciplinedq arrival clock and the venue's stamp had a floor between 9.3 and 11.2 ms in every window and was never negative in 17,855,057 measurements, so the venue's clock is honest to single-digit milliseconds. What the stamp cannot do is order events: 39.6% of frames share their millisecond with another frame for the same token, so order lives in the captured sequence and time lives in the stamp. Two different instruments.
The second question was whether the placement grid actually earned its 187. Every one of those overlaps was checked for a common trigger. No two disconnect times were identical, the smallest gap was 0.742 ms and the median 146 ms, which sounds like independence and is not: two processes closing two sockets never produce identical stamps even when one event caused both. What did discriminate was clustering. At 22:00:43.90 UTC nine A-socket drops and six B-socket drops landed inside 46 ms, against a background of one event every four seconds, and that single burst produced 40 of the 214 overlapping intervals. Under a permutation null that preserved both instances' timing and shuffled only the market pairing, the observed overlap sat at −0.1 standard deviations: the pairing was uncorrelated, which is what the grid promises, while a common trigger plainly existed underneath, which the grid cannot touchr. That burst is the ceiling on what physical separation could buy.
Each figure below was read from a dated document, heartbeat, or repository query at the time of writing. The "how" column is the method, so the number can be weighed rather than trusted.
| Measure | Figure | How, and when |
|---|---|---|
| Subscriptions per listener, full universe | 320,020 and 304,589 | Heartbeat readings from the two live listeners after the August redeploy, 2026-08-15. |
| Frames per minute, per listener | ~1.1–1.4M | Same heartbeats, last-60-second frame counts. |
| Writer ceiling on a 2-vCPU box | ~16.1k frames/s | Box measurement during the 2026-07-29 overflow investigation; disk ~5% busy, both cores saturated. |
| Boot warmup burst, before backpressure | +3,200 frames/s for ~100 s | Channel fill rate during the same incident, the evidence that it was a rate problem, not a buffer-size problem. |
| Markets per WebSocket shard | ~400 markets | Shard sizing in the backpressure decision record, 2026-07-30. |
| Rotation window | 2-minute | Production rotation period from 2026-07-18; visible in the archive's key layout. |
| Placement grid modulus | 601 | Prime chosen for the orthogonal grid, in the cross-instance redundancy design, 2026-08-01. |
| Same frame, different timestamp across connections | 0 of 8,493,778 content-matched frames; 0 of 14,933,753 hash-matched entries | Comparison of two listener instances on one box over 12 window pairs, 2026-06-15 to 2026-07-10, calm and stormy conditions. |
Venue clock sanity, received_at − timestamp | floor 10.3 ms, p50 ~13 ms, p99 ~94 ms, never negative | 1.45M frames in the 2026-07-11 baseline window, listeners disciplined by chrony. |
| Overlapping downtime in a venue-wide disconnect storm | 187 of ~174,000 touched assets; 156 s combined; median 0.83 s, max 1.64 s | Per-asset down-intervals reconstructed from both instances' coverage markers for the 2026-07-31 storm hour; both instances on one box at the time. |
| Steady-state archive growth | ~672 GB/day gzipped, both listeners' copies combined (~350 GB/day after cross-listener dedup) | Measured 2026-08-05 and again 2026-08-12 at full-universe capture. |
| Upload throughput to S3-compatible storage | 504.4 MB/s aggregate from one data centre, 151.6 MB/s from the other | Four concurrent 100 MB uploads from each production box, 2026-08-15. |
| Test suite | 945 test methods; 1125 of 1127 green on the last recorded run | Count of [Fact] and [Theory] attributes in the test tree at the final commit, 2026-08-30 (878 and 67); run recorded 2026-08-12. |
| Project size | 57 pull requests merged; 12 decision records | Repository, 2026-06-02 to 2026-08-30. |
| Capture window operated | 2026-06-05 to 2026-09-13, with a known outage 2026-06-07 to 2026-06-12; full-universe width from late July | First capture date from the archive; the outage is the disk-full incident under Rotation and upload; last window from the storage listing of 2026-09-25. |
Before spending on serving infrastructure we audited the market this archive would sell into, against primary sources: vendor API specifications, SDK source, live endpoint probes, package-download statistics, and a download of the largest free archive. The full assessment, with the cost side, is its own page. In brief: a free, CC BY 4.0 archive with commercial use permitted matches the venue's own tick cadence, carries full ladder depth, and is larger than the largest paid vendor; visible paid revenue across the independent vendors we could find was of the same order as the monthly cost of running a serving platform for this archive; where willingness to pay at institutional prices exists, it attaches to multi-venue breadth; and three business models tested against evidence all failed. So we stopped. The fleet was shut down in September 2026. The software is complete, tested, and idle.
The archiver is not open source. It is available as:
Pricing on enquiry. Write to archiver@gabbatron.com.au.
Email us about the archiverThis work was designed, built, and operated by Gabbatron Pty Ltd, an independent Australian software company (ABN 98 163 064 524) based in Mackay, Queensland. General enquiries: info@gabbatron.com.au.