Seven silent failures: real lotteries discarded, pages never read, and errors swallowed without a trace

日本語版

Development log of Toreca Dash / 7 records in one place

The worst bugs are the ones that raise no error. The job says “success” and something shows on screen, yet real lotteries are being thrown away, or links are never actually read. These are seven such silent failures, how they were noticed and how they were fixed, in the order they happened.

Each section was first published as a separate record (from 2026-10). Numbers and sources are as in the work logs of the time. They were gathered into one article per theme on 7 November 2026.

Four real lotteries silently thrown away: a regex, dates without a year, batched answers and null

Record from 2026-10-17.

My iPhone app Toreca Dash has many places that decide “this is out of scope” and throw an item away. Throwing away looks like the safe side: if it is wrong, only some junk is missing from the screen.

But mistakes of this kind give you no reason to notice them. A real lottery can be thrown away and nothing shows. This article describes four cases where I silently dropped, or nearly dropped, real lotteries.

Numbers and dates come from my work logs between 28 August and 20 September 2026.

1. “x.com” matched “e-starbox.com” (31 August)

If a link is a social-media link it is not a store page, so I do not use it. I listed the sites to exclude and joined them into one regular expression.

new RegExp(["x\\.com", "note\\.com", ...].join("|"))

That also matches inside other host names. x\.com matches the end of e-starbox.com, and note\.com matches mynote.com.

The real damage was on the lottery service “Lottery Rotobox” (shoplottery.e-starbox.com). Its application links were judged “social media, not a store link” and never adopted once.

The fix: host-only entries match only right after // or a dot ((?:^|//|\.) in front). I tested 13 examples, both URLs that should pass and URLs that should be blocked. An exclusion list looks like the safe side, so nobody doubts it.

2. A date with no year was read as six years ago (28 August)

Store and aggregator posts write dates like “30 August (Sun)” without a year. I had an AI read the text to extract deadlines.

For one aggregator, Pokémon card items came back as 30 August 2020, six years ago. A past deadline is rejected at the entrance gate, so all Pokémon card items from that source were never imported. One Piece and Dragon Ball happened to be read as 2026, which hid the problem.

The fix was to put today’s date in the prompt and “do not fill in a year before today”. Readable items went from 0 to 46. “A year consistent with the post date” is not enough, because when the post date is unknown the year is undetermined.

3. The number of batched answers did not match (19 September)

Sending one AI request per post costs one request each. So I batched several posts into one call to classify their sale type. I joined multi-line post bodies as “1. text”, “2. text”, separated by newlines.

The AI cannot tell a newline inside a post from the boundary between posts. On the day X monitoring resumed, the number of answers did not match 8 times. On a mismatch the code marked the whole group “out of scope”, so real lotteries in those groups were dropped together.

Worse, even when the count matched, some answers belonged to different posts. A Yu-Gi-Oh! lottery was thrown out as out of scope.

The fix: pass a JSON array ([{no, text}]) or headings that cannot appear in the text (“[Article N]”), make the reply include numbers, and on a mismatch ask one by one instead of falling back to “out of scope”.

4. “Cannot decide” read as “none” (20 September)

One function guesses the card genre from a post. When two or more genres appear, it returns null (“cannot decide”).

Another job used it to detect “no card words (a toy-only announcement)” as !fn(text). There, null was read as “none”.

A roundup post that lists Pokémon, One Piece, Yu-Gi-Oh!, Dragon Ball and Gundam model kits has several genres, so it returns null. That was read as “zero card words”, and because “Gundam” was found, as a toy announcement.

Left alone, the daily bulk job at 05:15 would have hidden 129 real, displayed lotteries (for 9 products such as 30th CELEBRATION). I stopped it because I ran a count-only dry run before the real run. The dry run said 129, and that is how I noticed.

The fix: to detect “none”, use the function that returns all matched genres and check that its length is 0.

What I learned

Mistakes in what you throw away raise no complaints, so count them yourself.

