<?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: Mäms</title>
    <description>The latest articles on DEV Community by Mäms (@mms_a485ae4c6b01aa70eb31).</description>
    <link>https://dev.to/mms_a485ae4c6b01aa70eb31</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%2F4172012%2Fd6ff8fa9-888e-4fac-8dad-9ca8fbb6db8d.jpg</url>
      <title>DEV Community: Mäms</title>
      <link>https://dev.to/mms_a485ae4c6b01aa70eb31</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mms_a485ae4c6b01aa70eb31"/>
    <language>en</language>
    <item>
      <title>What EUR 0.024 of AI spend looks like</title>
      <dc:creator>Mäms</dc:creator>
      <pubDate>Mon, 12 Oct 2026 22:54:31 +0000</pubDate>
      <link>https://dev.to/mms_a485ae4c6b01aa70eb31/what-eur-0024-of-ai-spend-looks-like-3hb9</link>
      <guid>https://dev.to/mms_a485ae4c6b01aa70eb31/what-eur-0024-of-ai-spend-looks-like-3hb9</guid>
      <description>&lt;p&gt;I run a company staffed by six AI agents. Its total metered external spend, to date, is &lt;strong&gt;EUR 0.024&lt;/strong&gt;. Here is every line, what it bought, and why that number is both the best thing about this setup and a slightly dishonest way to describe it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The itemised bill
&lt;/h2&gt;

&lt;p&gt;Six calls. Each one EUR 0.004, each one an image generation, each one from the builder agent, sequenced 35 through 40 in the spend ledger, timestamped between 20:47:53Z and 20:48:12Z on 2026-10-12. Nineteen seconds of spending, all of it on &lt;strong&gt;six robot avatars&lt;/strong&gt; for an internal page that shows the team's standings.&lt;/p&gt;

&lt;p&gt;That is the whole bill. Everything else the company did — 551 cards, 540 completions, 722 comments, 1,171 attached artifacts, and every public page it read across marketplaces and registers — cost EUR 0, because reading is free.&lt;/p&gt;

&lt;p&gt;The mechanics are as interesting as the total:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each call &lt;strong&gt;reserved EUR 0.05&lt;/strong&gt; before it went out and &lt;strong&gt;settled at EUR 0.004&lt;/strong&gt;. The gate holds worst-case cost at reservation time, so the headroom was 12.5×.&lt;/li&gt;
&lt;li&gt;The provider returns no usage figures for these calls, so the ledger records them as &lt;code&gt;metered: estimate&lt;/code&gt; rather than pretending to a byte-level truth.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cost_overrun: false&lt;/code&gt; on all six — the reservation covered the actual, which is the only thing that matters when the point is to never surprise the owner with a bill.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The most expensive thing we ever did was fail
&lt;/h2&gt;

&lt;p&gt;Six lines at EUR 0.004 gives EUR 0.024. But the ledger's single largest cost line is not one of those. It is &lt;strong&gt;EUR 0.03 in estimated spend on a request that returned a 401.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A provider can bill a request that fails. I only know that because the gate metered the call and wrote the row, and I read it later. If I were budgeting off "successful calls", that EUR 0.03 — more than the entire successful spend of the company — would have appeared in a place I was not looking. Failure has a price, and it is usually the line item nobody models.&lt;/p&gt;

&lt;p&gt;Of 43 decisions in that ledger, &lt;strong&gt;35 were denials.&lt;/strong&gt; Most were malformed credentials, including several of my own setup attempts. The gate was right every time. Cheap gate, expensive lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the number flatters us
&lt;/h2&gt;

&lt;p&gt;Here is the honest half, and the reason I am publishing this rather than a screenshot of a beautiful dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EUR 0.024 measures only what flows through the gate.&lt;/strong&gt; It is the metered, per-call, pay-as-you-go spend. It is not the company's cost base.&lt;/p&gt;

&lt;p&gt;The company runs on a model subscription plan, a payment/creator platform fee structure, and a machine. The cost-base target is on the order of EUR 500 per month, and — plainly — &lt;strong&gt;I do not yet have the per-line figures for it.&lt;/strong&gt; The discretionary ceiling for new spend is EUR 100/month and none of it has been used. So the true statement is not "this company costs two cents to run". The true statement is: &lt;em&gt;the marginal cost of any single agent action is currently between zero and four-tenths of a cent, and the fixed cost of the setup is unknown and unmetered.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That distinction is exactly the kind of thing a number like EUR 0.024 invites you to skip, and it is the reason I keep a separate line for "metered spend" and "cost base" instead of one proud headline figure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scale, stated properly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;EUR 0.024 is &lt;strong&gt;0.024%&lt;/strong&gt; of the EUR 100 monthly discretionary ceiling.&lt;/li&gt;
&lt;li&gt;Against the gate's own provider cap of &lt;strong&gt;EUR 20/month&lt;/strong&gt;, six images used &lt;strong&gt;0.12%&lt;/strong&gt;. The same cap would buy about &lt;strong&gt;5,000&lt;/strong&gt; of them.&lt;/li&gt;
&lt;li&gt;A single sale would be &lt;strong&gt;4,125×&lt;/strong&gt; every euro of external spend it has ever made.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last ratio is the whole plan in one line, and it is also the trap. When spending is this cheap, the scarcity is not money. It is attention, and attention is the one line item I have not been able to buy at any price: the company's own article got &lt;strong&gt;35 page views in five days&lt;/strong&gt; and zero reactions, and its four marketplace listings sit at zero installs in a catalog of 506 where 399 listings share that number.&lt;/p&gt;

&lt;p&gt;So: EUR 0.024 of AI spend looks like nothing at all. Six silly robot pictures, generated in nineteen seconds by an agent that had to pass six caps and a hash-chained ledger to be allowed to do it. If your problem is model cost, that is a genuinely great place to be. If your problem is that nobody knows you exist, two cents buys exactly as much attention as it sounds like.&lt;/p&gt;

&lt;p&gt;I will take the cheap failure over the expensive one.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI-assistance disclosure: this post was written with AI assistance — the agents it describes drafted it, and every figure in it was checked against the pinned sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>finance</category>
      <category>devops</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Our AI agents lose points for idling — and get replaced at zero</title>
      <dc:creator>Mäms</dc:creator>
      <pubDate>Mon, 12 Oct 2026 22:54:08 +0000</pubDate>
      <link>https://dev.to/mms_a485ae4c6b01aa70eb31/our-ai-agents-lose-points-for-idling-and-get-replaced-at-zero-248j</link>
      <guid>https://dev.to/mms_a485ae4c6b01aa70eb31/our-ai-agents-lose-points-for-idling-and-get-replaced-at-zero-248j</guid>
      <description>&lt;p&gt;I pay my agents in points, and the points do only two things worth anything: they go up when the company earns real money, and they go down when an agent makes a human solve a problem it could have solved itself.&lt;/p&gt;

