<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stephen Sookra </title>
    <description>The latest articles on DEV Community by Stephen Sookra  (@stephensook).</description>
    <link>https://dev.to/stephensook</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995917%2F9805a979-e04e-45cc-a7e7-f05dccd5f571.jpg</url>
      <title>DEV Community: Stephen Sookra </title>
      <link>https://dev.to/stephensook</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stephensook"/>
    <language>en</language>
    <item>
      <title>I built a way to see a sunscreen's white cast on your own face before you buy it</title>
      <dc:creator>Stephen Sookra </dc:creator>
      <pubDate>Mon, 02 Nov 2026 00:14:55 +0000</pubDate>
      <link>https://dev.to/stephensook/i-built-a-way-to-see-a-sunscreens-white-cast-on-your-own-face-before-you-buy-it-3pnj</link>
      <guid>https://dev.to/stephensook/i-built-a-way-to-see-a-sunscreens-white-cast-on-your-own-face-before-you-buy-it-3pnj</guid>
      <description>&lt;p&gt;A zinc sunscreen's label tells you how much zinc oxide is in it. It doesn't tell you how white it's going to look on your face, and you usually find that out after you've already bought the bottle.&lt;/p&gt;

&lt;p&gt;That gap hits some people a lot harder than others. In a 2024 dermatology survey, 25% of respondents with skin of color said white cast was a concern, compared to 7% of non-Hispanic White respondents, and 41% said they didn't know how to find suitable products (Wang et al., J Drugs Dermatol 2024).&lt;/p&gt;

&lt;p&gt;So I built Cast Forecast for the YouCam API Skin AI &amp;amp; eCommerce VTO Hackathon. You take a selfie, pick a mineral sunscreen, and it shows that bottle's predicted white cast on your own face before you buy it.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/sUUfVjgdH28" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Take a selfie or upload one. There's no login and nothing to install.&lt;/li&gt;
&lt;li&gt;Tap Suggest my tone. YouCam's Fitzpatrick Skin Type Analysis reads your skin type and the site suggests a starting tone. You can always change it.&lt;/li&gt;
&lt;li&gt;Tap a bottle, or search any of 687 FDA-listed zinc sunscreens. YouCam Makeup VTO paints an approximation of that bottle's predicted cast on your face, and you drag a slider to see before and after.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No selfie handy? Tap Use a sample face and you'll see recorded results without uploading anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the number comes from
&lt;/h2&gt;

&lt;p&gt;Every prediction starts from the bottle's FDA label, the zinc oxide percent, linked to its DailyMed page. My model is fitted to the raw data of a 2025 PLOS ONE study of 13 volunteers (Maldonado Lopez et al.). Tested on data it never saw, it misses by 1.61 L* on average, and 1.80 on darker skin. Every result shows its range.&lt;/p&gt;

&lt;p&gt;It also refuses instead of guessing. If your skin is deeper than the study covered, or the zinc is outside 5 to 30%, you get no number and a sentence saying why.&lt;/p&gt;

&lt;p&gt;One thing it shows really clearly is that the same bottle is predicted to leave more cast on deeper skin, from +5.3 to +8.3 L* across five tones.&lt;/p&gt;

&lt;p&gt;You can check the main number yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://cast-forecast.vercel.app/api/label-model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns the 1.61, computed by the shipped code from the study's raw data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan that didn't work
&lt;/h2&gt;

&lt;p&gt;My first plan was to measure every bottle myself, on cards and on skin, and run it through a film model. I decided to ship without in-person testing, so I needed a prediction I could actually defend.&lt;/p&gt;

&lt;p&gt;I tested a simple model based on the label's zinc percent against my film model, on data neither one had seen, and the simple one won by a lot (1.61 versus 4.79 L*). So I went with the simple one and I show its error on every number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two times the YouCam API surprised me
&lt;/h2&gt;

&lt;p&gt;The makeup try-on failed my first test when I used coverage as the strength. When I switched to color intensity, it passed every check, and the lightening followed a smooth curve. I ran 16 renders of my own selfie across the full strength range and fitted that curve (R² 0.98). Now the app turns it around to pick the strength for each bottle and tone.&lt;/p&gt;