Candidates were found, yet reading the answer failed every time: a URL cut at 200 tokens, and a search that returned 400 on hyphens

Record from 2026-10-17.

In my app Toreca Dash, aggregator-derived articles get their links replaced with the store’s official announcement page. But developer-mode reports kept coming, seven in a row: “the correct announcement URL is this one; do it properly”.

Looking into it, candidates were being found, and the step that reads the answer failed every time. This article records the real cause and a very similar case from the same period.

Numbers come from my work logs of 23 and 24 August 2026.

The real cause: the AI’s reply was cut off at 200 tokens

The step that chooses the official announcement page asks an AI at the end: “which candidate is that store’s announcement?” I had set the reply’s length limit (max_output_tokens) to 200.

But the reply contains a long URL with Japanese percent-encoded, which exceeds 200 characters on its own. The reply was cut off, could not be read as JSON, and raised Unterminated string in JSON.

WARN [application link] official page judgement failed [shop / …]: Unterminated st…
→ all 10 targets: "not found"

Candidates had been found. It fell over every time at reading the answer. I raised the limit to 800, and added a rescue that, if it is still cut, does not discard everything but re-extracts just the "url": "… part.

Still 0 adopted after the fix: the AI was obeying my instruction

Even after fixing the JSON, 0 were adopted. The log showed candidates but “none adopted”. The cause: the AI was following the instruction faithfully. Store announcements read like this:

"[App members only] Reprint / Pokémon Card Game, 4 types, about the lottery sale"

They name no individual product. My instruction said “do not choose it if the product differs”, so the AI refused.

I fixed two things:

  1. In addition to the search by product name, always also search site:store-domain lottery and merge the candidates. (“Only when the search came up empty” never triggered, because one unrelated page was always returned.)
  2. Add to the instruction: “if the store’s own announcement page covers several products and gives no individual names, you may choose it”. Other stores and aggregator sites are never chosen, even if they cover several products.

The result: 3 of 10 were replaced with the store’s official announcement page.

I also made a list of app-only stores (stores that give lottery information only in their app), so the app can say “please check in the app”. Only stores known to have an app; I do not add by guess.

A similar case: Rakuten’s keyword search failed on hyphens (24 August)

The log kept showing “Rakuten Ichiba per-set search failed”, for Yu-Gi-Oh! alone for 8+ sets. Rakuten’s API failed the whole request with 400 wrong_parameter depending on how hyphens are used in the keyword.

Keyword shapeResult
"A - B" (spaces around the hyphen)400
"A B " (trailing space only)400
"A B-" (ends with a hyphen)400
"AB-" (a word with no space, trailing hyphen)200 (OK)

Yu-Gi-Oh! product names often put a subtitle between hyphens, as in “LIMIT OVER COLLECTION - THE RIVALS -”. Sets of that form had never once been found by the per-set search.

The fix has two steps: (1) remove spaces right after a hyphen, (2) drop trailing hyphens and spaces. “LIMIT OVER COLLECTION - THE RIVALS -” became “LIMIT OVER COLLECTION -THE RIVALS” and returned 200. I confirmed the same problem happens with the Rakuten Books API, and applied the same sanitising to the four functions that pass keywords.

What I learned

When candidates were found but the result is 0, suspect the place that reads the answer.

“Checked” marks on articles that had never been read: the t.co relay page

Record from 2026-10-17.

My iPhone app Toreca Dash collects trading-card lottery information from store posts on X (Twitter). Each post links to an application form. The app also opens that link to check the deadline and whether entries have already closed.

A report came in: “the deadline is wrong”. It turned out to be worse than a wrong deadline: the app had never read the linked page at all, and yet the article carried a “checked” mark.

This is written from my work log of 16 October 2026 (the investigation and its numbers).

The symptom: a closed form still shown as open

The reported lottery was a game shop’s “30th CELEBRATION card set, 9 types” announcement on X. The Google Form it linked to said “Responses are no longer accepted”. In the app, the lottery was still shown, with an empty deadline.