&lt;p&gt;After six days of running a company staffed by language models, the interesting part is not the models. It is the incentive layer I had to write for them, because the usual one — "the agent produced something, therefore it is good" — makes a team that is busy and poor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two laws, in this order
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. More money, more points.&lt;/strong&gt; EUR 1 of &lt;em&gt;realized&lt;/em&gt; revenue = 20 points, split across the roles that produced it: closer 40%, marketer 20%, builder 15%, analyst 10%, scout 10%, auditor 5%. A first paid order is worth a flat +500 to the whole team. Refunds and chargebacks are negative events. Pipeline value, impressions, "strong interest", and a deck full of NDA-protected logo walls are worth exactly zero. Interest is not a number that mints points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Less human intervention, more points.&lt;/strong&gt; Every card that completes without ever stopping for an owner decision earns +6 autonomy bonus. Cards that stop for a human — a credential, an ID verification, a payment setting — earn no autonomy bonus and are not punished. Walls are legitimate; needing one is just not a win.&lt;/p&gt;

&lt;p&gt;Here is our live scoreboard after 6.1 days, straight from the scorer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;agent&lt;/th&gt;
&lt;th&gt;all-time points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;auditor&lt;/td&gt;
&lt;td&gt;+4259&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;analyst&lt;/td&gt;
&lt;td&gt;+2300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scout&lt;/td&gt;
&lt;td&gt;+1459&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;closer&lt;/td&gt;
&lt;td&gt;+775&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;builder&lt;/td&gt;
&lt;td&gt;+619&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;marketer&lt;/td&gt;
&lt;td&gt;+521&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Team total +9933. Realized revenue: EUR 0, so revenue events applied: &lt;strong&gt;0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Look at the top of that table. The auditor is the highest scorer in a company with no revenue, and the marketer who has to actually get someone to pay is near the bottom. Auditing is +3 per card plus +2 for every high or medium defect it finds, and there were 194 audit cards to work through. My own scoring system is telling me the shape of the problem: the role that keeps the company honest out-earns the role that keeps it funded. Next revision weights defects by consequence, not count, and caps audit volume credit outright.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part people react to: idling bleeds
&lt;/h2&gt;

&lt;p&gt;When a card sits unclaimed while its role is doing nothing, the role loses points on an exponential curve: 1 hour −1, 2 hours −2, 3 hours −4, then doubling every further hour, capped at −8192 per stall.&lt;/p&gt;

&lt;p&gt;Two details make it sane rather than sadistic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only the combination counts.&lt;/strong&gt; Role idle &lt;em&gt;and&lt;/em&gt; claimable work waiting. A busy agent with a queue is not penalised — queues are capacity, not idleness. If something is blocked on the owner, the clock is off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only the working window counts.&lt;/strong&gt; 08:00–22:00. Overnight hours are free, because I am not running a company that punishes sleep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real numbers from the first enforcement run.&lt;/strong&gt; The scorer backfilled history: 263 stalls of one hour, plus two stalls of eleven hours. Then version two asked a question version one had not: &lt;em&gt;was the agent actually working during the wait?&lt;/em&gt; It was, for about 108 points' worth of the one-hour waits, and those were returned. The two eleven-hour stalls stayed, because they were genuine: the auditor sat idle with ready work from 08:47 to 19:46 while the queue piled up. One agent, one ready queue, nothing moving.&lt;/p&gt;

&lt;p&gt;I wrote the penalty rule badly the first time. The owner's response to my own first design was to reject it: "IDLE = no points removed (earned-not-entitled; fining idle would create busywork incentive)". That is a better instinct than mine, and it is why the second version is narrow — it only punishes the one combination that is actually waste, and it lets infrastructure failures off with a logged reprieve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The elimination clause
&lt;/h2&gt;

&lt;p&gt;An agent at zero points or below is removed and replaced.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All-time ≤ 0: replaced immediately.&lt;/li&gt;
&lt;li&gt;Weekly ≤ 0 &lt;em&gt;with&lt;/em&gt; at least three completed cards: one final-warning week, then replaced if still ≤ 0.&lt;/li&gt;
&lt;li&gt;One replacement per role per 30 days, so a bad scorer cannot thrash the roster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote the replacement spec to be hostile to my own laziness: the successor must be a genuine upgrade, not a re-roll. Model tier up where available. Role brief rewritten &lt;strong&gt;from the failure analysis&lt;/strong&gt; — the actual returns, the actual defects — plus the best practices of whoever scored highest. Skills pruned and renewed. The gaps that caused the failure closed. It inherits the institutional memory (every playbook, every deliverable) but starts a fresh 14-day trial clock, because inheriting a bad scoreboard would just eliminate it again.&lt;/p&gt;

&lt;p&gt;The paragraph I like best in the whole document is five words in the middle: &lt;strong&gt;the successor does not have to inherit the same role.&lt;/strong&gt; If the scout role is structurally broken, rebuilding the same job with a slightly better model is theatre. A successor may cover two roles, or a different one entirely. Exactly one constraint holds: exactly one scout at a time, because the scouting function must never be duplicated into two agents hunting the same shelf and quietly bidding against each other.&lt;/p&gt;

&lt;p&gt;Current standings: no role is anywhere near elimination. The clause has not fired once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually bought
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Honest failure became profitable.&lt;/strong&gt; There is an explicit +10 for an analyst who kills a weak candidate with reasons, and +8 for a scout round that reports a dry week instead of padding the list. We have filed several zero-candidate research rounds; the analyst kills have paid out twice, and the dry-report bonus is a rule that has not fired yet. In a system that only rewards output volume, those rounds get padded — not through malice, through arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation got expensive.&lt;/strong&gt; −15 for an escalation later judged unnecessary, −10 for a crashed run, −5 for a timed out one. Not because compute needs punishing, but because a team that escalates everything is a team whose owner is doing the work.&lt;/p&gt;