&lt;p&gt;The skin tone analysis gave the same answer every time for the same photo, but it barely moved when I made the photo brighter. It reports a tone corrected for lighting, not the color in the photo. That's good for a shopper, but it meant I couldn't use it as a measurement, so the Fitzpatrick analysis suggests a tone and the shopper confirms it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two YouCam APIs, each for the job it's best at: Fitzpatrick Skin Type Analysis (Skin) to suggest a tone, and Makeup VTO (Beauty) to render the cast. Both run server side through Perfect Corp's REST flow (upload, create task, poll), so the key never reaches the browser, with per-session and per-IP limits and a credit floor.&lt;/li&gt;
&lt;li&gt;The bottles come from the FDA. openFDA's drug label API had 4,034 zinc records, and 687 zinc-only sunscreen labels are kept after leaving out tinted, powder, lip, unreadable and duplicate labels.&lt;/li&gt;
&lt;li&gt;The model is fitted in Python and ported to TypeScript, and tests pin the port to the Python numbers.&lt;/li&gt;
&lt;li&gt;The web app is Next.js on Vercel. GitHub Actions runs lint, type checks, tests, the build and a scan for leaked keys on every push, and a daily job checks the live site and the credit balance.&lt;/li&gt;
&lt;li&gt;For AI shopping agents, /api/mcp is a Model Context Protocol server (search the labels, predict a cast, get the held-out error) and /llms.txt explains it in plain text.&lt;/li&gt;
&lt;li&gt;The look comes from Lullabuy, my HackGT 13 project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;Every cast is a prediction from the label, not a measurement on a person, and the render was calibrated on one person (me). It shows how a sunscreen looks, not how well it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://cast-forecast.vercel.app" rel="noopener noreferrer"&gt;https://cast-forecast.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo film: &lt;a href="https://youtu.be/sUUfVjgdH28" rel="noopener noreferrer"&gt;https://youtu.be/sUUfVjgdH28&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/StephenSook/cast-forecast" rel="noopener noreferrer"&gt;https://github.com/StephenSook/cast-forecast&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Devpost: &lt;a href="https://devpost.com/software/cast-forecast" rel="noopener noreferrer"&gt;https://devpost.com/software/cast-forecast&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever bought a mineral sunscreen and regretted it the first time you put it on, I'd really love to know which one it was.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>python</category>
    </item>
    <item>
      <title>I let Twitch chat break my game's code, then made Codex race them to fix it</title>
      <dc:creator>Stephen Sookra </dc:creator>
      <pubDate>Sun, 01 Nov 2026 20:10:34 +0000</pubDate>
      <link>https://dev.to/stephensook/i-let-twitch-chat-break-my-games-code-then-made-codex-race-them-to-fix-it-acn</link>
      <guid>https://dev.to/stephensook/i-let-twitch-chat-break-my-games-code-then-made-codex-race-them-to-fix-it-acn</guid>
      <description>&lt;p&gt;So on Sunday afternoon I was live on Twitch with my game on screen, and a viewer typed &lt;code&gt;!break&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The game reloaded right there on stream with a real bug in it. One plus sign in the physics code had turned into a minus, so gravity was pointing the wrong way. Fourteen seconds after the break, that same viewer typed &lt;code&gt;!guess gravity-sign&lt;/code&gt; and got it right. The overlay said CHAT WON. Codex still came in after its wait, found the line and had the fix passing in about 28 seconds, and the fixed code swapped into the running game without resetting the player.&lt;/p&gt;

&lt;p&gt;The funny part is that the viewer was on another team in the same hackathon. That was honestly one of my favorite moments of the whole weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;On most streams chat just watches. You type something and it flies by. I wanted chat to have a real job, so I gave them the thing every developer hates the most, which is a bug showing up live.&lt;/p&gt;

&lt;p&gt;Most AI streams I've seen put chat and the AI on the same team. Chat asks for a feature and the AI builds it. I flipped that. Chat breaks the game, and Codex has to clean it up, so chat finally has someone to beat.&lt;/p&gt;

&lt;p&gt;I built it solo for the Codex x TwitchCon Hackathon and called it Chat vs Codex.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a round works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;A viewer types &lt;code&gt;!break&lt;/code&gt;, redeems the "Break the game" Channel Points reward, or whispers a bug type so nobody sees it coming.&lt;/li&gt;
&lt;li&gt;The server picks one real, pre-tested bug and splices it into the game's TypeScript. The game reloads broken on stream.&lt;/li&gt;
&lt;li&gt;Chat gets a 15 second head start to &lt;code&gt;!guess&lt;/code&gt; what broke while a replay shows the symptom.&lt;/li&gt;
&lt;li&gt;Then Codex gets a short bug ticket. It says which replays failed, the first frame where things went off and what viewers saw. It never names the bug, but if chat guessed during the head start, the most popular guess rides along as a one-word hint.&lt;/li&gt;
&lt;li&gt;Replay tests decide. If they pass, the fixed code hot-swaps into the game.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every fixed round leaves something behind too. There's an autopsy card with the real diff, a stream marker at the break and another at the fix, an auto clip with its link posted in chat, and the result pinned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part was making it fair
&lt;/h2&gt;