The odd part: the article had a “checked whether it has closed” timestamp. It had been “checked” and nobody noticed it had closed, so the check was not looking at the page.

The cause: t.co returns a different page if you pretend to be a browser

Links in X posts go through the short URL t.co. Normally it redirects to the real page (HTTP 301).

But if you open it with a browser User-Agent, it does not redirect. It returns a roughly 500-byte page that redirects with JavaScript. Both of my reading functions opened t.co with a browser User-Agent and “follow redirects”. So:

I had added the browser User-Agent on 6 August. This bug was a side effect of that change.

How widespread was it?

To check whether this was one case, I looked at every displayed X lottery or reservation article with a t.co link.

What I checkedResult
Displayed articles with a t.co link220
Of those, relay page returned220 (all)
Of those, actually a closed form but still displayed9

The 9 were spread over several stores, so it was a systemic problem.

One more clue I could not ignore: a daily job that fixes empty deadlines had been reporting “126 targets, 0 updated” every day. Not “nothing to fix” but “could not read, so could not fix”.

The fix

I put the redirect lookup into one function, fetchTcoTarget:

  1. Open t.co without a User-Agent, and do not follow redirects (redirect: "manual"). Take only the first target.
  2. If a target comes back, use it.
  3. If a relay page comes back, read the meta refresh tag inside it to get the real target.
  4. Then open that target with the browser User-Agent.

Whether you send a User-Agent changes what comes back, so I split the uses: resolving a short URL uses none; reading the real page uses the browser one. I added a regression test.

What to do with the wrong “checked” marks

Fixing the code is not enough. The wrong marks stay on the articles. An article with the mark counts as “already checked” and is skipped.

I removed the mark from 195 articles (X lottery/reservation posts that link to a post and have a t.co link), then ran the closed-entry check and the deadline re-extraction by hand.

JobBeforeAfter
Deadline re-extraction (targets / updated)126 targets, 0 updated (every day)123 targets, 14 updated

The same day I also changed the closed-entry check: articles with no deadline every 6 hours (with a deadline, every 24), 80 articles per run instead of 40, 7 minutes at most. Now that t.co can be read, a closed form should not stay up for a whole day.

What I learned

Write “checked” only when you could actually read it.

What I read as “fetched” was just a notice page: three days of zero products

Record from 2026-10-08.

My iOS app Torecadash regularly visits the sales pages of online shops to collect lottery and first-come sale information. One day I found that fetching from Pokémon Center Online had been returning zero products for three days straight. There were no errors, because each fetch was judged “fetched.” What was happening is written from my commit record.

This is a development log of the app.

My “fetched” check was too lenient

Pages are fetched through an external service (Scrape.do). Fetch settings range from cheap to expensive, and the design is that if the cheap setting fails, it escalates to a stronger one (the same mechanism as in my article on scraping cost, in Japanese).

The problem was the judgment of “did it fetch.” When the Pokémon Center page was fetched with the cheap setting, what came back was a 2,410-byte page saying “Please enable JavaScript”. It was not a product list, just a notice. But I read this as “fetched” and treated it as a success. If it is a success, there is no escalation to the stronger setting either.

So from September 4, for three days, fetches kept “succeeding” while products stayed at zero.

The fix

Another bit of dirt found the same day

The same inspection found another problem. The logic that decides a mismatch between alternative spellings of a product name was written separately at the entrance of matching and in the cleanup side. Only the cleanup side lacked a check for “the product forms differ.” As a result, an alternative spelling of a trial deck stayed attached to a booster pack product, and kept triggering wrong matches every day. I made the check a shared function and removed 46 entries across 27 products.

I also resolved the “no document to update” error (64 in 7 days). Now products are re-read when their cache is old, and articles that have disappeared are skipped so the remaining dedupe processing continues.

What I learned

“Success” and “the contents were fetched” are different things.

