Get the devlog by email
One email a week, only when an issue is published. Nothing else goes to this list, and every email carries a one-click unsubscribe.
Prefer a reader? JSON Feed · RSS
Issue #4
Issue #3 went up on the site on Monday and did not go out by email, so if you only read these by email you have not seen it. It is summarised at the end of this issue, before the open list, and in full at puntersedge.online/devlog/2026-09-14. The four days since it: two outages and what changed because of them, the data licence stated in one place, a week of fixes aimed at the first hour a developer spends with the API, and two venue reports from a customer reading their own data closely. As before, anything half-done says so.
What went dark, and what changed because of it
- BetGold and BoostBet published no prices from 14:29 Saturday 12 September to 13:41 Monday 14 September, 47 hours. The cause was ours: the connector sent a browser identifier cut short, a shape no real browser emits, and the platform's bot filter began refusing it. The refusal was silent by construction, logged as zero races each cycle with no error, and one address on the same host kept answering, so a spot check looked healthy. Fixed Monday at 13:41; both books were back on 115 races within two minutes. For the whole 47 hours the public coverage report said 14 bookmakers, because that count is measured over seven days, and two books that quoted normally for five days really were served in the window.
- So coverage-report.json now carries a second, faster statistic beside the first:
quoting_now_count, degraded_bookmakers and, per book, last_price_utc and hours_stale, measured over the last six hours. It fails safe: a book with no observation counts as degraded, never as quoting. Its first version then named 12 of 14 books degraded every night, because nothing races between the last jump and the next morning's first; market_quiet and races_started_recently now say whether any Australian or New Zealand race actually ran, read from two stores fed by different pipelines so both must fail to fake a quiet market. The same mistake was in /v1/health/connectors, which marked every Australian racing connector degraded overnight because two books quoting overseas racing counted as working peers; the peer set is now racing connectors only. - Sportsbet published no prices from 18:37 Wednesday 16 September to 08:46 Thursday morning. The machine that runs the Sportsbet feed lost power and slept until it was plugged back in, so Wednesday evening's racing from about 18:40 had no Sportsbet quote, Thursday's 08:00 reading had Sportsbet on 0 of 32 races, and the 10:00 reading had it on 32 of 32. Our connector alarm saw it and did not act on it: it raised and cleared every ten minutes instead of latching, which is the same shape it shows for a routine gap. Both halves are on the open list.
- Found while looking: when Sportsbet's race-listing call fails, the feed serves its last known list of races so that an empty answer does not zero the card. Never a stale price, every price is re-read each cycle; what freezes is the list. That fallback had no limit. Four times since 1 September it served a frozen list for between two and fifteen hours while reporting itself healthy, so new races never entered the feed and coverage drained as the cached ones jumped. It now stops after 15 minutes, publishes nothing, and Sportsbet drops out of the card honestly. The other 311 fallback runs on record were under five minutes and are unaffected.
- On Monday morning the announcement email about the referral programme used up the day's sending allowance with our email provider, and two login links requested in the following ninety minutes were queued instead of sent and expired unread. Two fixes: a queued login link now lives 24 hours and the email says which expiry it actually has, and batch mail stops at a daily budget that keeps room for verification and login mail, which are never queued behind a campaign again. Of the 178 announcement emails, 24 never left and were not re-sent, so issue #3's line that every active account had been told was wrong for 24 of you. The programme page says everything the email did.
One meeting, one name
Asked by a full-time punter modelling off six books, who sent two reports in one day and was right both times.
- Wellington on Tuesday arrived as
WELLINGTON for race 1 and Wellington for races 2 to 8, so a board grouped on venue_canonical showed the meeting in two pieces. Race 1 was the race transferred in from Bathurst's Sunday card; its row was created later, by a different bookmaker, and the field documented as the canonical display name was falling back to whichever spelling created the row. Measured over the live window and 60 days of results, twelve venues were being served under an all-caps and a mixed-case name at once: Ballarat, Bathurst, Cannington, Nowra, Q1 Lakeside, Redcliffe, Richmond, Rockhampton, Sandown Park, Taree, The Meadows and Traralgon. After Wednesday's fix, none. venue_id and venue_site were correct throughout, and prices and results were joined correctly the whole time; only the display name disagreed. Group on venue_id or venue_canonical, never the raw venue string. - Winnellie Park is Darwin's greyhound track, and two books send the track name where the rest send the city. It now resolves to Darwin. One meeting, one entry in the list.
- Split races merged on the way out. Bookmakers disagree about what a track is called, so one race can exist as two rows: 36 races in the 30 days to 14 September, at Pakenham, Ballarat, Sandown, Devonport and Randwick. Devonport R2 on 2 September carried the same four greyhounds twice, 8 books under Devonport and 7 under Devonport Synthetic, so the best price was the best of eight where fifteen were quoting.
/v1/racing/best-odds and /v1/racing/next-to-go now fold the two halves into one race when the registry site, code, race number and jump minute match and the fields overlap by at least half; the published venue is the name most books used; a pair that fails the field test is served as two races, never dropped. The rows underneath are untouched. - The Darwin duplicate is a different fault. Darwin's greyhound card on 15 September existed twice: the real card on 13 to 14 books, and a copy on two books running exactly 30 minutes later with the same dogs. The 30 minutes is those books' own advertised time, passed through unchanged; the copy kept taking prices until one minute after the real race jumped, then stopped. The automatic merge that handles same-minute splits deliberately refuses anything at a different second, so a time-tolerant search now runs beside it in report-only mode, proposing merges for a person to read. Nothing is merged automatically: over-merging puts one race's price beside another race's, which is worse than the duplicate. Measured over 30 days, 128 Australian races carry a duplicate. Darwin's exact-30-minute shape is 8 of them; the rest cluster at 15 to 25 minutes, the gap between greyhound races, which points at a race matched to its neighbour. That shape is not fixed and is on the open list.
Late meetings, and the morning window measured
Asked by the same customer, who named the second Victorian country meeting and the South Australian country meeting as the ones priced late.
- Two changes to how far races are re-read after the 09:30 morning window closes. A race a bookmaker lists but has not priced was never stamped as read, so it sorted first in every cycle and held the per-cycle cap; it is now re-read every five minutes on its own timer. And the first priced read of a far race between 09:30 and 13:00 Sydney puts that race on the book's morning cadence for 45 minutes, so a meeting that opens late is read every two to three minutes for its first hour instead of every fifteen.
- There is now a record of when each book first priced each race, kept for 14 days, and a report at 12:30 Sydney that lists first prices per meeting and per book. Tuesday's late meeting was Wellington, the transferred one: race 1 was first priced between 09:23 and 10:11 depending on the book, and races 2 to 8 between 09:31 and 10:09 on every book but Sportsbet, which had them at 08:11. At the 10:00 reading all 29 races on Tuesday's card carried a price; Sportsbet had win and place on 22 of them, seven still win-only, and Betr listed 24 of the 29.
- The morning window, measured. Issue #3 described the 07:45 to 09:30 tier; this is what it did. Average age of the quote across the whole day card at 09:15 Sydney, Monday to Thursday: Ladbrokes 50 to 104 seconds, PointsBet 43 to 65, TAB 49 to 118, Betr 87 to 169, Sportsbet 63 to 268, and Unibet 324 to 772 on the 13 to 20 races a day it listed. Before the tier, the same books sat at roughly 300 to 900 seconds at 10:00 on 9 September.
- Unibet's day card, off since it was built, went on at 11:56 Monday: greyhound races beyond six hours with a Unibet price went from 0 of 79 to 64 of 75 within ten minutes. It went off again at 15:20. With it on, the racing cycle every other book rides took 64 seconds instead of 35, because the lane's writes contended with the main pass on the same rows, and a first fix that moved its writes into the main pass did not hold. It stays off while the fetch and write halves are timed separately. Unibet still lists its nearest 20 races per code.
Results and form
Asked by the customer moving a racing feed onto us, whose Saturday count was the open item, and a customer commissioning the results feed for a betting app.
- Racing Australia answers ten meeting pages per run and serves a challenge page for the eleventh onward. Every hourly run on Saturday 12 September, from 07:50 to 12:50, read the same ten meetings and parsed the challenge as zero races for the other nine, the same line the log prints for a meeting not yet published, so it read as those meetings having no results. Racing Australia lists by state, so the Queensland, Western Australian, South Australian and Northern Territory cards were the ones always cut. Since Monday a challenge counts as a fetch failure, the run stops there rather than spending more requests against a wall, and the next run resumes where the wall fell, so a full card is covered every two runs. Since then every day's card has filled: Monday 22 of 22, Tuesday 29 of 29, Wednesday 46 of 46.
- Saturday 12 September is not filled. 55 of its 104 thoroughbred finals carry the full field, the same 55 as Monday morning. The fix above covers the day's card as it is published; the eight meetings lost that day (Belmont, Morphettville, Doomben, Mackay, Toowoomba, Carnarvon, Thangool, Alice Springs) need the separate backfill pass, which is run by hand and had not been run as this issue went out. All 104 are linked to their priced race and carry placings and dividends.
- Missed from issue #3: every result carries
field_status (complete, pending, overdue, unavailable, unsupported or not_applicable) with field_source and a note, and GET /v1/racing/results/coverage (1 credit) reports the fill per day and code, so a null field can be told from a late one without asking us. - Greyhound results from our own collection: 282 of 282 comparable Victorian races agree with the interim feed over the seven days to Thursday. Still report-only, still Victoria only. The premiership tables are complete: all 180 state and national tables, jockeys and trainers, both seasons, refreshed nightly without tripping the bot wall.
- Horse form is still tripping the wall. Issue #3 said form reads had moved to the second outbound route, which Racing Australia serves normally. They have, and it is better, not fixed:
/v1/racing/horses/form answered 503 to between 118 and 215 calls a day from Monday to Wednesday, down from 566 on Saturday, to two to four keys a day. Every one was a horse with no stored copy, asked for inside a sweep large enough to trip the 15-minute breaker. A 503 is unbilled and carries Retry-After, and stored horses keep serving through it. The fix that would finish this is an overnight pre-read of tomorrow's acceptances into the store at the paced rate, so a sweep of a day's runners finds most of them already there. Not built yet.
For the people building on the API
Asked by roughly a third of everyone who has used the API, who hit a 422 guessing a parameter name; a directory's import bot; and one builder who sent 22,000 consecutive 401s and left.
venue is now accepted on /v1/racing/best-odds, and limit on /v1/racing/next-to-go and /v1/racing/best-odds as an alias for num_races. The racing endpoints disagreed with each other about parameter names, and the guard that refuses an undeclared parameter, rightly, because a silently dropped filter bills you for unfiltered data, had refused venue on best-odds 2,519 times. Not done, because they are features rather than spellings: a race-number filter, date on events, and state or track on results.- Say who you are in the User-Agent. A daily report now reads the
User-Agent header off the day's traffic and flags a named caller that is getting 401s, 5xx or rate limits, or has gone quiet. It is how two named apps being served 503s on horse form were found, and how the 401 storm above was seen at all. About twenty named apps call the API today. If yours carries a name and a version, a fault on our side shows up with your name on it, which is a much shorter path to a fix than an anonymous key hitting a wall. - A 401 now says what is wrong. An
Authorization header present means the body says this API does not use bearer tokens and the key goes in X-API-Key; a near-miss header name is named; a key with trailing whitespace, which a copy from an email or a .env file routinely carries and no log shows, is called out; and every 401 links to a free key and the keyless demo endpoint, so you can prove the API works before fixing your auth. - The OpenAPI document had no
servers block, which the specification requires and which is why a directory's import bot rejected it and every generated client had to guess the base URL. It now declares https://api.puntersedge.online. GET /v1/usage?format=csv returns your spend by endpoint as a spreadsheet, with the billing period and limits in the header block: free, every plan, and not capped at 50 rows the way the JSON display is. The three keyless demo endpoints carry a signup_url field and point at the right page; they had been sending readers through a redirect. - Eight standalone example repositories, all MIT, all running against the keyless demo endpoints before you register, indexed from puntersedge-api-examples: a stdlib-only racing client, a one-file odds dashboard, AFL best price and overround in Python and Node, NRL fixtures and prices to CSV, a price-movement tracker, a sports-odds Discord bot that runs without a token, a caching backend that keeps the key server-side, and MCP client configs with real protocol transcripts. The source of the npm
puntersedge client is public at puntersedge-node; the package's repository link had pointed at a repo that did not exist. - Docs corrected: the list of books with no
source_url was stale in both directions (BetDeluxe now links every quote; BetGold, BoostBet and the exchange do not), so branch on whether the field is present rather than on the prose. The webhook example's signing secret was shaped like a real one; it now shows what actually happens, which is that the secret is returned once at creation and is never readable again. - /requests now publishes what customers asked for and what shipped: 15 entries, each checkable against the public API, no names. The API changelog is linked from the footer; it had been reachable from nowhere. And this devlog: the API signup form has an unticked box for it, it was ticked eight times in its first three days, and each issue has its own share card and a permanent address in the sitemap.
The data licence, stated once
Asked by a customer building subscriber-facing widgets who asked what Platform allows, and a prospect who read the word commercial on the Unlimited card as permission to redistribute.
- Section 8.1 of the terms now states four licences, each adding to the ones before it. Personal use (Free, Hobby, Standard): your own betting, models, research and dashboards. Display with attribution (Plus): show our odds to your own users, paying subscribers included, with a visible Prices by PuntersEdge credit. Commercial use (Business, Unlimited): the data anywhere in your business, internal systems, client work, products you sell. Redistribution (Platform): pass it on inside your product as a feed, an export or an API, without attribution.
- This moves commercial use up to Business. Until 15 September every plan carried it, so section 8.2 says what happens to you: a key that existed on 15 September keeps the earlier licence, attached to the key and not the plan, through upgrades and downgrades, for as long as it stays in continuous active use. Nothing you were already doing needs a higher plan. It is recorded against your key and we will confirm it in writing on request. It does not survive the key: cancel and create a new one later, and the new key starts under the terms as they stand then.
- Plus sits below Business on purpose. Plus is permission to show our prices in one product, and carries the commercial permission that product needs; Business is permission to use the data anywhere in a business, whether or not a price is ever displayed. The licence had been typed into sixteen pages and five were already wrong: four said redistribution needs written permission, untrue since Platform was given it on 8 September, and one said commercial use was fine on any paid tier. All 21 plan cards on four pages now render one sentence from one source, checked at startup. For a day this week the homepage and /api cards showed commercial use only on Unlimited at A$249, A$150 above the tier that carries it; fixed on Wednesday.
Plumbing
- A key created by hand carried a null usage counter, which made
/v1/usage answer 500 for that one key 37 times on Monday and Tuesday. The quieter half was worse: the same null would have made the credit check refuse every call with a 402 that no payment could clear, because the only statement that could have cleared it was the one it broke. Coerced in three places and the column made non-null; no live key was affected. - Heavy next-to-go calls, 200 races at once, grew from about 1,400 to 6,700 a day in a week, and the API serves them from one process, so a 200-race build held the line for about 1.5 seconds and every light call behind it waited. Normalising runner names was a fifth of that and is now cached, which took the median heavy build from 1.49 to 1.27 seconds in an interleaved test. The rest of the fix is structural and is next.
- The pricing page's most-called-endpoint figure was measuring our own internal traffic on a route customers cannot buy. With internal keys excluded the list reads next-to-go, results, best-odds and changes, which is what customers actually call.
- A pending operating-system security update needs the API host rebooted. The reboot is scheduled for a gap in racing overnight Thursday to Friday, Sydney time; the API is unreachable for about two minutes while the machine restarts, which the socket that covers ordinary restarts cannot hide.
Issue #3, in brief
- Issue #3 went up on Monday 14 September and was not emailed. It covers Thursday 10 to Monday 14 September. In brief:
- Saturday 12 September measured: 105 thoroughbred races across 18 meetings. Every race priced at 08:00 Sydney; all 105 with a best win and place by 09:15, Sportsbet on 104 and Betr on 103; 11 to 14 bookmakers on every race jumping before 17:00. Two things went wrong that day and are written up there: BetGold and BoostBet ran 14 minutes stale until 10:05, and Racing Australia refused form reads from 13:00 to 15:20.
- Morning tier between 07:45 and 09:30 Sydney: Ladbrokes, TAB and Betr re-price the whole day card every two to three minutes and PointsBet every minute. Placeholder guard: a quote of $21 or more at four times the market is kept out of
best_win, best_place and the market figures, and listed as suspect_win / suspect_place. - Results: the meetings Racing Australia files under another name (Kensington for Randwick, Darwin for Fannie Bay) were being dropped and now join; the racing code is part of the result-to-race match;
race_id links through the venue registry; abnormal is Racing Australia's own cell passed through, and only SB flips status to scratched; the acceptances card names every runner we did not publish; track-conditions lists each meeting once. - Email login on the account page and
/login: a one-time link, signed in for 30 days on that device, and one click issues a fresh key with the old one working for 24 hours. Paying lands you on the page that shows your key. - Referral programme: 10% of every invoice a developer you refer pays us, for 12 months, as cash or credits; your code and link are on the account page. Built with PuntersEdge is a showcase with a submission form.
- SDKs:
puntersedge on npm regenerated from the live OpenAPI document, docs at /developers/typescript; puntersedge-mcp 0.2.1 with 26 tools and three credit costs corrected. - Also: rugby union from one bookmaker to six; the sports endpoints are pre-match only and the docs say so; a payment failure on an API plan now gets an email with a link to fix the card; the NSW daylight-time date bug fixed three weeks early; premiership state tables filled; comped trials expire on schedule.
Still open
- Saturday 12 September's result fields. 49 of 104 still without the full field (Belmont, Morphettville, Doomben, Mackay, Toowoomba, Carnarvon, Thangool, Alice Springs). The backfill pass runs in the next quiet window; every day since has filled on its own.
- Unibet's day card. On for three and a half hours on Monday, off since; the write contention described above.
- Horse form 503s. 118 to 215 a day through the second route, to two to four keys. The overnight pre-read of tomorrow's acceptances is the fix, and it is not built.
- Neighbour-matched duplicate races. The 15 to 25 minute shape, about 120 races in 30 days. The report-only search sees them; the guards that stop an over-merge are the hard part.
- The Sportsbet feed's machine, and an alarm that latches. A connector that stops for hours must page once and stay paged, not raise and clear every ten minutes.
- TAB betslip deep links. Failing daily since 8 September: the slip opens but arrives empty. Unibet passes. TAB rows carry null betslip fields until a run passes. Not yet diagnosed.
- Greyhound results from our own collection. 282 of 282 agree; the flip to our own collection is still a deliberate step, Victoria only. The three collector findings from issue #3 (a frozen partial field, a horse engaged twice in one day, dead-heat placings) are still planned.
- Hong Kong: unchanged, prices only. The showcase has no entries; 20 referral codes exist and none has converted yet. BSP and traded volume: Betfair data licence.
- The API changelog at /api/changelog is eleven days behind the commits it is meant to summarise. This devlog is current; that page is not, and catching it up is on the list.
Reply with anything you want on the list. Both outages this week were found by measurement after the fact rather than by an alarm at the time, and that is the next thing to fix. The venue reports came from a customer reading their own data closely, which is still the most useful kind of report there is. Write to hamish@punters-edge.com or use the feature request form.