&lt;p&gt;Getting an AI to fix a bug on stream is the easy part. Making it a fair race is where almost all the time went.&lt;/p&gt;

&lt;p&gt;Codex runs in a sandbox under a custom permission profile. It can't read the original code, it can read the tests but can't edit them, and it has no network. A canary test checks all of that from inside the sandbox, so I'm not just trusting the config.&lt;/p&gt;

&lt;p&gt;A fix only counts if the replays pass. The game runs at a fixed 60 Hz with integer physics, so the same inputs give the same frames every time. That's what lets a test actually prove a fix instead of just saying it compiles.&lt;/p&gt;

&lt;p&gt;The bugs have to earn their spot too. Stryker suggests candidates and most of them don't make it. A bug has to compile, change how the game behaves, fail a replay, show up on screen within 600 frames and leave the player alive. 53 made the pool.&lt;/p&gt;

&lt;p&gt;Then I needed to know if the race was even close. I ran 212 sandboxed trials over that pool, with no hints at all, and Codex fixed 211. The one miss was a dropped network connection, and I kept it in the count. GPT-6.1 Terra at low effort had a median of 14.5 seconds to a passing fix, which is about the same as chat's head start. So it's a real race, and chat can actually win it.&lt;/p&gt;

&lt;p&gt;There's also a boss round where two Codex models race each other on the same bug while chat bets on who wins. On Sunday GPT-6.1 Terra beat GPT-6.1 Sol in 22.1 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it with Codex
&lt;/h2&gt;

&lt;p&gt;Codex wrote almost all of the code. I ran one main Codex thread for the whole build. I decided what got built and in what order, Codex wrote it, and every turn had to pass CI before the next one started. That ended up being 17 turns, 99 commits on main and 787 tests running on every push. The phone app came from a second Codex thread.&lt;/p&gt;

&lt;p&gt;Codex is also the opponent at runtime. Each round starts a fresh run through the Codex SDK, and its real steps stream to the overlay so chat can watch it work. During a round it can also call a small read-only MCP server I built called Game Inspector, which replays frames and finds the first frame where the broken game drifts from the good one.&lt;/p&gt;

&lt;p&gt;On the Twitch side, EventSub brings in chat messages, Channel Points redemptions and whispers. Helix handles everything going back out, like chat messages, the reward, refunds, stream markers, clips and pins. I run it on my own regular channel, which isn't Affiliate or Partner, so you don't need either one.&lt;/p&gt;

&lt;p&gt;I also made a phone app called Pit Wall. My phone buzzes the moment chat breaks the game, and I can veto a round or send Codex a one word hint. On iPhone there's a Live Activity on the lock screen with the score and the last result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff that went wrong
&lt;/h2&gt;

&lt;p&gt;Going live kept wiping my channel. OBS's Twitch dock had old values saved, so every time I went live it overwrote my title, category and tags. Now I reset them from the API right after.&lt;/p&gt;

&lt;p&gt;One live round took 1:14 because another Codex build was eating my laptop's CPU at the same time. I don't run builds during live rounds anymore.&lt;/p&gt;

&lt;p&gt;Right before Sunday's stream my Twitch refresh token died and I had to redo the sign-in. And at one point my Mac was down to 384 MB of free space because an iPhone simulator download ate the rest. That was fun.&lt;/p&gt;