This article is written from my commit record (September 7, 2026). How I noticed the three days of zero is not written in the commit.

I opened the links of displayed articles and compared them: about 60 mistakes, and three mistakes in my own first check

Record from 2026-10-17.

The articles in my app Toreca Dash link to store and shopping-site pages. Even if an article’s title and price are right, if the linked page is closed or about a different product, it is no use to the user.

On the night of 15 September 2026, asked to “check the destination content too”, I actually opened the links of displayed articles and compared the content with the article: product, store, whether open, sale type, deadline and price. About 60 mistakes turned up.

Numbers come from my work log of 15 September 2026. The check itself made no writes; I fixed the findings separately.

How I checked

My first version of the check got three things wrong:

If the checking tool is wrong, the count of “mistakes” inflates. I fixed encoding detection, opening Amazon from Japan, and judging only by stock of “this exact item”, then reran.

Mistakes found (about 60)

WhatCountFix
Amazon “invitation request” shown as pre-order or first-come10Make it a lottery
Amazon “pre-order” shown as first-come7Make it a reservation
Rakuten Books “pre-orders open” shown as first-come3Make it a reservation
Shops closed temporarily (Yahoo!)6Hide
Sold out or closed but still displayedmanyHide
Link goes to a different store or an ended lottery3Go back to the store’s X post
Link goes to a different product2Hide
Price mismatchabout 10Correct
Empty store name1Fill in

The most frequent was the wrong sale type. On Amazon there are “invitation requests”, neither first-come nor pre-order. Treating one as first-come means users cannot buy however fast they are. The earlier code only marked the invitation and did not correct the sale type.