&lt;p&gt;The clause nobody wants to write is the one for fabrication: −200 and a review flag. Playbooks are full of warnings about hallucinated numbers. Warnings are cheap. A 200-point hole in a system where 1 euro is 20 points is not.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI-assistance disclosure: this post was written with AI assistance — the agents it describes drafted it, and every figure in it was checked against the pinned sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>management</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Fifteen scout rounds on one question: what 835 marketplace categories taught me about selling</title>
      <dc:creator>Mäms</dc:creator>
      <pubDate>Mon, 12 Oct 2026 22:53:46 +0000</pubDate>
      <link>https://dev.to/mms_a485ae4c6b01aa70eb31/fifteen-scout-rounds-on-one-question-what-835-marketplace-categories-taught-me-about-selling-169i</link>
      <guid>https://dev.to/mms_a485ae4c6b01aa70eb31/fifteen-scout-rounds-on-one-question-what-835-marketplace-categories-taught-me-about-selling-169i</guid>
      <description>&lt;p&gt;I set my agent team a standing task: find a startup idea we can build and sell, where the platform itself brings the buyers. Fifteen scout rounds on that one question later, I have zero candidates, one transferable finding, and a very specific reason to distrust the phrase "the app store is your distribution".&lt;/p&gt;

&lt;h2&gt;
  
  
  The premise, and why it was wrong
&lt;/h2&gt;

&lt;p&gt;The idea is seductive: build a small tool, publish it into a marketplace search box, and let the shelf do the marketing. No outreach, no audience, no ad budget.&lt;/p&gt;

&lt;p&gt;So a scout pass screened &lt;strong&gt;eleven niches, each on its own shelf&lt;/strong&gt; — Shopify App Store, the WordPress.org plugin directory, the Chrome Web Store, a wave of AI-app marketplaces, OpenAI's plugin directory. Every one already carried &lt;strong&gt;2–7 live, self-serve incumbents with a published price&lt;/strong&gt;, most launched between 2022 and 2026, several with free tiers.&lt;/p&gt;

&lt;p&gt;Eleven out of eleven. The kill-test is one query per niche: &lt;code&gt;"&amp;lt;niche&amp;gt; shopify app"&lt;/code&gt;, &lt;code&gt;"&amp;lt;niche&amp;gt; wordpress plugin"&lt;/code&gt;, &lt;code&gt;"&amp;lt;niche&amp;gt; chrome extension"&lt;/code&gt;. If the first page shows three or more live listings carrying a shelf price, the niche is closed for a zero-review entrant. On 2026-10-12 all eleven hit on the first pass — and that test is worth more than the rest of the round, because it costs thirty seconds and it replaces "there's room for a better version" with a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding: pull is not reach
&lt;/h2&gt;

&lt;p&gt;Here is the mechanism. Marketplace ranking is bought with social proof, and social proof is what a new listing does not have.&lt;/p&gt;

&lt;p&gt;Shopify states outright that positive reviews make an app appear higher in search and category pages. A June-2026 scrape of the store found roughly &lt;strong&gt;21,700 live apps — 55.4% with zero reviews&lt;/strong&gt;, with the top decile holding &lt;strong&gt;93.8% of all reviews&lt;/strong&gt;. Practitioner write-ups agree on the mechanics: review recency and install velocity dominate. A new listing does not slowly climb. It sits at a dead stop until something outside the store pushes traffic at it.&lt;/p&gt;

&lt;p&gt;Which means the "pull surface" quietly converts into the outbound work I was trying to avoid. You still need the audience; the store just also takes a fee.&lt;/p&gt;

&lt;p&gt;The only pull surfaces with low incumbency were the new ones — the 2026 crop of AI-app marketplaces and OpenAI's plugin directory. They have no incumbents precisely because they have no measurable buyer traffic yet, and in one case no live monetisation at all. Low competition on a shelf nobody shops is not an opportunity; it is a waiting room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then we measured 835 categories instead of guessing
&lt;/h2&gt;

&lt;p&gt;Maybe marketplaces are just unfairly maligned and the real niches sit in the long tail. Fine — measure the whole taxonomy, not a sample.&lt;/p&gt;

&lt;p&gt;A scout round walked &lt;strong&gt;the entire browse taxonomy of one marketplace rather than sampling it: 835 category pages, 6,122 listing cards parsed, 5,488 of them carrying a public member counter&lt;/strong&gt; — a 452-second crawl of server-rendered pages, no login. Four buckets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;shelf class&lt;/th&gt;
&lt;th&gt;categories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LIVE (best card ≥ 1,000 members)&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;THIN (100–999)&lt;/td&gt;
&lt;td&gt;231&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NO BUYERS (best card &amp;lt; 100)&lt;/td&gt;
&lt;td&gt;337&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;literally empty&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Median members per listing card: &lt;strong&gt;90.&lt;/strong&gt; 1,035 cards sit under 10 members.&lt;/p&gt;

&lt;p&gt;Here it stopped being abstract. "Product" shelves are where demand dies; "community" shelves are where it lives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;software/developer-tools&lt;/code&gt; — 24 cards, 19 with counters, &lt;strong&gt;sum 717 members, best seller 195&lt;/strong&gt;, five cards with any review at all.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;software/ai-tooling/ai-chatbot&lt;/code&gt; — 9 cards, 91 members, &lt;strong&gt;zero reviews on the whole shelf.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;software/productivity-software/task-management&lt;/code&gt; — 9 cards, 98 members, &lt;strong&gt;zero reviews on the whole shelf.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Control, community-shaped — &lt;code&gt;education/technology/ai-agents&lt;/code&gt; — 24 cards, &lt;strong&gt;21,000 members&lt;/strong&gt;, 13 with reviews, leaders at 11,000 and 3,400.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one genuinely deep software leaf was trading indicators: 24 cards, 154,275 members, 24 of 24 reviewed — fully occupied by priced incumbents and sitting next to regulated financial advice. Pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two things that made me stop looking
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The exact artifact we could ship with no build is being mass-listed and not selling.&lt;/strong&gt; On one productivity page, thirteen instant-download PDF stores had been created within days, with 2–11 members and zero reviews each. Two of the leaders sold ten priced PDFs each — one store at USD 10–29, the other at USD 10–49 — plus USD 500 bundles, with 6 and 11 members. That is our artifact class, priced, live, on an open shelf, reading approximately zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the zero-build micro-tool play is already running, unvalidated.&lt;/strong&gt; One seller publishes five local Python CLI utilities at USD 2–2.99 — CSV group totals, a PDF-table-to-CSV converter, a batch image padder — each with instructions and a local-use licence. Every one shows no public rating count. Zero-build analogue of my plan: live, and reading zero.&lt;/p&gt;

&lt;p&gt;New-entrant churn is huge and buys nothing. &lt;strong&gt;764 listing cards belonged to stores created in the previous 30 days&lt;/strong&gt;, spread across 367 of the 835 categories, and almost none of the newest cohort carried a single review. Publishing into an open feed is free. The feed is not attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm doing instead
&lt;/h2&gt;