&lt;p&gt;The biggest thing I learned is that a fair AI race is way harder to build than a flashy one. Hiding the answer from Codex, proving every fix with replays and giving chat a real head start took most of the work. That's also the part that makes it fun to watch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Replay a real round in your browser, no install and no key: &lt;a href="https://chat-vs-codex-judge.vercel.app" rel="noopener noreferrer"&gt;https://chat-vs-codex-judge.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The round where a viewer beat Codex: &lt;a href="https://www.twitch.tv/steve_sociall/clip/SpoopyConfidentBobaLitty-mrwNi8kcsFCEbvn5" rel="noopener noreferrer"&gt;https://www.twitch.tv/steve_sociall/clip/SpoopyConfidentBobaLitty-mrwNi8kcsFCEbvn5&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The boss round: &lt;a href="https://www.twitch.tv/steve_sociall/clip/StrongOpenLasagnaJonCarnage-RfFf1HVjaLdZhpAm" rel="noopener noreferrer"&gt;https://www.twitch.tv/steve_sociall/clip/StrongOpenLasagnaJonCarnage-RfFf1HVjaLdZhpAm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/StephenSook/chat-vs-codex" rel="noopener noreferrer"&gt;https://github.com/StephenSook/chat-vs-codex&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pit Wall on iPhone (TestFlight): &lt;a href="https://testflight.apple.com/join/ZpGa8pcr" rel="noopener noreferrer"&gt;https://testflight.apple.com/join/ZpGa8pcr&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pit Wall on Android: &lt;a href="https://github.com/StephenSook/chat-vs-codex/releases/tag/pit-wall-android-preview-1" rel="noopener noreferrer"&gt;https://github.com/StephenSook/chat-vs-codex/releases/tag/pit-wall-android-preview-1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you stream and want to try it, setup is Node 22, pnpm and the Codex CLI on your computer, one OBS browser source and a one time Twitch sign-in.&lt;/p&gt;

&lt;p&gt;What I really want to see next is a big chat, like a few hundred people all trying to break the same game at once. I wonder how long Codex holds up against a chat that actually works together.&lt;/p&gt;

</description>
      <category>twitch</category>
      <category>ai</category>
      <category>gamedev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How I built Curtail: a governed agent fleet for water curtailment, where the human always signs</title>
      <dc:creator>Stephen Sookra </dc:creator>
      <pubDate>Tue, 18 Aug 2026 03:42:55 +0000</pubDate>
      <link>https://dev.to/stephensook/how-i-built-curtail-a-governed-agent-fleet-for-water-curtailment-where-the-human-always-signs-3861</link>
      <guid>https://dev.to/stephensook/how-i-built-curtail-a-governed-agent-fleet-for-water-curtailment-where-the-human-always-signs-3861</guid>
      <description>&lt;p&gt;In August 2022, a water association serving about 80 ranchers ran its pumps for eight days against a standing drought curtailment order on the Shasta River. The proposed penalty came to $4,000 in total, which is $500 per day. One rancher said, on the record: "We could have kept going for $500 a day."&lt;/p&gt;

&lt;p&gt;The statute has since moved. Water Code 1846(b), as amended effective January 1, 2025, sets the rate at $10,000 per day. But the regulation a person actually reads, 23 CCR 875.9(b), still prints $500. The published rule understates the law by a factor of twenty, and nothing in the state's tooling notices.&lt;/p&gt;

&lt;p&gt;That gap is the product. Curtail is my entry to the Google x Devpost All Things Agentic Hackathon: a governed multi-agent system that watches a river in real time, computes who must stop diverting under California prior-appropriation priority law, and drafts the curtailment order for a named human official to sign. Nothing self-executes. This post is about how it was built, and what building it taught me about putting language models anywhere near legal text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first decision: what may never be an LLM
&lt;/h2&gt;

&lt;p&gt;The center of the system is not an agent. The Allocation Core, the thing that decides which priority groups a curtailment must reach, is deterministic Python with golden tests and property tests. Priority ordering, decree membership, gage reading against the operative minimum, computed shortfall: these are facts, and facts do not need a temperature parameter.&lt;/p&gt;

&lt;p&gt;Just as deliberately, the Core produces a recommendation, never a determination. The regulation (23 CCR 875(b)) assigns the determination to a human official and expressly preserves their discretion to decline, narrow, or suspend. So the codebase uses the word "recommendation" throughout, and "determination" is reserved for the signature. The signed record shows what the official actually considered, including the judgment inputs the system surfaces but refuses to auto-resolve: weather, fisheries information, voluntary contributions, groundwater evidence.&lt;/p&gt;

&lt;p&gt;Around that core sits a four-node fleet built on Google's Agent Development Kit, running on Cloud Run: a Gage Sentinel that classifies USGS readings against date-period-bounded minimums, an Order Scribe on gemini-3.5-flash via Vertex AI that drafts the legal document, a Herald that models the four legally distinct service methods under Water Code 1121 and never reports a notification as legal service, and a Season Ledger holding the statutory clocks (reconsideration windows, response deadlines) durably in Cloud Firestore. Durability was proven the only way I trust: a second, independently constructed client read the season back after the writer was gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guards, because models fabricate exactly where it hurts
&lt;/h2&gt;

