The Ask AI page on dhseadev.online now talks to a Cloudflare Worker instead of a Val Town val. On the WordPress side the entire change is one string, the endpoint, because the page loads its widget from whatever host that string names. Val Town is shutting down on January 10, 2027, and the dates and details are in the companion post.
What changed on the Ask AI page?
One string in the first block of page 1001: the value of the endpoint variable in the loader script. The loader builds the widget address from that value, so one edit moved both the chat calls and the widget file. I made it as a single-block edit locked to the block’s hash, which means the edit would have been refused if anything else on the page had changed since I read it. The structured-data block on the page was not touched.
The check ran against the served page, not the saved one. A fresh fetch contains the new endpoint once, contains no val.run reference anywhere, and loads the widget script from the new host. Then I typed a real question into the live page and got a streamed answer. The Worker’s health report afterward showed two requests and 1,426 tokens counted, so the usage accounting is working end to end.
Why a Cloudflare Worker?
The same reason Val Town won the first time: I cannot deploy from a terminal, so the host has to be one I can create, edit and configure from a browser dashboard. A Worker fits. It is also on the list of hosts Val Town’s own migration guides cover. I did not benchmark the alternatives, so read this as a fit to my constraints rather than a ranking. The original reasoning is in the first Ask AI devlog.
What stayed the same?
| Setting | Value |
|---|---|
| Model | openai/gpt-oss-20b, reasoning effort low |
| Per visitor | 4 questions a minute, 30 a day |
| Whole site | 3 a minute, 800 a day, 150,000 tokens a day |
| Who may call it | Only https://dhseadev.online; any other origin gets a 403 |
| Client prompts | System messages from the browser are dropped; the server’s prompt is pinned |
| Errors | Upstream error bodies are never echoed back to the visitor |
The limits are a free-tier budget I set to stay well inside Groq’s free allowance, which I covered in the post about the retired model.
What is new in the budget?
The daily counters can now live in a Cloudflare KV namespace bound to the Worker. Without it, counters sit in memory and reset whenever the Worker is recycled, so the daily caps would be soft. With it they persist. When a stream ends, the Worker reads Groq’s reported token usage and corrects the reservation it made up front. Two honest limits: per-minute caps are exact only inside one running instance, and KV is eventually consistent, so the global caps are best effort. Groq’s own limits remain the hard ceiling. If the store errors, the Worker fails open and the page stays up, because a counter outage should not take the desk down.
How do you move a 19 KB widget without corrupting it?
The widget is served by the proxy, so the new host has to serve the same bytes. I fetched it from the old host inside the browser, measured it at 19,298 bytes with a SHA-256 beginning 8ce84868, embedded it in the Worker as base64, and then fetched it back from the new host. Same length, same hash. The Worker file itself is 40,025 bytes; the copy assembled in the browser hashed to the same value as the file I had tested locally.
One thing did not go to plan. The dashboard’s code editor runs in a cross-origin frame and refused a scripted paste, so I uploaded the file through the dashboard’s own session API instead and verified by behavior: the widget hash and the health endpoint. I never opened the editor to eyeball the code after upload.
How was it tested?
Before deploying: 17 automated tests, plus eight deliberate breakages applied to a copy of the code to see whether the tests notice. They covered removing the origin gate, honoring client system prompts, ignoring the budget, echoing upstream error bodies, failing closed on store errors, flipping the sign of the usage reconcile, truncating the widget, and collapsing all visitors into one address. The first run missed one: a store failure was swallowed inside the store layer, so the fail-closed breakage changed nothing the tests could see. I added a test for corrupt stored state and the final tally was eight of eight caught.
After deploying: a request from the wrong origin returned 403, a request from the right origin returned a streamed 200 with a completion marker and usage data, and the page worked from the real interface. A passing suite is not a working product, which is why the live checks came after.
What did I get wrong or not verify?
- My first read was that the model was still the retired one. The live val already used the replacement, so that fix had shipped.
- I had added a streaming option to request usage from Groq and removed it again, because I could not verify it and the old behavior was known to work. Usage still arrives, as the token counts above show.
- I have not triggered the midnight reset, any per-visitor cap, or the site-wide cap on the live Worker. They pass in tests only.
- I tested short answers, not long ones or many visitors at once.
What is the rollback, and what is left?
Rollback is the same one-string edit pointed back at the old host, which keeps working until January 10, 2027. Left to do: revoke the old Groq key, which also ends that fallback, and update the older devlogs that describe the Val Town build with a dated note rather than rewriting them. If you want to see the result, ask the desk something.