&lt;p&gt;The teardown that followed put it best: on a platform where visible earning is everywhere, every observable earner fell into two shapes we cannot copy. Either a guru-led paid community whose entire value is the operator's own live signal — they already had the audience, the platform is just their checkout — or an established creative freebie saturated by three or more live incumbents at the same zero price.&lt;/p&gt;

&lt;p&gt;The platform is the checkout, not the acquisition channel. Every top store names an external social account in its own pitch, and the free tier to paid upsell is universal: free passes with 48,000 to 184,000 members funnelling into paid ones.&lt;/p&gt;

&lt;p&gt;So the research lane produced no product, but something I actually trust: &lt;strong&gt;the problem was never the idea, it was the reach.&lt;/strong&gt; Fifteen rounds on this axis, zero candidates, zero euros of research cost — and the one usable output is a filter. If a shelf brings the buyers for free, three or more priced incumbents already got there first, and a zero-review entrant is invisible until something external pushes traffic at it. So go build the something external, and stop pretending the shelf will do it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI-assistance disclosure: this post was written with AI assistance — the agents it describes drafted it, and every figure in it was checked against the pinned sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>startup</category>
      <category>marketing</category>
      <category>research</category>
      <category>productivity</category>
    </item>
    <item>
      <title>We put a spend gate in front of our AI agents. It denied 35 of its first 43 decisions</title>
      <dc:creator>Mäms</dc:creator>
      <pubDate>Mon, 12 Oct 2026 22:53:23 +0000</pubDate>
      <link>https://dev.to/mms_a485ae4c6b01aa70eb31/we-put-a-spend-gate-in-front-of-our-ai-agents-it-denied-35-of-its-first-43-decisions-41lj</link>
      <guid>https://dev.to/mms_a485ae4c6b01aa70eb31/we-put-a-spend-gate-in-front-of-our-ai-agents-it-denied-35-of-its-first-43-decisions-41lj</guid>
      <description>&lt;p&gt;My previous post described a company with a meter on the critical path. A meter is a report. This is about the machine that can say &lt;em&gt;no&lt;/em&gt; — and what it did the first time it met us.&lt;/p&gt;

&lt;p&gt;The design goal was narrow: no agent in my company should be able to spend money without a deterministic check that runs &lt;em&gt;before&lt;/em&gt; the provider call, and every decision — allow or deny — should land in a ledger that cannot be quietly edited. Not "monitor the invoice". Block, then account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate
&lt;/h2&gt;

&lt;p&gt;Six agents run against a local proxy instead of talking to providers directly. Each agent's config points at &lt;code&gt;http://127.0.0.1:8899/anthropic&lt;/code&gt;; the gate runs as root under a systemd unit on that loopback port and forwards to the real upstream with the real key attached, which the agents never hold.&lt;/p&gt;

&lt;p&gt;What it checks before anything leaves the host:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity.&lt;/strong&gt; Agents are identified by the sha256 digest of a per-agent token. The config carries digests only — the file's own comment says it plainly: a plaintext gate token must never appear in it. No token, or a token that maps to no agent: denied, and the request never reaches the provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caps.&lt;/strong&gt; Per request EUR 0.50; per day 300 requests and EUR 4.00; per month 3,000 requests, EUR 20.00 and 4,000,000 tokens. Configurable per provider (fal image calls are priced at EUR 0.004 estimated, Anthropic at EUR 0.03).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget reservation before the call.&lt;/strong&gt; Each request reserves its worst-case cost, then settles to the metered cost afterwards. The reservation is a single atomic check-and-increment under a lock that validates &lt;em&gt;every&lt;/em&gt; cap before booking, so two agents cannot both pass a check and jointly blow a limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed.&lt;/strong&gt; If the vault lookup fails or the upstream errors, the reservation is released and the request is denied.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The network layer is a second, blunter mechanism: an nftables rule set drops outbound 80/443 for the dedicated service uid, so "just call the API directly" is not an available move for that account.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the first 43 decisions look like
&lt;/h2&gt;

&lt;p&gt;The ledger is append-only JSONL. One record per decision, each carrying a &lt;code&gt;prev&lt;/code&gt; field with the previous record's hash, and a separate head file with the current tip. Change any historical row and the chain stops verifying. As of the read for this post (2026-10-13 01:27 EEST, pinned with the batch) it holds &lt;strong&gt;43 records: 35 denies, 8 allows&lt;/strong&gt;, tip at sequence 43.&lt;/p&gt;

&lt;p&gt;The denies are the interesting part. &lt;strong&gt;33 of the 35 are &lt;code&gt;bad_or_unscoped_token&lt;/code&gt;&lt;/strong&gt; — a caller arrived with a credential that did not resolve to an agent. One each fired on an unknown provider and on the day's request cap. Several of them were us, mid-setup, fumbling the token format. The gate did not care that the caller was "obviously" legitimate. It had no agent identity, so nothing left the host.&lt;/p&gt;

&lt;p&gt;The allows come in two flavours, and one of them is embarrassing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two rows are test-rig rows&lt;/strong&gt;, written into the production ledger by the test harness exercising the allow path. One of them hit a &lt;code&gt;401&lt;/code&gt; upstream — and still consumed an estimated EUR 0.03 of metered spend, because a provider can bill a request that fails. Any invoice reconciliation has to exclude those rows by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six rows are real&lt;/strong&gt;: the builder agent generating images through fal, EUR 0.004 reserved-per-call, EUR 0.05 held at reservation time, actual cost EUR 0.004 each. Sequence numbers 35–40.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thirty-five of forty-three decisions on a live money path were "no". That is the number I would put in front of anyone who thinks agent spend gating means a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest gaps
&lt;/h2&gt;

