<?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>Forem: Guilherme Secca</title>
    <description>The latest articles on Forem by Guilherme Secca (@seccaz).</description>
    <link>https://forem.com/seccaz</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3902753%2F1e5973c7-9655-415d-9918-7c2d3c654f6b.jpeg</url>
      <title>Forem: Guilherme Secca</title>
      <link>https://forem.com/seccaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/seccaz"/>
    <language>en</language>
    <item>
      <title>When agents hit your 429 without reset_at, things get bad fast</title>
      <dc:creator>Guilherme Secca</dc:creator>
      <pubDate>Tue, 28 Apr 2026 16:22:30 +0000</pubDate>
      <link>https://forem.com/seccaz/when-agents-hit-your-429-without-resetat-things-get-bad-fast-596d</link>
      <guid>https://forem.com/seccaz/when-agents-hit-your-429-without-resetat-things-get-bad-fast-596d</guid>
      <description>&lt;p&gt;A 429 without a machine-readable reset timestamp doesn't just slow agents down. Under the right conditions it turns them into a coordinated attack on your own API.&lt;/p&gt;

&lt;p&gt;Think through what happens: 50 agents running generated code against the same SDK all hit a rate limit at roughly the same time. None of them gets a &lt;code&gt;reset_at&lt;/code&gt;. So each one does the sensible thing and starts exponential backoff with jitter. Except "jitter" across agents running identical generated code tends to cluster. They back off, then they all retry at roughly the same time, then they all hit the limit again. The thundering herd is your own clients. You don't need pathological load for this — you just need enough agents running the same generated retry logic.&lt;/p&gt;

&lt;p&gt;This is what we designed around building &lt;a href="https://truval.dev" rel="noopener noreferrer"&gt;Truval&lt;/a&gt;: API infrastructure built to be called by agents. Email verification is the first surface — more APIs will follow. The fix was straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rate_limit_exceeded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rate limit of 10 req/min exceeded."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wait until reset_at (plus a small cushion) before retrying."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"window"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reset_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-24T12:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.truval.dev/api/email-verify#rate-limits"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also send &lt;code&gt;Retry-After&lt;/code&gt; as an HTTP header — same idea, expressed as delta-seconds until &lt;code&gt;reset_at&lt;/code&gt; — for clients that read it there. One edge case worth knowing: &lt;code&gt;reset_at&lt;/code&gt; is a raw server-side timestamp with no built-in buffer. If a client's clock runs slightly ahead of ours, an early retry might land before the window resets and get another 429. The SDK adds 50ms; for generic clients without tight loops, 1-2s is a safe conservative default. Either way, sleep past &lt;code&gt;reset_at&lt;/code&gt; rather than exactly on it — which is why the action field says "plus a small cushion."&lt;/p&gt;

&lt;p&gt;Now retry logic is just: sleep until &lt;code&gt;reset_at&lt;/code&gt; (plus a small cushion), then retry once. No backoff math. No jitter. No storm.&lt;/p&gt;

&lt;p&gt;That one change is the most important thing I’d tell someone designing an API that agents will call. Everything else matters, but it’s downstream of “don’t make agents guess.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents depend on contracts, not docs
&lt;/h2&gt;

&lt;p&gt;When a developer integrates an API they read the docs, try things, adjust. The feedback loop is interactive. Agents don’t work that way. They ingest a machine-readable surface once, pick a path, and repeat it across every generated call forever. If anything in that path is ambiguous, they’ll invent a plausible answer. Wrong base URL, wrong auth header, a missing retry on a transient 503 that gets treated as permanent.&lt;/p&gt;

&lt;p&gt;The contract an agent actually depends on is: base URL, auth shape, OpenAPI spec, and a small stable set of error codes with typed fields. Not the prose. Not the getting started guide.&lt;/p&gt;