&lt;p&gt;Before writing any code I had six external models review the concept. Two of them cited case law that does not exist in any database. Invented authorities, with confident citations, in a legal domain. That settled the security design: a system prompt forbidding invention is not a guardrail.&lt;/p&gt;

&lt;p&gt;So the Scribe's output passes through a deterministic, server-side citation scrubber after the model call. Any authority not present in a verified allowlist is stripped and flagged before the draft can reach the document generator, and the fabricated citations from that review ship in the repository as a CI test fixture, so the guard can never quietly stop guarding. A second layer, ledger validation, rejects any draft that asserts a right, a priority date, or a tier the deterministic Core did not compute, with one retry and then escalation to the human queue marked UNVERIFIED. Model Armor screens untrusted order text on the way in, chunked to fit its documented prompt-injection window, and an unreachable screen reports UNAVAILABLE rather than clean.&lt;/p&gt;

&lt;p&gt;The same discipline applies to my own prose. Every number in the demo video narration, the README, and the Devpost description draws from one generated fact sheet, docs/FACTS.md, computed from the shipped code. CI fails if the file drifts from the code that produces it. A gate reads the narration script and refuses any figure, digits or spelled out, that the fact sheet cannot source. That gate caught real errors before they reached a published, uncorrectable video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing against the Board's own decisions
&lt;/h2&gt;

&lt;p&gt;The credibility artifact is a backtest. I fetched the State Water Board's published curtailment orders and addenda for the Scott and Shasta rivers, 98 PDFs byte-verified, 95 scorable, and replayed the gage record against them. Curtail reproduces the direction of 6 of 6 scored historical curtailment decisions, with 5 documents excluded before scoring for stated reasons (a bound is not a reading; a named-diverter scope is not a basin threshold decision). The exclusions are reported next to the result, because a denominator you cleaned in private is not a metric.&lt;/p&gt;

&lt;p&gt;The rights tables are the Board's own attachments, not synthetic data. The Scott table carries 384 rights, and here is the finding that justifies the whole architecture: the Board's attachment states each right's curtailment group in its own column, and inferring those groups from the rights' attributes instead agrees with the Board on 8 of 384. Group 1 is curtailed first and group 8 nearly last, so that gap is the difference between a ranch irrigating and a ranch shutting off. Read the record; never re-derive what the agency already stated.&lt;/p&gt;

&lt;p&gt;My favorite fixture is July 2025. Fort Jones read 48.7 cfs on a Sunday night and curtailment was reinstated. Community members disputed the measurement, the watermaster district ran field flows, USGS revised the rating curve, and the same water read 78.4 cfs on Tuesday morning. Curtailment lifted. The river never rose; the measurement moved. An agent system that cannot be corrected by the person responsible for it is not governed, so that sequence, human evidence flowing back into the machine's recommendation, is the demo's centerpiece, and the near-threshold band that flags "field verification recommended" exists because of it.&lt;/p&gt;

&lt;p&gt;There is also a scored axis for restraint: five cases where the right answer is not the obvious action, including a reading inside the near-threshold band and rights the Board published without a priority date. The system refuses or withholds rather than extrapolating. Scoring that axis immediately found a real bug: the Sentinel would classify a negative discharge, which on a sensor fault reads as far below minimum and points at curtailment. That is now unrepresentable at the domain object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data sovereignty, concretely
&lt;/h2&gt;

&lt;p&gt;One more piece worth naming: document intake runs Gemma (gemma3:4b) locally through Ollama. It reads a published Board order and returns four filing fields, each verified verbatim against the source text before acceptance, and no document leaves the machine. An agency that cannot send landowner records to a third-party inference API can host these weights itself. The model files documents; it is not permitted to read law out of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Most of my defects were false greens: checks that reported success without checking. A guard scoped to tracked files cannot see the file you are about to commit. A web application firewall serves its block page as HTTP 200, so a corpus fetcher that trusts status codes downloads error pages named as PDFs. A test can assert a figure that no primary document contains, and then defend the error against correction. The pattern underneath all of them: verify the artifact, never the action that claims to have produced it. That principle is also the product thesis, which is the kind of symmetry you only notice after the third incident.&lt;/p&gt;