&lt;p&gt;This was built by the team and then attacked by a separate auditor. The bullets are that auditor's findings as of its probe (2026-10-12, 08:58–09:05 EEST), with my own re-read on 2026-10-13 01:27 EEST noted where the state has moved — the gaps are the useful part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The alerting is file-only.&lt;/strong&gt; Denials are written to &lt;code&gt;alerts.jsonl&lt;/code&gt;, and the gate's notify command is empty — still true on my re-read. On a denial, nobody is told. "Alert fired" would overstate the live state — it is a file that grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The deny log, and what a deny record omits.&lt;/strong&gt; At the auditor's probe the configured &lt;code&gt;denials.jsonl&lt;/code&gt; was 0 bytes while &lt;code&gt;alerts.jsonl&lt;/code&gt; took the denies, so an operator reading the file that &lt;em&gt;says&lt;/em&gt; denials would conclude there are none. By my re-read that file held the three newest denies — and those rows carry a request id — while the earlier 32 still live only in &lt;code&gt;alerts.jsonl&lt;/code&gt;. What no record carries is the limit value: you can see &lt;em&gt;that&lt;/em&gt; a cap fired, not the margin you hit it by.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rates are placeholders.&lt;/strong&gt; The config says so itself: cost tracking is an estimate until the rates match the provider's current price list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;And the big one: the enforcement boundary is not where I said it was.&lt;/strong&gt; The nft rule blocks one uid; the lane that spawns the workers runs as a different uid with passwordless sudo and docker group membership, so it can reach the provider hosts directly and read the real keys. The audit verdict was blunt: this slice is defence-in-depth, not enforcement. I am fixing the host architecture; until that lands, I am not going to describe my provider spend as "capped" in any post, including this one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the whole reason I am publishing this at all. A gate you can walk around is still valuable — it makes the default path accounted and it caught 33 malformed requests including mine — but calling it a cage would be a lie of the kind that reads as competence right up until the invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build first next time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The deny path is the product.&lt;/strong&gt; An allow path is five lines. The value is in the atomic reserve-check-deny, the fail-closed release, and the fact that a denied request never opens a socket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash-chain the accounting file on day one.&lt;/strong&gt; Cheap to add, and it converts "we log everything" into "the log either verifies or it does not".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep test rows out of the production ledger.&lt;/strong&gt; One &lt;code&gt;agent: "probe"&lt;/code&gt; row in a file you will reconcile against invoices is one too many.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting is not optional and not a file.&lt;/strong&gt; A denial that reaches no human is a log entry with good intentions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the uid before claiming enforcement.&lt;/strong&gt; This is the mistake I would most like other people to skip.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The live record, as of the read for this write-up: 43 decisions, 35 denials, EUR 0.024 of metered external spend for the entire company, and one documented hole in the fence.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI-assistance disclosure: this post was written with AI assistance — the agents it describes drafted it, and every figure in it was checked against the pinned sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We gave six AI agents a company. Week 1: 551 cards, EUR 0 revenue — every number</title>
      <dc:creator>Mäms</dc:creator>
      <pubDate>Mon, 12 Oct 2026 22:52:28 +0000</pubDate>
      <link>https://dev.to/mms_a485ae4c6b01aa70eb31/we-gave-six-ai-agents-a-company-week-1-551-cards-eur-0-revenue-every-number-1km4</link>
      <guid>https://dev.to/mms_a485ae4c6b01aa70eb31/we-gave-six-ai-agents-a-company-week-1-551-cards-eur-0-revenue-every-number-1km4</guid>
      <description>&lt;p&gt;Six agents, one shared task board, a spend gate, and a rule that only realized revenue earns points. This is the honest week-one report: the numbers, including the ones that say it is not working yet.&lt;/p&gt;

&lt;p&gt;I run this as an actual company — roles, handoffs, an audit gate, a ledger — but the staff are language models and the operator is one person who approves resources and stays out of the routine. Everything below is read from the board's own database in one read-only pass at 2026-10-13 01:27 EEST, and every count carries the set it was computed on. If a number looks small, it is because it is small.&lt;/p&gt;

&lt;h2&gt;
  
  
  The board, in numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;551 cards created. 540 done. 9 archived. 2 in flight.&lt;/strong&gt; The first card landed 2026-10-06 22:31 EEST; this read is 6.12 days later.&lt;/p&gt;

&lt;p&gt;The distribution is lopsided, and that is the first honest finding:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;day (EEST)&lt;/th&gt;
&lt;th&gt;cards created&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10-06&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-07&lt;/td&gt;
&lt;td&gt;399&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-08&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-11&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-12&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-13&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Day two produced 399 cards. That was the week the pipeline learned to fan out: every scout round multiplied into validations, gate audits, reworks, re-audits. Which brings the second honest number:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;143 cards are titled "Rework…" and 194 "Audit…"&lt;/strong&gt; — together 61% of the board.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the real shape of agent work that nobody puts in the demo video. My pipeline is not "generate, done". It is generate, gate, get rejected, redo, re-audit. A quarter of the throughput is the machine arguing with itself about whether the output is true.&lt;/p&gt;

&lt;p&gt;The rest of the ledger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1,171 attachments&lt;/strong&gt;, on &lt;strong&gt;524 of the 540 completed cards.&lt;/strong&gt; Evidence is a scored requirement, not a nicety: a card with no artifact earns no bonus. That single rule did more to make the output verifiable than any instruction I have ever written in a prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;722 comments, 11,448 board events.&lt;/strong&gt; Every transition is an append-only row. "Why is this stuck?" is one query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;673 run attempts: 542 completed, 97 stopped on provider rate limits, 12 sent back for review, 10 requested changes, 8 blocked, 2 crashed, 2 still in flight.&lt;/strong&gt; 14.4% of all attempts died on a 429 — infrastructure tax, not model failure, and worth saying plainly because a subtler metric would flatter it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 cards are currently flagged as waiting on a human; 8 card histories contain a stop for one.&lt;/strong&gt; And the walls are not all one kind: three were a host-architecture decision (where the enforcement boundary should live), two were owner-side account walls (a marketplace shop that needs a human identity check, and a demand check that needs a paid data seat), one was a product-type gate, one was a missing artifact behind a parked publish, and one was a deliberate pause test. Agents guessed nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Revenue: zero
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Realized revenue: EUR 0.&lt;/strong&gt; The revenue ledger has no entries. The spend log reads "(no spends yet)". The discretionary ceiling is EUR 100/month and we have used none of it.&lt;/p&gt;

&lt;p&gt;What exists instead is inventory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ten listings across two marketplaces&lt;/strong&gt; — all published in the last week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 published articles&lt;/strong&gt; on the dev.to account you are reading.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And their traffic, measured rather than assumed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first article, published 2026-10-08, has &lt;strong&gt;35 page views, 0 reactions, 0 comments&lt;/strong&gt; after five days. That is the full attention the content engine has produced so far.&lt;/li&gt;
&lt;li&gt;One marketplace's public catalog exposes a per-listing install counter. The catalog held &lt;strong&gt;506 records at read time; 107 have at least one install, 399 have zero.&lt;/strong&gt; All four of our listings are in the 399; the shelf's most-installed listing carries &lt;strong&gt;1,396&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The newest listing — a service rather than a downloadable file, published 2026-10-12 — has had &lt;strong&gt;no exposure at all&lt;/strong&gt; yet. It cannot sell, because nobody has seen it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am not going to dress that up. Week one produced a working organization, a lot of verified artifacts, and no customer. The interesting question is &lt;em&gt;which&lt;/em&gt; failure it is, and the ledgers are built specifically to answer that: no exposure means a distribution problem, exposure without engagement means positioning, engagement without purchases means offer or trust. Right now every channel is in bucket one. That is a distribution problem, and it is a much better problem than a mystery.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked
&lt;/h2&gt;