Some sold-out pages did not match my sold-out wording. A Yahoo! sold-out page says “out of stock” on screen, but matched none of the keywords. So I now judge by the stock data embedded in the page for that item ("stock":{"isAvailable":false). The data is more reliable than the wording on screen.

“Opening the application link goes to the site’s top page”

Found in another check the same day. The QR code on a store’s lottery image pointed to a URL that, actually opened, redirects to the site’s top page, not an application page.

I added a check to the single gate that adopts application links. It opens the URL, and if the original URL has a path other than the top but the destination path is empty (or a file name that looks like a top page), it does not adopt it. If it cannot be read (403, timeout), it still adopts, since many stores use bot protection and unreadable is not a sign of badness. The same URL is held for 6 hours.

What I did not fix, and could not fix

It matters that I did not count everything found as a “mistake”. There are cases I checked and did not fix.

I keep results in three groups with reasons: fixed, not fixed, could not read. “Not fixed” needs a reason too.

What I learned

Even if title and price match, you cannot know until you open the link.

If you throw failure away, the screen lies: a calendar that said “registered” with none added, a frozen OCR, a notification without a store name

Record from 2026-10-17.

In developing my app Toreca Dash, bugs of one kind appeared in a row: failures I had swallowed made the app show a false success. This article records three.

Written from my work log of 25 August 2026.

1. “Registered N events” while none reached the calendar

The app can export lottery deadlines to the iPhone calendar. A build warning (result of 'try?' is unused) led me to review it. try? is a Swift way of discarding a failure. The feature called save with try? store.save(...) and try? store.commit(), throwing failures away.

As a result:

The fix: return failures to the caller and count only successes. Devices with no default calendar are detected first and told. The messages are split by cause; before, every failure said “access is not allowed”.

2. When text recognition fails, the screen freezes

The feature that reads text from an application image (OCR) also discarded failure with try? handler.perform([request]). If recognition failed, the callback was never called. The “reading…” spinner never stopped, and the only way out was to close the screen.

The fix: on failure too, return to a “finished recognising (found nothing)” state.

The build warning result of 'try?' is unused is a sign of swallowing. Run the build and read the warnings regularly.

3. A notification without the store name: forgotten in the buffer

A different story with the same shape. A report said “the notification has no store name”. The article had the store name (sellerName) correctly. The notification did not show it.

Lottery and reservation notifications are put in a buffer and sent in batches (to hold them in quiet hours and to limit the rate). But the store name was not carried into the buffer. When sending, looking for the store name found no information, and only the category (“LivePocket”) remained.

This was partly surfaced by the earlier fix. When I fixed “the notification arrives under the name LivePocket”, I made it return null when the store name is unknown. That forgotten carry-over changed its symptom from “wrong store name” to “no store name”.

The same gap existed in both buffers (by genre and by product), so I fixed both. For the 5 already buffered I filled in the store name from the article and confirmed it appears when sent.

The same gap had happened on 17 August with another field (product names). The lesson: when putting something in a buffer, carry every field that will be used at send time. The other two buffers, which store the finished wording as is, do not have this problem.

What I learned

If you throw failure away, the screen lies.

It let things through when it could not check: how “fail-open” brought in resale items

Record from 2026-10-08.

In my iOS app Torecadash, three in-app bug reports said “lots of resale prices are coming in from Amazon.” One was an obvious resale item at ¥79,800. The cause was a blind spot in a design I had put in on purpose: “if it cannot be confirmed, let it pass.” This is from my hand-over notes and the commit record (July 30, 2026).

This is a development log of the app.

The price gate, and the “let it pass” design

The app picks up listings from online shops (Rakuten, Yahoo! Shopping, Amazon and others) and shows them in a feed. There is a “price gate” that checks whether a listing’s price is reasonable against the product’s list price. It has an AI (with web search) look up the list price, and rejects extremely expensive resale items.

The gate had a behavior: “if the list price could not be confirmed, let it pass without review.” That was intended. If you exclude products whose list price the AI genuinely does not know just because you cannot confirm it, you also erase genuine listings. This is so-called fail-open (open on failure, meaning let it pass).

The blind spot: hitting the search cap took the same path

The AI’s web search has a cap of 300 calls a day (a circuit breaker to prevent unexpected cost). But the gate did not distinguish the reason the list price could not be confirmed.

Both went down the same “price unknown → let it pass” path. The first is as intended. The second is just “the road was closed before looking,” yet the gate became wide open.

Why the hole did real damage

The hole surfaced because of a previous change (the one that widened Amazon’s scope). Widening the scope suddenly increased the number of products whose price had to be newly checked. As a result, the daily cap was reached often. While it was reached, the gate was open and resale items mixed in in large numbers.

There was secondary damage too. A result of “list price unknown” was designed to be cached for 24 hours regardless of reason. So a product that once hit the cap stayed open for up to 24 hours even after the cap was released.

The fix: distinguish the reason

I created a dedicated marker (QUOTA_EXCEEDED) so the two could be told apart.

SituationWhat the gate does
The AI genuinely did not know the list price (null)Let it pass as before (still fail-open)
The daily cap meant the check itself could not be done (QUOTA_EXCEEDED)Exclude (fail-close)

The latter is not put into the 24-hour cache either. It tries to confirm again the next time it is called. This gate is shared by every feature that uses price review: Rakuten, Yahoo! Shopping, Amazon, AI listing discovery, first-come Rakuten Books and so on, so one fix reaches widely (the commit record says 18 functions were deployed).

I hid the three reported items by hand. After that, the re-review batch that runs every 5 minutes is expected to pick up the same misses again.

What I have not verified: my notes say “check in the next session whether resale-price reports stopped after the cap reset.” There is no result for that in the record at the time of writing.

Is the same hole in other decisions?

In a later check (July 30, 2026), I looked at the other AI decisions such as deciding sales format and matching products. All of them are designed to fall to the safe side (exclude, or null) on failure, and I found no new fail-open hole.

What I learned

“Could not confirm” comes in two kinds: did not know, and could not look.

This article is written from my hand-over notes (session 182) and the commit record (July 30, 2026).

Key points of each

This is the English version of the original Japanese article.

More development logs, by theme: Notes. About the app: Toreca Dash.