&lt;p&gt;The demo video's agent section is one continuous, uncut take of the production system, including the full length of the real Gemini call, because a rules requirement ("an unedited, live execution") is also just the honest way to show an agent working.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Demo (3:41): &lt;a href="https://youtu.be/Kz5tv6oe070" rel="noopener noreferrer"&gt;https://youtu.be/Kz5tv6oe070&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live console, no account needed: &lt;a href="https://curtail-console-api-672785135387.us-central1.run.app" rel="noopener noreferrer"&gt;https://curtail-console-api-672785135387.us-central1.run.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository (Apache-2.0): &lt;a href="https://github.com/StephenSook/curtail" rel="noopener noreferrer"&gt;https://github.com/StephenSook/curtail&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I created this piece of content for the purposes of entering the All Things Agentic Hackathon. #AllThingsAgenticHackathon&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>ai</category>
      <category>python</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>Building strongly consistent parental consent across regions with Amazon Aurora DSQL</title>
      <dc:creator>Stephen Sookra </dc:creator>
      <pubDate>Mon, 22 Jun 2026 01:56:08 +0000</pubDate>
      <link>https://dev.to/stephensook/building-strongly-consistent-parental-consent-across-regions-with-amazon-aurora-dsql-d8a</link>
      <guid>https://dev.to/stephensook/building-strongly-consistent-parental-consent-across-regions-with-amazon-aurora-dsql-d8a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I created this article for the purpose of entering the H0 Hackathon, Hack the Zero Stack with Vercel v0 and AWS Databases.&lt;br&gt;
&lt;strong&gt;#H0Hackathon&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://custody-zeta.vercel.app" rel="noopener noreferrer"&gt;https://custody-zeta.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap I wanted to close
&lt;/h2&gt;

&lt;p&gt;When a parent revokes consent or sets a spending cap for a child on a global platform, that decision may not take effect everywhere at once.&lt;/p&gt;

&lt;p&gt;The platform writes the change in one region, replication catches up later, and during that window, the child may still be able to play or spend money somewhere else. There is also rarely verifiable proof of exactly when the parent acted.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Custody&lt;/strong&gt;, a neutral system of record for parental consent and minor spending controls, to close that gap.&lt;/p&gt;

&lt;p&gt;With Custody, a parent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grant or revoke consent&lt;/li&gt;
&lt;li&gt;Set a spending cap&lt;/li&gt;
&lt;li&gt;Apply the decision consistently across regions once it commits&lt;/li&gt;
&lt;li&gt;Verify every decision through a tamper-evident audit trail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every action becomes a link in a hash chain that can be verified directly in the browser.&lt;/p&gt;

&lt;p&gt;All data in the demonstration is synthetic. It contains no real minors, biometrics, or personal information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I chose Amazon Aurora DSQL
&lt;/h2&gt;

&lt;p&gt;Custody depends on one critical property:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a write commits, every region must agree on the result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Amazon Aurora DSQL is an active-active, multi-region database with strong consistency on commit. It provides the property Custody needs without requiring me to build and maintain a distributed consensus layer myself.&lt;/p&gt;

&lt;p&gt;The deployment uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;us-east-1&lt;/code&gt; as one regional endpoint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;us-east-2&lt;/code&gt; as the second regional endpoint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;us-west-2&lt;/code&gt; as the quorum witness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The witness does not expose a database endpoint. Its purpose is to help maintain agreement between the two writable regions.&lt;/p&gt;

&lt;p&gt;One framing rule I followed throughout the project was never calling the process “instant.”&lt;/p&gt;

&lt;p&gt;A commit requires roughly two cross-region network round trips, and an optimistic-concurrency retry may add more time. The accurate claim is that the system is &lt;strong&gt;strongly consistent on commit&lt;/strong&gt;, without a replication-lag window after the commit completes.&lt;/p&gt;

&lt;p&gt;Precision matters more than a superlative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model
&lt;/h2&gt;

&lt;p&gt;Custody uses append-only event tables combined with per-entity projection rows.&lt;/p&gt;

&lt;p&gt;Aurora DSQL is PostgreSQL-compatible, but it is not traditional PostgreSQL. The application architecture must respect its specific rules.&lt;/p&gt;