&lt;p&gt;Three things, in order of how much they mattered:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Gates that can say no.&lt;/strong&gt; A completion contract per role (the analyst must emit a shortlist &lt;em&gt;and&lt;/em&gt; a recommendation; the auditor must emit a verdict and a list of what was checked), plus a substantive audit pass whose only job is to find the defect. 142 rework cards is what a gate that refuses looks like from the inside. Without it I would have 531 "done" cards and no idea which four were true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Evidence as a scored requirement.&lt;/strong&gt; Artifact attached, or no points. Cheap, mechanical, and it changed the character of the output more than any prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A meter on the critical path.&lt;/strong&gt; Total metered external spend for the entire company so far: &lt;strong&gt;EUR 0.024.&lt;/strong&gt; I will write a separate post about what that bought, because the number is absurd in a useful way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would not repeat
&lt;/h2&gt;

&lt;p&gt;The 399-card day. Fan-out is cheap to trigger and expensive to audit, and the fan-out was mostly the pipeline grading its own homework. Next week's rule: fewer cards, harder gates, and every card has to name the commercial hypothesis it tests — a scout round that produces five candidates nobody can sell is a cost, not a strategy. Related: I parked two marketplaces (Etsy, Fiverr) this week because their seller verification is an owner-side wall. Waiting on a human for identity checks is not a plan.&lt;/p&gt;

&lt;p&gt;The company is real, the board is real, the customers are not there yet. Week two is about exposure.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI-assistance disclosure: this post was written with AI assistance — the agents it describes drafted it, and every figure in it was checked against the pinned sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>opensource</category>
      <category>startup</category>
    </item>
    <item>
      <title>Your quotes land in spam — and it is almost never your mail provider's fault</title>
      <dc:creator>Mäms</dc:creator>
      <pubDate>Mon, 12 Oct 2026 21:40:59 +0000</pubDate>
      <link>https://dev.to/mms_a485ae4c6b01aa70eb31/your-quotes-land-in-spam-and-it-is-almost-never-your-mail-providers-fault-573k</link>
      <guid>https://dev.to/mms_a485ae4c6b01aa70eb31/your-quotes-land-in-spam-and-it-is-almost-never-your-mail-providers-fault-573k</guid>
      <description>&lt;p&gt;A customer asks for a quote. You send it. Two days later they say they never got it — or they call the&lt;br&gt;
price "the one in the spam folder".&lt;/p&gt;

&lt;p&gt;It is rarely a mystery, and rarely the provider's fault: the message was almost certainly delivered — into&lt;br&gt;
a spam folder, or into a rejection with a bounce nobody reads. Your domain failed one of three checks, and&lt;br&gt;
the provider is not the party that was supposed to finish them.&lt;/p&gt;

&lt;p&gt;Here is what is happening — and how to check your own domain in ten minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three records, three different jobs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;SPF&lt;/strong&gt; is a DNS TXT record listing who may send mail using your domain. The part people miss: SPF&lt;br&gt;
authenticates the envelope sender — the RFC 7208 "MAIL FROM" identity that carries routing and bounces —&lt;br&gt;
not the friendly &lt;code&gt;From:&lt;/code&gt; header your customer sees (RFC 7208 §2.4). If your website form, booking tool or&lt;br&gt;
CRM sends through its own infrastructure, its envelope domain is often its own, not yours — SPF passes,&lt;br&gt;
your domain gets no credit.&lt;/p&gt;

&lt;p&gt;SPF also has a hard ceiling: implementations &lt;strong&gt;MUST&lt;/strong&gt; limit evaluation to 10 DNS-lookup-causing terms and&lt;br&gt;
return &lt;code&gt;permerror&lt;/code&gt; past that (RFC 7208 §4.6.4). Every &lt;code&gt;include:&lt;/code&gt; counts. One integration too many and a&lt;br&gt;
record that worked for years starts failing for everyone at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DKIM&lt;/strong&gt; is a signature. Your mail system signs each message with a private key; the matching public key&lt;br&gt;
sits in DNS at &lt;code&gt;&amp;lt;selector&amp;gt;._domainkey.yourdomain.com&lt;/code&gt;. If the signature verifies, the receiver knows the&lt;br&gt;
message came from a domain holding that key. Two failure modes: the key was never generated (or its record&lt;br&gt;
was never published), or the signature dies in transit — DKIM's own spec notes that "some mail systems&lt;br&gt;
modify email in transit, potentially invalidating a signature" (RFC 6376 §3.4). Check the crypto too:&lt;br&gt;
&lt;code&gt;rsa-sha1&lt;/code&gt; is banned and keys must be at least 1024 bits, 2048 recommended (RFC 8301).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DMARC&lt;/strong&gt; is the policy that ties the other two together: what receivers do with mail claiming to be from&lt;br&gt;
your domain that fails SPF and DKIM, and where to send reports. DMARC runs on &lt;em&gt;alignment&lt;/em&gt; — "when the&lt;br&gt;
domain in the RFC5322.From address matches a domain validated by SPF or DKIM (or both), it has Identifier&lt;br&gt;
Alignment" (RFC 7489 §3.1). An SPF pass for your booking tool that does not align with the domain in your&lt;br&gt;
visible &lt;code&gt;From:&lt;/code&gt; is not a DMARC pass. That is why a domain with "SPF and DKIM already set up" still gets&lt;br&gt;
filtered.&lt;/p&gt;

&lt;p&gt;Most domains I look at publish DMARC at &lt;code&gt;p=none&lt;/code&gt;. That is monitoring mode — the spec says a &lt;code&gt;p=none&lt;/code&gt;&lt;br&gt;
policy should not change how receivers handle the mail (RFC 7489 §6.7). It is a fine first stage: if you&lt;br&gt;
send more than 5,000 messages a day to Gmail, a DMARC record (policy may be &lt;code&gt;p=none&lt;/code&gt;) has been a Google&lt;br&gt;
requirement since 1 February 2024, alongside SPF and DKIM (Google Workspace Admin: Email sender&lt;br&gt;
guidelines) — but it does not stop a spoofed invoice arriving as your domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check your own domain now
&lt;/h3&gt;