&lt;p&gt;For Truval this meant publishing three stable URLs and treating them like a public interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;https://api.truval.dev&lt;/code&gt; — the base, not moving&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;https://api.truval.dev/openapi.json&lt;/code&gt; — source of truth for codegen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;https://mcp.truval.dev/mcp&lt;/code&gt; — tool surface for MCP-compatible hosts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The OpenAPI spec is a compatibility boundary, same as a library interface. Renaming an operation casually is a breaking change. Adding a field is fine. Removing or renaming one is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other errors worth getting right
&lt;/h2&gt;

&lt;p&gt;Monthly quota hits need the same treatment as rate limits — &lt;code&gt;reset_at&lt;/code&gt;, plus &lt;code&gt;used&lt;/code&gt; and &lt;code&gt;limit&lt;/code&gt; so agents can decide what to surface. &lt;code&gt;reset_at&lt;/code&gt; is an exact boundary timestamp. (The “small cushion” matters most for short windows like per-minute rate limits. For monthly quota resets it’s midnight UTC, so being off by 1–2 seconds is irrelevant in practice.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"monthly_quota_exceeded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Monthly free tier limit of 500 verification units reached for this UTC month."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Upgrade your plan or wait until the next UTC month."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"used"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reset_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-01T00:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.truval.dev/api/email-verify#monthly-quota"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Billing blocks should carry the numeric fields that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hard spend cap reached. Upgrade or raise your cap to continue."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Adjust your hard cap in Billing &amp;amp; Limits, or upgrade your plan."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hard_cap_eur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"current_overage_eur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;52.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.truval.dev/api/email-verify#spend-cap"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That lets a client render a real UI state rather than retrying forever on something that won’t resolve.&lt;/p&gt;

&lt;p&gt;Quota check failures — when you genuinely can’t tell whether a request is within limits — should be a dedicated 503, not a generic 500. Agents need to treat it as transient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quota_check_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Could not verify monthly usage quota. Retry shortly."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If this persists, contact support."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.truval.dev/api/email-verify#monthly-quota"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Auth shape mismatches are the other common failure
&lt;/h2&gt;

&lt;p&gt;Agents frequently mix up key types, especially when an API has more than one. For Truval there’s a verification key and a provisioning key — different prefixes, different permissions. Without a specific error, an agent with the wrong key will just retry with the same key and eventually give up.&lt;/p&gt;

&lt;p&gt;The fix is a typed error that says exactly what went wrong and what to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wrong_key_type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This endpoint requires a verification key (sk_live_). You sent a provisioning key (sk_mgmt_)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Use your verification key for this request."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.truval.dev/api/email-verify#authentication"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Retry semantics: make the policy explicit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Retry on 429 using &lt;code&gt;reset_at&lt;/code&gt;. Sleep until then (plus a small cushion), retry once.&lt;/li&gt;
&lt;li&gt;Retry on transient 5xx and network timeouts with a short fixed delay.&lt;/li&gt;
&lt;li&gt;Never retry on &lt;code&gt;invalid_request&lt;/code&gt; — it won’t change.&lt;/li&gt;
&lt;li&gt;Never retry on &lt;code&gt;payment_required&lt;/code&gt; or &lt;code&gt;monthly_quota_exceeded&lt;/code&gt; — same.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ship an SDK, encode this directly in the client. Generated code will get it wrong otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Canonical base URL, not moving&lt;/li&gt;
&lt;li&gt;OpenAPI spec at a stable URL, treated as a compatibility boundary&lt;/li&gt;
&lt;li&gt;Stable error codes with typed fields: &lt;code&gt;reset_at&lt;/code&gt;, &lt;code&gt;limit&lt;/code&gt;, &lt;code&gt;used&lt;/code&gt;, &lt;code&gt;hard_cap_eur&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Retry-After&lt;/code&gt; header on all 429s matching the JSON &lt;code&gt;reset_at&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Explicit retry semantics consistent with how you actually enforce limits&lt;/li&gt;
&lt;li&gt;MCP as a thin adapter over the same contract, not a separate API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agents will hold you to all of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://truval.dev" rel="noopener noreferrer"&gt;truval.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>agents</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