&lt;p&gt;A few important examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary keys use random UUIDs instead of &lt;code&gt;SERIAL&lt;/code&gt;, because sequences can create hot keys.&lt;/li&gt;
&lt;li&gt;DSQL does not support foreign keys, triggers, or stored procedures, so referential integrity is handled in the application layer.&lt;/li&gt;
&lt;li&gt;Indexes are created with &lt;code&gt;CREATE INDEX ASYNC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A unique asynchronous index does not enforce uniqueness until its build job finishes, so migrations wait for completion through &lt;code&gt;sys.jobs&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each consent action and spending action is stored as an append-only event.&lt;/p&gt;

&lt;p&gt;The current consent status and running spending total are stored as projection rows. Each projection is updated in the same transaction as its related event append.&lt;/p&gt;

&lt;p&gt;These projections exist per user or per minor. Custody never relies on a single global counter row because that row would become a hot key and create cascading conflicts under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing the hash chain from forking
&lt;/h2&gt;

&lt;p&gt;The part of the data model I am most proud of is how it handles concurrent writes.&lt;/p&gt;

&lt;p&gt;Each event table uses a composite primary key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(user_id, seq)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imagine two requests attempting to append an event for the same user at the same time.&lt;/p&gt;

&lt;p&gt;Both requests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the same current chain tip.&lt;/li&gt;
&lt;li&gt;Calculate the same next sequence number.&lt;/li&gt;
&lt;li&gt;Attempt to insert the next event.&lt;/li&gt;
&lt;li&gt;Collide on the composite primary key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Aurora DSQL then raises a serialization conflict at commit using SQLSTATE &lt;code&gt;40001&lt;/code&gt; with error code &lt;code&gt;OC000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One transaction succeeds. The other retries, reads the new chain tip, and appends after it.&lt;/p&gt;

&lt;p&gt;This prevents the chain from forking.&lt;/p&gt;

&lt;p&gt;I tested this with eight concurrent append requests. The stored events returned with sequence numbers &lt;code&gt;1&lt;/code&gt; through &lt;code&gt;8&lt;/code&gt;, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No gaps&lt;/li&gt;
&lt;li&gt;No duplicate sequence numbers&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;prev_hash&lt;/code&gt; matching the previous event’s &lt;code&gt;entry_hash&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The optimistic-concurrency wrapper
&lt;/h2&gt;

&lt;p&gt;Every write passes through the same retry helper.&lt;/p&gt;

&lt;p&gt;The helper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retries only serialization conflicts using &lt;code&gt;40001&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Uses exponential backoff with full jitter&lt;/li&gt;
&lt;li&gt;Limits the maximum number of attempts&lt;/li&gt;
&lt;li&gt;Retries the entire transaction, never only part of it&lt;/li&gt;
&lt;li&gt;Does not retry normal reads, because DSQL does not conflict-check plain reads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writes are also idempotent.&lt;/p&gt;

&lt;p&gt;The client provides a request UUID, which is stored under a unique asynchronous index. The application uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the same request is replayed, it becomes a no-op rather than creating a duplicate event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tamper-evident audit trail
&lt;/h2&gt;

&lt;p&gt;Custody’s audit trail is a separate SHA-256 hash chain for each user.&lt;/p&gt;

&lt;p&gt;Each &lt;code&gt;entry_hash&lt;/code&gt; is calculated from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The canonical JSON representation of the event payload&lt;/li&gt;
&lt;li&gt;The previous event’s hash&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first event uses a genesis previous hash containing 64 zeros.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entry_hash = SHA256(canonical_payload + previous_hash)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Canonical JSON is important. Custody uses deterministic canonical JSON in the JCS style: object keys are recursively sorted and formatting is stable, so two logically identical payloads always serialize to the same bytes. It is not a full RFC 8785 implementation.&lt;/p&gt;

&lt;p&gt;Without canonicalization, two logically identical JSON objects could produce different hashes because of differences in key ordering or number formatting. That would create false tampering warnings.&lt;/p&gt;

&lt;p&gt;The verification function recalculates every event hash and reports the exact first index where the stored chain no longer matches.&lt;/p&gt;

&lt;p&gt;Verification runs in the browser using the Web Crypto API. A judge can modify an event block in the interface and immediately see the chain turn red starting from the first broken link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credential-free authentication on Vercel
&lt;/h2&gt;

&lt;p&gt;There is no database password stored anywhere in Custody.&lt;/p&gt;

&lt;p&gt;The frontend uses Next.js 16 and runs on Vercel.&lt;/p&gt;