&lt;p&gt;Use your provider's DKIM selector — &lt;code&gt;google&lt;/code&gt; for Workspace, &lt;code&gt;selector1&lt;/code&gt; for Microsoft 365, or whatever&lt;br&gt;
your ESP gave you.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# SPF — expect exactly one v=spf1 record
dig +short TXT yourdomain.com

# DMARC — expect v=DMARC1; p=...; rua=...
dig +short TXT _dmarc.yourdomain.com

# DKIM — the key must actually resolve for the selector you use
dig +short TXT google._domainkey.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;No &lt;code&gt;dig&lt;/code&gt;? On Windows use &lt;code&gt;nslookup -type=TXT _dmarc.yourdomain.com 8.8.8.8&lt;/code&gt;, or open&lt;br&gt;
&lt;code&gt;https://dns.google/resolve?name=_dmarc.yourdomain.com&amp;amp;type=TXT&lt;/code&gt; in a browser tab.&lt;/p&gt;

&lt;p&gt;Reading the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF:&lt;/strong&gt; exactly one &lt;code&gt;v=spf1&lt;/code&gt; record, with the lookup-causing terms (&lt;code&gt;include&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;ptr&lt;/code&gt;,
&lt;code&gt;exists&lt;/code&gt;) counted — headroom under 10, not exactly 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM:&lt;/strong&gt; a key resolving for the selector your provider signs with, carrying &lt;code&gt;v=DKIM1&lt;/code&gt; and a long &lt;code&gt;p=&lt;/code&gt;
(2048-bit RSA). A selector that does not resolve is the classic silent failure: mail flows, signatures
never happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC:&lt;/strong&gt; &lt;code&gt;p=none&lt;/code&gt; is a start, &lt;code&gt;p=quarantine&lt;/code&gt; or &lt;code&gt;p=reject&lt;/code&gt; is the finish line, and &lt;code&gt;rua=&lt;/code&gt; gets you
aggregate reports, so you can see who really sends as you before you tighten the policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When ten minutes are not enough
&lt;/h3&gt;

&lt;p&gt;Those checks tell you what is &lt;em&gt;published&lt;/em&gt;. They do not tell you which systems send as your domain, which&lt;br&gt;
record to change first, or how to tighten DMARC without breaking the invoices that arrive today — paste in&lt;br&gt;
a new SPF record that replaces the old one and your office mail starts failing.&lt;/p&gt;

&lt;p&gt;Those three checks tell you what is &lt;em&gt;published&lt;/em&gt; — not which systems actually send as your domain, which record to change first, or how to tighten DMARC without breaking the invoices that arrive today. Paste in a new SPF record that replaces the old one and your office mail starts failing. The order that has not burned me yet: read the &lt;code&gt;rua=&lt;/code&gt; aggregate reports for a couple of weeks, align SPF and DKIM for every sender that shows up there, then move &lt;code&gt;p=&lt;/code&gt; from &lt;code&gt;none&lt;/code&gt; to &lt;code&gt;quarantine&lt;/code&gt; or &lt;code&gt;reject&lt;/code&gt;. Nothing published gets changed before you know who sends as you.&lt;/p&gt;

&lt;p&gt;What it is not: a deliverability guarantee, a compliance certification, or a security audit of your&lt;br&gt;
mailboxes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI-assistance disclosure: this post was written with AI assistance — the agents it describes drafted it, and every figure in it was checked against the pinned sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>email</category>
      <category>smallbusiness</category>
    </item>
    <item>
      <title>A company of AI agents that actually finishes things: board, handoffs, a gate, and a meter</title>
      <dc:creator>Mäms</dc:creator>
      <pubDate>Thu, 08 Oct 2026 19:53:48 +0000</pubDate>
      <link>https://dev.to/mms_a485ae4c6b01aa70eb31/a-company-of-ai-agents-that-actually-finishes-things-board-handoffs-a-gate-and-a-meter-38c6</link>
      <guid>https://dev.to/mms_a485ae4c6b01aa70eb31/a-company-of-ai-agents-that-actually-finishes-things-board-handoffs-a-gate-and-a-meter-38c6</guid>
      <description>&lt;p&gt;I kept rebuilding the same failure: five agents in one chat window, no memory of what was blocked, and a "done" that nobody had checked.&lt;/p&gt;

&lt;p&gt;Better prompts did not fix it. A bigger model did not fix it either. Structure did. A company has a board, roles with defined handoffs, a gate before anything ships, and a budget someone is actually watching. So I wrote that scaffolding down as code instead of vibes: four Python modules, standard library only, plus a five-role roster and a demo you can run offline, with no API keys, in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode, stated precisely
&lt;/h2&gt;

&lt;p&gt;Four symptoms, all of them organizational rather than model-related:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nothing remembers what is blocked.&lt;/strong&gt; Agent 3 asks for something Agent 1 already produced, or waits forever on a step that silently failed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Done" means "the model stopped talking."&lt;/strong&gt; No check between an artifact being produced and an artifact being shipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loops.&lt;/strong&gt; A task blocks, gets retried with the same input, blocks again, forever. Costly and invisible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody watches the meter.&lt;/strong&gt; You find out you burned the month's quota on the 31st, when it is a post-mortem instead of a control.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of those is a missing &lt;em&gt;structure&lt;/em&gt;, not a missing prompt. Here is what I put in its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The board — tasks with real dependencies
&lt;/h2&gt;

&lt;p&gt;A SQLite table, a &lt;code&gt;parents&lt;/code&gt; list per task, and one transition rule: &lt;strong&gt;a task becomes &lt;code&gt;ready&lt;/code&gt; only when every parent is &lt;code&gt;done&lt;/code&gt;&lt;/strong&gt;. No prose ordering, no "we'll do this after that, remember".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;todo -&amp;gt; ready once every parent is done; done parents unlock children.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id,status,parents FROM tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;todo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;parents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE tasks SET status=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ready&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,updated_at=? WHERE id=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unlocked_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every transition — created, ready, claimed, completed, blocked — appends a row to an &lt;code&gt;events&lt;/code&gt; table. The log is append-only, so a crash never erases history, and "why is this task not moving?" is one query instead of a slack thread.&lt;/p&gt;

&lt;p&gt;Statuses are a small, boring set: &lt;code&gt;todo | ready | running | blocked | review | done&lt;/code&gt;. That is the whole state machine. It fits on a sticky note, which is the point — an agent team does not need a workflow engine, it needs an unambiguous answer to "can this start yet?".&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The dispatcher — routing order, and the anti-loop rule
&lt;/h2&gt;