&lt;p&gt;At runtime:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Vercel issues an OpenID Connect token.&lt;/li&gt;
&lt;li&gt;The application exchanges it through AWS &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;AWS grants access to a restricted IAM role.&lt;/li&gt;
&lt;li&gt;The Aurora DSQL connector creates a short-lived, region-scoped authentication token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A token created for one region only authenticates against that region’s endpoint. Because of this, the application maintains a separate connection pool for each regional endpoint.&lt;/p&gt;

&lt;p&gt;This architecture removes the need for permanent database credentials in Vercel environment variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demonstrating cross-region consistency with SSE
&lt;/h2&gt;

&lt;p&gt;The cross-region demonstration uses Server-Sent Events.&lt;/p&gt;

&lt;p&gt;Vercel does not host persistent WebSocket servers, so each region has its own SSE route that streams the latest committed state.&lt;/p&gt;

&lt;p&gt;When a parent revokes consent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Next.js Server Action submits the transaction.&lt;/li&gt;
&lt;li&gt;The transaction commits to Aurora DSQL.&lt;/li&gt;
&lt;li&gt;Both regional streams read the new committed state.&lt;/li&gt;
&lt;li&gt;Both region panels update.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;us-east-2&lt;/code&gt; panel reads from the second Aurora DSQL endpoint. It is not a duplicated client-side animation.&lt;/p&gt;

&lt;p&gt;What appears in the interface reflects the state returned by the two regional database endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardening the public demonstration
&lt;/h2&gt;

&lt;p&gt;The deployed application is open for judges and other users to test, so the public database role follows the principle of least privilege.&lt;/p&gt;

&lt;p&gt;It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SELECT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Drop tables&lt;/li&gt;
&lt;li&gt;Run DDL&lt;/li&gt;
&lt;li&gt;Take ownership of tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aurora DSQL also does not support &lt;code&gt;TRUNCATE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This gives the public application exactly what it needs: read access, append access, and permission to update projection rows. It does not have permission to wipe the database.&lt;/p&gt;

&lt;p&gt;Mutation routes are also rate-limited with Upstash Redis, and the event ledger is append-only by construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is wired into the live app
&lt;/h2&gt;

&lt;p&gt;The deployed version includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A multi-region Aurora DSQL ledger&lt;/li&gt;
&lt;li&gt;Strongly consistent cross-region commits&lt;/li&gt;
&lt;li&gt;Optimistic-concurrency retries&lt;/li&gt;
&lt;li&gt;Idempotent writes&lt;/li&gt;
&lt;li&gt;A per-user SHA-256 hash chain&lt;/li&gt;
&lt;li&gt;Client-side tamper verification&lt;/li&gt;
&lt;li&gt;Credential-free Vercel OIDC authentication&lt;/li&gt;
&lt;li&gt;Region-scoped Aurora DSQL tokens&lt;/li&gt;
&lt;li&gt;Live cross-region state streaming with SSE&lt;/li&gt;
&lt;li&gt;SD-JWT age-bracket selective disclosure&lt;/li&gt;
&lt;li&gt;A least-privilege public database role&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The age proof uses selective disclosure under RFC 9901. It is not a complete zero-knowledge proof, and I do not claim that it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Custody currently has &lt;strong&gt;88 tests&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;83 unit tests&lt;/li&gt;
&lt;li&gt;5 tests that run against the live Aurora DSQL cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live-cluster tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-region read-after-commit verification&lt;/li&gt;
&lt;li&gt;Eight-way concurrent append testing&lt;/li&gt;
&lt;li&gt;Hash-chain integrity checks&lt;/li&gt;
&lt;li&gt;Idempotency verification&lt;/li&gt;
&lt;li&gt;Optimistic-concurrency retry behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technology stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; Amazon Aurora DSQL, multi-region and active-active&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend and server:&lt;/strong&gt; Next.js 16 and React 19&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; Vercel OIDC and AWS IAM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting:&lt;/strong&gt; Upstash Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail:&lt;/strong&gt; SHA-256 hash chains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selective disclosure:&lt;/strong&gt; SD-JWT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live regional updates:&lt;/strong&gt; Server-Sent Events&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try Custody
&lt;/h2&gt;

&lt;p&gt;Custody demonstrates how a globally distributed application can apply parental consent and spending-control decisions consistently across regions while maintaining a verifiable audit trail.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://custody-zeta.vercel.app" rel="noopener noreferrer"&gt;https://custody-zeta.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built with Amazon Aurora DSQL and Vercel for the H0 Hackathon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#H0Hackathon&lt;/strong&gt; &lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>distributedsystems</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