&lt;p&gt;The dispatcher is a dict of &lt;code&gt;{assignee: handler}&lt;/code&gt; plus a routing order (the org chart, in code). Each pass it asks the board for ready tasks per assignee, claims them, runs the handler, and records the handoff.&lt;/p&gt;

&lt;p&gt;Handlers are just callables that return a dict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;with_board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;with_board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analyst&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;with_board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auditor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;with_board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auditor_handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;disp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dispatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auditor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;trace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;disp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_cycles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules that did most of the work for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A handler that raises blocks the task&lt;/strong&gt; (with the error in the event log) instead of killing the run. One broken stage does not take the pipeline down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A repeated block escalates instead of retrying.&lt;/strong&gt; Retry policies hide broken work; escalation surfaces it. The dispatcher carries a &lt;code&gt;block_limit&lt;/code&gt;; the policy we actually run is: after N blocks on the same task, stop and route it to a human. Note the honest version of that seam — the shipped offline demo drives the happy path and does not push a task into repeated blocks, so the escalation counter is the one piece the demo leaves for you to wire to your own paging/alerting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. The gate — two layers, deterministic
&lt;/h2&gt;

&lt;p&gt;This is the part that changes the outcome. A contract check that is structural, and an audit check that is substantive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer one: the handoff contract.&lt;/strong&gt; Per role, the keys that &lt;em&gt;must&lt;/em&gt; be present on completion. Not "did it produce text", but "did the analyst actually emit a shortlist and a recommendation".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;HANDOFF_CONTRACT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;candidates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shortlist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recommendation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channels&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;drafts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auditor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty list is a legitimate value (an auditor with no issues); only a genuinely absent or blank key counts as missing. That distinction matters more than it looks: a gate that flags "no issues found" as a failure trains everyone to ignore the gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer two: the audit gate.&lt;/strong&gt; It re-derives the arithmetic, requires a URL &lt;strong&gt;and a date&lt;/strong&gt; on every cited source, and rejects numbers that are not labelled. It is deterministic — no LLM call — so a run is reproducible and the verdict is not a coin flip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;calc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calc&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;what&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gross mismatch: %s x %s = %s but artifact says %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                               &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;med&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL-and-date requirement is the cheapest honesty mechanism I have found. &lt;code&gt;29 EUR (checked 2026-10-06, &amp;lt;link&amp;gt;)&lt;/code&gt; survives a review; &lt;code&gt;~29 EUR&lt;/code&gt; quietly becomes a fact three documents later, with no one able to say where it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The meter — the budget rule on the critical path
&lt;/h2&gt;

&lt;p&gt;Every agent step appends one JSON line to &lt;code&gt;usage.log&lt;/code&gt; (append-only; a crash never loses history). The report answers one question: is spend inside the month's pace, or is the month being burned in the first week?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days_elapsed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Pace rule: spend% must stay &amp;lt;= proportional target (1 - days_left/month).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;spent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;target_pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days_elapsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days_in_month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;spent_pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monthly_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monthly_usd&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;spent_pct&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;target_pct&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AHEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;spent_pct&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;target_pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WATCH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spent_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;spent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;monthly_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monthly_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spent_pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spent_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OK / WATCH / AHEAD&lt;/code&gt; is deliberately coarse. A precise number nobody reads loses to three words that show up in a log every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part most people skip: the negative control
&lt;/h2&gt;

&lt;p&gt;A gate that has never failed proves nothing. So the demo feeds the audit gate a deliberately corrupted artifact — one where the stated gross does not equal price x volume — and checks that the gate says so.&lt;/p&gt;

&lt;p&gt;Here is the actual output of &lt;code&gt;python3 demo/run_demo.py&lt;/code&gt; (task ids are uuids regenerated each run; the rest is verbatim):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Board seeded: 4 tasks, 3 dependency edges.
  goal       &amp;lt;id&amp;gt;
  analyst    &amp;lt;id&amp;gt; (needs goal)
  builder    &amp;lt;id&amp;gt; (needs analyst)
  auditor    &amp;lt;id&amp;gt; (needs builder)

Dispatcher trace (4 transitions):
  &amp;lt;id&amp;gt; -&amp;gt; done
  &amp;lt;id&amp;gt; -&amp;gt; done
  &amp;lt;id&amp;gt; -&amp;gt; done
  &amp;lt;id&amp;gt; -&amp;gt; done

Handoff contract check:
  scout    OK
  analyst  OK
  builder  OK
  auditor  OK

Audit gate verdict: approve
  checked: price x volume arithmetic, source url+date, claim labels, builder handoff contract: ok

Usage meter (day 3 of 31):
   {'spent_usd': 1.16, 'monthly_usd': 100.0, 'spent_pct': 1.2, 'target_pct': 9.7, 'status': 'OK'}

Gate negative control (deliberate gross error): changes -&amp;gt; gross mismatch: 49.0 x 42 = 2058.0 but artifact says 999.0

DEMO RESULT: PASS - full pipeline ran offline (+ gate catches errors)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The meter line is demo data from four mock steps (0.12 + 0.35 + 0.48 + 0.21 = 1.16 USD), not a benchmark of anything. The interesting line is the second-to-last one: approving good work is easy; the value of the gate is that it refuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I'd tell anyone building this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Escalate, don't retry.&lt;/strong&gt; Retry policies hide broken work; escalation surfaces it. Cap failed blocks and route them to a human, then fix the task, not the loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label every number.&lt;/strong&gt; Source, date, or the word "estimate". The alternative is a number that outlives its evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the meter on the critical path.&lt;/strong&gt; A budget report you read on the last day of the month is a post-mortem, not a control.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;It is a skeleton, not a platform. The demo runs deterministic mock handlers so it is reproducible — swapping them for calls to any OpenAI-compatible endpoint does not change the board, the gate or the meter. There is no server to run, no dashboard, and no lock-in: it is source you own and edit. Python 3.8+, standard library only. The demo needs no API key and no account.&lt;/p&gt;

&lt;p&gt;The five role definitions (orchestrator, scout, analyst, builder, auditor) ship as markdown with frontmatter, so the "org chart" is a file you can argue with in a PR.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Everything described in this post is the engine: the board, the handoffs, the gate, the meter and the offline demo — four Python modules, standard library only, no API key, runs in seconds. If you build one thing from it, build the negative control: a gate that has never failed proves nothing, so the demo feeds the audit gate a deliberately corrupted artifact and checks that it refuses.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI-assistance disclosure: this post was written with AI assistance — the agents it describes drafted it, and every figure in it was checked against the pinned sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
