<?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: Nguyễn Tiến Huy</title>
    <description>The latest articles on DEV Community by Nguyễn Tiến Huy (@myuh250).</description>
    <link>https://dev.to/myuh250</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%2F1330232%2F191a7ea7-3d9b-4ec5-814b-9c98ea022c23.jpeg</url>
      <title>DEV Community: Nguyễn Tiến Huy</title>
      <link>https://dev.to/myuh250</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/myuh250"/>
    <language>en</language>
    <item>
      <title>Stopping the $10,000 AWS Bedrock Bill: How We Built a Zero-Proxy Kill Switch for $1/Month</title>
      <dc:creator>Nguyễn Tiến Huy</dc:creator>
      <pubDate>Thu, 05 Nov 2026 14:59:44 +0000</pubDate>
      <link>https://dev.to/myuh250/stopping-the-10000-aws-bedrock-bill-how-we-built-a-zero-proxy-kill-switch-for-1month-278m</link>
      <guid>https://dev.to/myuh250/stopping-the-10000-aws-bedrock-bill-how-we-built-a-zero-proxy-kill-switch-for-1month-278m</guid>
      <description>&lt;p&gt;As a Junior DevOps engineer holding the keys to the AWS kingdom, life was peak "unrestricted free will." I could spin up architecture, tear down stacks, and feel like a tech god. Then came the inevitable executive tap on the shoulder: "Show me the bill."&lt;/p&gt;

&lt;p&gt;I pulled up AWS Cost Explorer. He saw the numbers, smiled, and signed off. Smooth sailing.&lt;/p&gt;

&lt;p&gt;Then leadership dropped the big vision: "We are going full AI-first!"&lt;/p&gt;

&lt;p&gt;Naturally, I handed him Amazon Bedrock. He was thrilled. And like every proper business executive, he eventually asked the classic follow-up: "Show me the bill."&lt;/p&gt;

&lt;p&gt;I pulled up Cost Explorer again. Still green, still happy. Then came the twist that ruined my week: "Now break down the cost for EACH individual developer."&lt;/p&gt;

&lt;p&gt;Here is where the corporate tragedy set in. Turns out we aren't running on a Payer Account. Which instantly meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost allocation tags on IAM users? Only the payer account can activate them. Not us.&lt;/li&gt;
&lt;li&gt;CUR 2.0? Billing lives with the payer. And even with it, CUR refreshes a few times a day. That's an autopsy, not a guardrail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Worse, leadership didn't just want visibility, they asked the million-dollar question: &lt;em&gt;"Can we stop a developer before they set our budget on fire?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AWS Budgets? Data lags 8–24 hours. By the time the alert lands, the agent loop has been burning tokens all night. A LiteLLM reverse proxy? Extra latency on every token, a shiny new single point of failure, and a pager that's now lowkey my personality.&lt;/p&gt;

&lt;p&gt;So I did what any reasonable engineer does when backed into a corner: I built my own solution.&lt;/p&gt;

&lt;p&gt;This is the engineering story of &lt;code&gt;bedrock-cost-sentry&lt;/code&gt;: an event-driven, out-of-band IAM budget guard for Amazon Bedrock. Zero proxy, zero added latency, full &lt;code&gt;/model&lt;/code&gt; freedom in Claude Code, and per-developer enforcement within 1–3 minutes, for about $1 a month.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Context: What Are We Building?
&lt;/h2&gt;

&lt;p&gt;At its core, &lt;code&gt;bedrock-cost-sentry&lt;/code&gt; is a completely serverless, out-of-band control plane that monitors Bedrock consumption, calculates token-level dollar costs on the fly, and cuts off developers the second they cross their spending cap.&lt;/p&gt;

&lt;p&gt;Developers do not connect to a custom domain or route their requests through a proxy. They talk directly to AWS Bedrock runtime endpoints in their chosen region using their standard IAM access keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CODE_USE_BEDROCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"AKIA..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7aj14r3blvnhx1xdldbu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7aj14r3blvnhx1xdldbu.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture in 5 Steps:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Direct Invocation&lt;/strong&gt;: The developer's machine talks directly to Amazon Bedrock. Latency penalty = 0ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry Ingestion&lt;/strong&gt;: Bedrock writes invocation metadata (caller IAM ARN, model ID, input/output/cache token counts) to Amazon CloudWatch Logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Filtering&lt;/strong&gt;: CloudWatch subscription filters stream matching log records to an AWS Lambda function (&lt;code&gt;enforcer&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounting &amp;amp; Idempotency&lt;/strong&gt;: The Lambda checks an idempotency ledger table in DynamoDB, calculates the call cost using an internal pricing engine, and atomically increments the user's spending tally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforcement (The Kill Switch)&lt;/strong&gt;: If the user exceeds their configured dollar or token cap, the Lambda attaches an inline IAM &lt;code&gt;Deny&lt;/code&gt; policy (&lt;code&gt;BedrockBudgetDeny&lt;/code&gt;) directly to the user's IAM entity. Their very next request fails with &lt;code&gt;AccessDenied&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  2. The Problem: Why Do We Need This?
&lt;/h2&gt;

&lt;p&gt;Why go through the trouble of building an event-driven architecture when AWS already has billing tools? Because enterprise cloud governance is full of edge cases that official sales decks never mention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitation 1: The "Next-Day Autopsy" of AWS Budgets
&lt;/h3&gt;

&lt;p&gt;AWS Budgets and AWS Cost Explorer rely on billing pipeline data exports. That data lands on a delay anywhere between 8 and 24 hours. If an engineer accidentally kicks off an agentic script that burns $300 an hour at midnight, an AWS Budget alert arriving at 11:00 AM the next morning is not a guardrail—it's a funeral notice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitation 2: The Enterprise Org Payer Account Lockout
&lt;/h3&gt;

&lt;p&gt;AWS natively supports tagging Bedrock spend using &lt;strong&gt;Application Inference Profiles&lt;/strong&gt;. In theory, you could give each developer an inference profile tagged &lt;code&gt;User = alice&lt;/code&gt;, and group spend in Cost Explorer. But &lt;strong&gt;Cost Allocation Tags can only be activated in the AWS Organization Payer (Management) Account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In any mid-to-large organization, engineering teams are relegated to member accounts or sandbox accounts. Access to the payer account is guarded by corporate finance and enterprise cloud teams behind a fortress of bureaucracy. If you cannot activate tags at the root payer level, Cost Explorer per-user cost allocation simply does not work.&lt;/p&gt;

&lt;p&gt;&lt;del&gt;Now I get why people rage-quit corporate for startups&lt;/del&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitation 3: Why Proxies (LiteLLM / API Gateway) Weren't the Answer
&lt;/h3&gt;

&lt;p&gt;The alternative was spinning up a proxy container. But proxies introduce heavy trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Point of Failure&lt;/strong&gt;: If your proxy crashes, the entire engineering department stops coding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency &amp;amp; Streaming&lt;/strong&gt;: Streaming completions (&lt;code&gt;InvokeModelWithResponseStream&lt;/code&gt;, &lt;code&gt;ConverseStream&lt;/code&gt;) require persistent HTTP chunking or WebSockets. Proxies frequently drop long-running connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling Friction&lt;/strong&gt;: You have to force developers and CLI tools to override their base URLs, bypass enterprise certificate authorities, and reconfigure their environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not signing up to be the single point of failure for every coding agent in the company. We needed a system that observed reality out-of-band without getting in the developer's way and keep my job safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Solution: How Does It Work?
&lt;/h2&gt;

&lt;p&gt;Let's break down the mechanics of the stack into its key engineering components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Component 1: Metadata-Only Ingestion (Zero Plaintext Prompts)
&lt;/h3&gt;

&lt;p&gt;To count tokens, you must enable Bedrock Model Invocation Logging. But turning on logging often triggers an immediate veto from InfoSec: &lt;em&gt;"Are we logging sensitive company code and user prompts to CloudWatch?!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer is no. Tokens live in the metadata, so we turn off every payload flag. Compliance 101: follow the rules. Can't follow the rules? Don't get caught. Getting caught? I don't know you. (Author not liable. This is satire. Mostly.)&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;modules/bedrock-logging/main.tf&lt;/code&gt;, we explicitly turn off payload delivery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrock_model_invocation_logging_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;logging_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Token counts live in metadata — full prompt/response text is disabled.&lt;/span&gt;
    &lt;span class="c1"&gt;# Avoids logging proprietary source code in plaintext and cuts&lt;/span&gt;
    &lt;span class="c1"&gt;# CloudWatch ingest cost by orders of magnitude.&lt;/span&gt;
    &lt;span class="nx"&gt;embedding_data_delivery_enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nx"&gt;image_data_delivery_enabled&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nx"&gt;text_data_delivery_enabled&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="nx"&gt;cloudwatch_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;log_group_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="nx"&gt;role_arn&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bedrock_logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&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;By setting &lt;code&gt;text_data_delivery_enabled = false&lt;/code&gt;, Bedrock logs only the invocation metadata: caller identity, model identifier, and token numbers. Not a single line of code or prompt text enters CloudWatch. This also shrinks log payload sizes by ~99%, slashing ingestion and retention bills to pennies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Component 2: Multi-Region Ingest, Centralized State
&lt;/h3&gt;

&lt;p&gt;Developers don't stick to one region. Claude Sonnet might run in &lt;code&gt;us-east-1&lt;/code&gt;, while regional quotas push someone to &lt;code&gt;ap-southeast-1&lt;/code&gt; (Singapore) or &lt;code&gt;us-west-2&lt;/code&gt; (Oregon).&lt;/p&gt;

&lt;p&gt;Because Bedrock invocation logging is a regional singleton, each tracked region needs its own logging configuration and a local instance of the &lt;code&gt;enforcer&lt;/code&gt; Lambda. However, spinning up separate DynamoDB tables across regions would fracture budgets.&lt;/p&gt;

&lt;p&gt;Instead, regional enforcers stream events across AWS's backbone into a &lt;strong&gt;single, centralized DynamoDB table&lt;/strong&gt; in &lt;code&gt;us-east-1&lt;/code&gt;. Because DynamoDB rows are partitioned by IAM username (&lt;code&gt;userId&lt;/code&gt;), Singapore usage and US usage fan into the exact same budget row:&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="c1"&gt;# Primary region stores the central table; regional Lambdas point explicitly to it
&lt;/span&gt;&lt;span class="n"&gt;dynamodb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DYNAMODB_REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DYNAMODB_TABLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;us-east-1&lt;/code&gt;?&lt;/strong&gt; Honest answer: I picked it because it felt like the safest region. Oldest, biggest, the one AWS surely babysits the hardest. Then I remembered which region keeps showing up in outage post-mortems. Yes, that one. Bold choice, junior me.&lt;/p&gt;

&lt;p&gt;It turned out to be the right call anyway, for reasons I only appreciated later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The kill switch lives there.&lt;/strong&gt; IAM is "global," but its control plane endpoint (&lt;code&gt;iam.amazonaws.com&lt;/code&gt;) sits in &lt;code&gt;us-east-1&lt;/code&gt;. So does Cost Explorer (&lt;code&gt;ce:GetCostAndUsage&lt;/code&gt;). Keeping budget state next to the APIs that act on it means less cross-region plumbing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most traffic is already there.&lt;/strong&gt; Claude calls land mostly in &lt;code&gt;us-east-1&lt;/code&gt;, so the busiest enforcer writes locally. Singapore pays a ~200ms cross-region write on an async pipeline whose logs already arrive 1–3 minutes late. Nobody will ever notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Tables would lie to us.&lt;/strong&gt; Multi-region DynamoDB Global Tables resolve conflicts with last-writer-wins. Two regions doing an atomic &lt;code&gt;ADD costUsd&lt;/code&gt; on the same row at the same moment can silently drop one increment. A budget counter that undercounts is worse than no counter. One table means atomic stays atomic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if &lt;code&gt;us-east-1&lt;/code&gt; has one of its famous bad days? Enforcement pauses, but it fails open. Developers keep shipping; they just get a few unmetered minutes of free will. Which, to be fair, is exactly what they had before I showed up.&lt;/p&gt;

&lt;p&gt;Yes, I know. One table is a single point of failure. I also know a fleet of replica tables, just to protect a few dozen budget counters, is the kind of architecture that looks great in a diagram and terrible on the bill I'm supposed to be shrinking. So the table gets cheap insurance instead: deletion protection and point-in-time recovery (PITR), both on. And if PITR ever lets me down, the real source of truth never lived in DynamoDB anyway. Every invocation's token counts sit in CloudWatch Logs (30-day retention), so I can replay them and rebuild every counter from scratch. We've already run a smaller version of this for real: after the Grok double-billing bug in Pain Point 4, we corrected the counters straight from the invocation log. Is it fun? Absolutely not. It's a long afternoon of Logs Insights queries and atomic &lt;code&gt;ADD&lt;/code&gt;s. But "painful but possible" is a recovery plan, and it costs $0 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Component 3: The Pricing Engine &amp;amp; Model Precedence
&lt;/h3&gt;

&lt;p&gt;Log events don't report dollar amounts; they report token integers (&lt;code&gt;inputTokenCount&lt;/code&gt;, &lt;code&gt;outputTokenCount&lt;/code&gt;, &lt;code&gt;cacheReadInputTokenCount&lt;/code&gt;, &lt;code&gt;cacheWriteInputTokenCount&lt;/code&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This enforcer speaks fluent Claude. Other providers have their own dialects of "token." Grok, for example, quietly stuffs cached tokens into &lt;code&gt;inputTokenCount&lt;/code&gt;, and our enforcer happily billed them twice, about 3x the real cost (full story in Pain Point 4). New model family? Reconcile against Cost Explorer before you trust it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The enforcer computes the cost in Python using an internal price table:&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;PRICE_PER_1K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# modelId substring: {input, output, cacheWrite, cacheRead}  (excerpt)
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-fable-5-1&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.010&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.050&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0125&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.00025&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;claude-fable-5&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.010&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.050&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0125&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.001&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;claude-opus-5-5&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.004&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.020&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.005&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0002&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;claude-opus-4-1&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.015&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.075&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.01875&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0015&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;claude-opus-4&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.005&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.025&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.00625&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0005&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;claude-sonnet-5-5&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.002&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.010&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0025&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0001&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;claude-sonnet-5&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.003&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.015&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.00375&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0003&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;claude-sonnet-4&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.003&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.015&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.00375&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0003&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;claude-haiku-5-5&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0001&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0005&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.000125&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.00001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;  &lt;span class="c1"&gt;# prompts &amp;gt; 100k tokens use a separate, pricier tier
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-haiku-4&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.001&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.005&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.00125&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0001&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;grok-4.7&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;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.002&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;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.006&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;cacheWrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.002&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;cacheRead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0005&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
    &lt;span class="c1"&gt;# ...plus Claude 3.x, Qwen, Nova Canvas (priced per image, so $0 per token)
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# Fallback to the Opus 4.1 price so unlisted models never silently undercount
&lt;/span&gt;&lt;span class="n"&gt;DEFAULT_PRICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PRICE_PER_1K&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-4-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Important&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Substrings and Dictionary Ordering&lt;/strong&gt;: In Python 3.7+, dictionary iteration preserves insertion order. Notice how &lt;code&gt;"claude-sonnet-5-5"&lt;/code&gt; precedes &lt;code&gt;"claude-sonnet-5"&lt;/code&gt;, &lt;code&gt;"claude-fable-5-1"&lt;/code&gt; precedes &lt;code&gt;"claude-fable-5"&lt;/code&gt;, and &lt;code&gt;"claude-opus-4-1"&lt;/code&gt; precedes &lt;code&gt;"claude-opus-4"&lt;/code&gt;. If you match shorter substrings first, your newer, cheaper models get billed at the legacy rate. Every model generation also needs its own key: &lt;code&gt;"grok-4.6"&lt;/code&gt; does not match &lt;code&gt;grok-4.7&lt;/code&gt;, and a miss silently bills a Grok call at the Opus rate (~7x over-count).&lt;/p&gt;

&lt;p&gt;[!WARNING]&lt;br&gt;
&lt;strong&gt;Token fields don't mean the same thing for every model family.&lt;/strong&gt; For Claude, &lt;code&gt;inputTokenCount&lt;/code&gt; excludes cached tokens, so &lt;code&gt;input * price + cacheRead * price&lt;/code&gt; is correct. For Grok on &lt;code&gt;bedrock-runtime&lt;/code&gt;, &lt;code&gt;inputTokenCount&lt;/code&gt; &lt;em&gt;includes&lt;/em&gt; the cached tokens, so the same formula bills every cached token twice (once at the full input rate, once at the cache-read rate). Grok needs &lt;code&gt;(input - cacheRead)&lt;/code&gt; as billable input. See Pain Point 4 for how we found out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If an unknown model arrives, it deliberately falls through to &lt;code&gt;DEFAULT_PRICE&lt;/code&gt; (the Opus 4.1 rate). In financial engineering, an accidental over-count that triggers an alert is an annoyance; an accidental under-count that lets an unbudgeted model burn thousands of dollars is a disaster.&lt;/p&gt;

&lt;p&gt;So yes, our numbers run a little hot. In our monthly audits, the sentry's total lands roughly 5–8% &lt;em&gt;above&lt;/em&gt; Cost Explorer. The enforcer prices everything at public list rates, while the real invoice quietly applies discounts and credits it never tells us about. Add the occasional unknown model billed at Opus prices, and the report is always a bit pessimistic. And that's the point. A budget guard that's slightly paranoid blocks a developer one request early. A budget guard that's slightly optimistic sends finance a $4,000 surprise. I'll take paranoid every time. The rule we actually watch is direction, not size: if the sentry ever reads &lt;em&gt;below&lt;/em&gt; Cost Explorer, something is being missed, and that's a bug, not a rounding error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Component 4: The IAM Kill Switch
&lt;/h3&gt;

&lt;p&gt;When a user crosses their budget ceiling, how do you sever their connection without tearing down their access keys?&lt;/p&gt;

&lt;p&gt;You inject an inline policy directly onto their IAM user entity:&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;attach_deny_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_user_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;UserName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PolicyName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BedrockBudgetDeny&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PolicyDocument&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DENY_POLICY_DOCUMENT&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;In AWS IAM evaluation logic, &lt;strong&gt;an explicit Deny always supersedes any Allow&lt;/strong&gt;. Even if the user belongs to an &lt;code&gt;AdministratorAccess&lt;/code&gt; group or holds broad developer policies, the inline &lt;code&gt;Deny&lt;/code&gt; halts further Bedrock calls on their very next API attempt. In IAM, an explicit Deny outranks everything. &lt;code&gt;AdministratorAccess&lt;/code&gt;? Cute. Over budget means over budget, admin or not.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Implementation: Let's Get Our Hands Dirty
&lt;/h2&gt;

&lt;p&gt;Let's dive into the core implementation details and patterns that make this system bulletproof.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Defining Budgets in Terraform
&lt;/h3&gt;

&lt;p&gt;Budgets and users are declared in &lt;code&gt;terraform.tfvars&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev-alice"&lt;/span&gt;
    &lt;span class="nx"&gt;team&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"platform"&lt;/span&gt;
    &lt;span class="nx"&gt;cost_limit_usd&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;        &lt;span class="c1"&gt;# Monthly limit in USD&lt;/span&gt;
    &lt;span class="nx"&gt;input_token_limit&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;          &lt;span class="c1"&gt;# 0 = unlimited on this axis&lt;/span&gt;
    &lt;span class="nx"&gt;output_token_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;tags&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend-ai"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;period&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"month"&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev-bob-agentic"&lt;/span&gt;
    &lt;span class="nx"&gt;team&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"automation"&lt;/span&gt;
    &lt;span class="nx"&gt;cost_limit_usd&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;         &lt;span class="c1"&gt;# Daily sandbox allowance&lt;/span&gt;
    &lt;span class="nx"&gt;input_token_limit&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;output_token_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;tags&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"rag-testing"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;period&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"day"&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;]&lt;/span&gt;

&lt;span class="nx"&gt;alert_emails&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"devops-alerts@example.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;alert_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;  &lt;span class="c1"&gt;# Warn via SNS at 80% of limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: The Terraform State vs. Runtime Mutation Trap
&lt;/h3&gt;

&lt;p&gt;Here is a classic infrastructure-as-code conundrum:&lt;br&gt;
You want to declare user budgets in Terraform (&lt;code&gt;aws_dynamodb_table_item&lt;/code&gt;). But the &lt;code&gt;enforcer&lt;/code&gt; Lambda continuously updates that exact same row at runtime (&lt;code&gt;costUsd&lt;/code&gt;, &lt;code&gt;inputTokensUsed&lt;/code&gt;, &lt;code&gt;blocked&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;If you run &lt;code&gt;terraform apply&lt;/code&gt; next week to adjust an alert email, Terraform will see that &lt;code&gt;costUsd = 142.50&lt;/code&gt; differs from the declared &lt;code&gt;costUsd = 0&lt;/code&gt; in your code. By default, Terraform will happily overwrite the row, wiping out live usage counters and unblocking every blocked user!&lt;/p&gt;

&lt;p&gt;To fix this, we split seeding from updating in &lt;code&gt;modules/dynamodb/main.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_dynamodb_table_item"&lt;/span&gt; &lt;span class="s2"&gt;"user_budgets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_dynamodb_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;budgets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;hash_key&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_dynamodb_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;budgets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash_key&lt;/span&gt;

  &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;userId&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;costLimitUsd&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cost_limit_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;costUsd&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;blocked&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BOOL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# ... other counters ...&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;# Ignore changes to live operational data!&lt;/span&gt;
  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ignore_changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Replaced whenever a limit changes, so the provisioner re-runs;&lt;/span&gt;
&lt;span class="c1"&gt;# updates ONLY limits via UpdateItem&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"terraform_data"&lt;/span&gt; &lt;span class="s2"&gt;"budget_limits"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;triggers_replace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cost_limit_usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cost_limit_usd&lt;/span&gt;
    &lt;span class="nx"&gt;period&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;period&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_dynamodb_table_item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_budgets&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python3 ${path.module}/sync_limits.py"&lt;/span&gt;
    &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_dynamodb_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;budgets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
      &lt;span class="nx"&gt;USER_ID&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
      &lt;span class="nx"&gt;COST_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cost_limit_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;PERIOD&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;period&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="p"&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;The initial seed sets up the row with &lt;code&gt;ignore_changes = [item]&lt;/code&gt;. Whenever you change a budget ceiling in &lt;code&gt;terraform.tfvars&lt;/code&gt;, &lt;code&gt;terraform_data&lt;/code&gt; fires a focused Python script (&lt;code&gt;sync_limits.py&lt;/code&gt;) that executes an &lt;code&gt;UpdateItem&lt;/code&gt; exclusively targeting &lt;code&gt;costLimitUsd&lt;/code&gt; and &lt;code&gt;resetPeriod&lt;/code&gt;. Live spend counters remain untouched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Marker-First Idempotency
&lt;/h3&gt;

&lt;p&gt;CloudWatch log subscriptions deliver events with &lt;strong&gt;at-least-once&lt;/strong&gt; semantics. If AWS retries an enforcer batch, or if Lambda encounters transient network issues, the same log event can be processed multiple times. Without deduplication, retried calls would double-count usage and prematurely block innocent developers.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;lambda-src/enforcer.py&lt;/code&gt;, we implement a dedicated &lt;strong&gt;idempotency ledger&lt;/strong&gt;:&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;claim_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Marker-first idempotency using DynamoDB condition expressions.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;PROCESSED_TABLE&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROCESSED_TABLE&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Item&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;requestId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;PROCESSED_TTL_SECONDS&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(requestId)&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&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;Code&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;ConditionalCheckFailedException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# Already counted! Skip it.
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the critical design choice: &lt;strong&gt;The marker is claimed &lt;em&gt;before&lt;/em&gt; incrementing the user's budget.&lt;/strong&gt;&lt;br&gt;
If the Lambda crashes after claiming the marker but before incrementing the budget, we under-count by a fraction of a cent. But if we incremented first and crashed before marking, a retry would double-count spend and trigger an unfair block. In billing systems, always fail in favor of the developer.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: The Inspection CLI (&lt;code&gt;report.py&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;No need to wade through the AWS Management Console to inspect usage. A standalone Python script scans the central DynamoDB table and generates rollups in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rollup total spend grouped by team&lt;/span&gt;
python3 report.py &lt;span class="nt"&gt;--table&lt;/span&gt; bedrock-cost-sentry-budgets &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--group-by&lt;/span&gt; team
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cost by tag 'team':
  group                    cost($)  users           input          output       cacheRead      cacheWrite
  platform                  184.20      4       1,240,512         412,080      18,940,210       4,120,500
  automation                 18.50      2         180,400          84,200       1,450,000         210,000

Per-user:
  user              cost($)   limit($)      %  blocked          input         output      cacheRead     cacheWrite
  dev-alice          142.10     250.00     57    False        910,200        290,100     12,400,000      2,900,000
  dev-bob             18.50      20.00     93    False        180,400         84,200      1,450,000        210,000

  TOTAL                     202.70                                 1,420,912         496,280
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Pain Points &amp;amp; Hard-Won Lessons
&lt;/h2&gt;

&lt;p&gt;Building systems on top of AWS telemetry sounds trivial until reality hits you in the face. Here are the scars we earned along the way:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The "Action: bedrock:InvokeModel*" IAM Trap
&lt;/h3&gt;

&lt;p&gt;When writing the IAM deny policy, almost every engineer instinctively writes:&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;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bedrock:InvokeModel*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&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;It looks comprehensive. It passes code review. &lt;strong&gt;And it completely fails to block Claude Code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code and modern Bedrock clients do not invoke raw models with &lt;code&gt;InvokeModel&lt;/code&gt;. They use the Bedrock Converse API (&lt;code&gt;bedrock:Converse&lt;/code&gt; and &lt;code&gt;bedrock:ConverseStream&lt;/code&gt;). In AWS IAM, &lt;code&gt;InvokeModel*&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; match &lt;code&gt;Converse&lt;/code&gt;!&lt;/p&gt;

&lt;p&gt;Your deny policy must be explicit:&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;DENY_POLICY_DOCUMENT&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;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Version&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;2012-10-17&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;Statement&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;Sid&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;DenyBedrockOverBudget&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;Effect&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;Deny&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;Action&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;bedrock:InvokeModel&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;bedrock:InvokeModelWithResponseStream&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;bedrock:Converse&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;bedrock:ConverseStream&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;bedrock-mantle:CreateInference&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;bedrock-mantle:GetInference&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;bedrock-mantle:CancelInference&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;bedrock-mantle:CallWithBearerToken&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;Resource&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;*&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;h3&gt;
  
  
  2. The Zero-Token Streaming Ghost Record
&lt;/h3&gt;

&lt;p&gt;When we first enabled deduplication, token counts started randomly dropping by 50%. It turns out that for streaming calls (&lt;code&gt;InvokeModelWithResponseStream&lt;/code&gt; and &lt;code&gt;ConverseStream&lt;/code&gt;), Bedrock frequently emits &lt;strong&gt;two distinct CloudWatch log events under the exact same &lt;code&gt;requestId&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The first event is an initial handshake containing metadata where &lt;strong&gt;every token field is 0&lt;/strong&gt;.&lt;br&gt;
The second event arrives milliseconds later carrying the actual token totals.&lt;/p&gt;

&lt;p&gt;If your deduplicator claims the &lt;code&gt;requestId&lt;/code&gt; on that first zero-token event, it locks the ID in DynamoDB. When the second event with the real 150,000 tokens arrives, the enforcer assumes it's a duplicate and ignores it!&lt;/p&gt;

&lt;p&gt;The best part? Nothing broke. No error, no alarm, no DLQ message. I only caught it in the monthly audit, sentry vs. Cost Explorer, and by then the money was already gone: one developer was $100 on our books and about 3x that on the real bill. But I have a crush on her, so I let it slide (´▽`ʃ♡ƪ) (The CEO knows nothing about this. Lucky me. Please don't forward this post.)&lt;/p&gt;

&lt;p&gt;The fix was a simple guard: we checked for both events.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Ghost Bearer Token
&lt;/h3&gt;

&lt;p&gt;During deployment, we ran into a bizarre bug: &lt;code&gt;CreateInferenceProfile&lt;/code&gt; kept throwing &lt;code&gt;AccessDenied&lt;/code&gt;, even though our IAM deployment role had full administrator rights.&lt;/p&gt;

&lt;p&gt;After three hours of head-scratching, we found the culprit: an engineer's shell had an exported environment variable:&lt;br&gt;
&lt;code&gt;AWS_BEARER_TOKEN_BEDROCK="ey..."&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The AWS CLI and Boto3 silently prioritize this bearer token over standard IAM profiles, invisibly redirecting Bedrock calls to a completely different AWS account where the credentials were unauthorized. Always sanitize your shell before running Terraform:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;bash&lt;br&gt;
unset AWS_BEARER_TOKEN_BEDROCK&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Grok: Two Endpoints, Two Billing Surprises
&lt;/h3&gt;

&lt;p&gt;Amazon Bedrock Mantle (which hosts models like xAI's Grok) does not publish to standard Bedrock invocation logs, and its CloudTrail &lt;code&gt;CreateInference&lt;/code&gt; data event carries no token counts. To track it, we configured a multi-region CloudTrail data event trail for call counts, paired with a Lambda poller running every 5 minutes. The poller bills a closed hour only after AWS Cost Explorer (&lt;code&gt;ce:GetCostAndUsage&lt;/code&gt;) and CloudWatch &lt;code&gt;AWS/BedrockMantle&lt;/code&gt; metrics agree, then splits that hour's tokens across users by call count.&lt;/p&gt;

&lt;p&gt;Then we checked our monthly bill:&lt;br&gt;
Cost Explorer API requests cost &lt;strong&gt;$0.01 per request&lt;/strong&gt;.&lt;br&gt;
Polling every 5 minutes: &lt;code&gt;12 calls/hr * 24 hrs * 30 days = 8,640 requests ≈ $86.40/month&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We were spending nearly $90 a month in API polling charges to monitor a barely used model! Once our clients moved Grok to the bedrock-runtime endpoint (which does write invocation logs, so the normal enforcer prices it in real time), we disabled the poller (&lt;code&gt;state = "DISABLED"&lt;/code&gt; in Terraform) and left the Mantle path in place as a fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second surprise came after the move to runtime.&lt;/strong&gt; Moving Grok to &lt;code&gt;bedrock-runtime&lt;/code&gt; made the enforcer price it in real time, and our report happily showed one engineer at $187 for the month. Cost Explorer said the whole Grok line was far smaller. We compared token counts for one model over the same window:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token type&lt;/th&gt;
&lt;th&gt;Invocation log&lt;/th&gt;
&lt;th&gt;Cost Explorer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;output&lt;/td&gt;
&lt;td&gt;1,644,844&lt;/td&gt;
&lt;td&gt;1,644,844&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache read&lt;/td&gt;
&lt;td&gt;64,101,456&lt;/td&gt;
&lt;td&gt;64,101,456&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;input&lt;/td&gt;
&lt;td&gt;71.7M&lt;/td&gt;
&lt;td&gt;8.5M&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Output and cache-read matched to the token, so Cost Explorer &lt;em&gt;was&lt;/em&gt; tracking runtime calls (a common suspicion: it is, under the same &lt;code&gt;xai.grok-*&lt;/code&gt; usage types as Mantle). Input did not: the log's input is roughly Cost Explorer's input &lt;strong&gt;plus&lt;/strong&gt; the cache reads. Grok reports cached tokens inside &lt;code&gt;inputTokenCount&lt;/code&gt;; Claude does not. Our formula charged them at the input rate and again at the cache-read rate, inflating the Grok number about 3x.&lt;/p&gt;

&lt;p&gt;We deployed the fix and backfilled the DynamoDB counters from the invocation log (subtracting the double-charged cache tokens per request, never resetting), so the live numbers were right again without losing history. The lesson is the same as the substring trap above: a price table is not enough, you also need to know what each provider's token fields &lt;em&gt;mean&lt;/em&gt;, and you should reconcile against the invoice-side numbers (Cost Explorer usage quantities) per model, not just in total.&lt;/p&gt;

&lt;p&gt;Honestly, if I were CEO, everyone would be on Claude. Period. Every new model someone "just wants to try" means I get to study a fresh set of token quirks and patch the enforcer around them. Multi-model is a great strategy for the company and a terrible one for my weekends. They don't pay me enough for this!!!!!!&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Trade-offs &amp;amp; Limitations: What's the Catch?
&lt;/h2&gt;

&lt;p&gt;No architecture is without trade-offs. Here is where &lt;code&gt;bedrock-cost-sentry&lt;/code&gt; shines, and where it falls short:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature / Property&lt;/th&gt;
&lt;th&gt;Reverse Proxy (LiteLLM / Kong)&lt;/th&gt;
&lt;th&gt;AWS Native Budgets&lt;/th&gt;
&lt;th&gt;Bedrock Cost Sentry (This Architecture)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Added Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10ms – 100ms+&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0ms (Direct Path)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Developer Experience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Custom URLs &amp;amp; Endpoints&lt;/td&gt;
&lt;td&gt;Native AWS SDK&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Native AWS SDK (&lt;code&gt;CLAUDE_CODE_USE_BEDROCK=1&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enforcement Speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Instant (In-band)&lt;/td&gt;
&lt;td&gt;8 – 24 hours (Useless)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Near-real-time (1 – 3 min lag)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model Switching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires proxy config&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unrestricted (&lt;code&gt;/model&lt;/code&gt; works natively)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single Point of Failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (Proxy down = Devs blocked)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No (Direct AWS communication)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly Infra Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$40 – $150+ (EC2/ECS/ALB)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.70 (Serverless Pay-per-use)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not an estimate. This is Cost Explorer for October 2026, filtered to the services the stack uses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;October 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch alarms&lt;/td&gt;
&lt;td&gt;~$0.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;td&gt;$0.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;$0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudTrail data events&lt;/td&gt;
&lt;td&gt;$0.007&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SNS email alerts&lt;/td&gt;
&lt;td&gt;$0.002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch Logs ingest (metadata only)&lt;/td&gt;
&lt;td&gt;&amp;lt; $0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.70&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Honest Limitations:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The "Soft Ceiling" Overshoot&lt;/strong&gt;: Because CloudWatch logs take 1 to 3 minutes to batch and deliver, enforcement is asynchronous. The &lt;em&gt;current&lt;/em&gt; request always finishes; the user is blocked on their &lt;em&gt;next&lt;/em&gt; request. If an engineer runs an enormous 200k-token call right as they sit at 99% of their budget, they will slightly overshoot their cap. &lt;strong&gt;Set budgets with 10% headroom.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM Users Only&lt;/strong&gt;: The enforcer attaches inline policies using &lt;code&gt;iam:PutUserPolicy&lt;/code&gt;. This requires long-lived IAM Users. If your developers authenticate via AWS IAM Identity Center (SSO) with ephemeral assumed roles (&lt;code&gt;arn:aws:sts::...:assumed-role/&lt;/code&gt;), attaching user-level inline policies won't work without adapting the architecture to session revocation or SCPs. Long-term Bedrock API keys are fine: they are bound to an IAM user, so the same inline &lt;code&gt;Deny&lt;/code&gt; (including &lt;code&gt;bedrock-mantle:CallWithBearerToken&lt;/code&gt;) cuts them off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual Price Tables&lt;/strong&gt;: AWS does not offer an official, real-time pricing API for Bedrock token rates. The price dictionary in &lt;code&gt;enforcer.py&lt;/code&gt; must be maintained as AWS drops rates or releases new model tiers. A missed model is never free: it falls back to the Opus rate and over-counts until you add its key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List Price, Not Invoice Price&lt;/strong&gt;: The cap compares spend at public list price. If your account has a negotiated discount (ours bills at roughly 80% of list), the guard trips earlier than the invoice would suggest. That is the safe direction, but set caps with it in mind.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  7. Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Quick recap for the people who scrolled straight to the bottom (I see you):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem:&lt;/strong&gt; leadership wanted per-developer Bedrock costs and a way to stop someone &lt;em&gt;before&lt;/em&gt; the bill caught fire. We aren't the payer account, so no cost allocation tags and no CUR. AWS Budgets lag 8–24 hours. A proxy means latency, a new single point of failure, and me on call forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix:&lt;/strong&gt; read the invocation logs Bedrock already writes (metadata only, no prompts), price every call in a Lambda, keep a running total per IAM user in one DynamoDB table, and drop an explicit IAM &lt;code&gt;Deny&lt;/code&gt; the moment someone crosses their cap. Three regions, no proxy, 1–3 minutes from overspend to blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bill:&lt;/strong&gt; about $0.70 a month for the stack, guarding roughly $1,130 of Bedrock. Plus one $46 lesson about pollers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scars:&lt;/strong&gt; IAM wildcards that don't cover &lt;code&gt;Converse&lt;/code&gt;, streaming calls that log zero tokens first, a bearer token pointing at the wrong account, Grok counting cached tokens twice, and a corporate proxy that hates Terraform state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not perfect. It's a soft ceiling, it trusts a price table I maintain by hand, and an admin in the same account can still delete their own Deny. But it's cheap, it stays out of the developers' way, and it answers both of leadership's questions.&lt;/p&gt;

&lt;p&gt;So the next time someone taps my shoulder and says "Show me the bill," I open &lt;code&gt;report.py&lt;/code&gt;, not a resignation letter.&lt;/p&gt;

&lt;p&gt;If your org is going "AI-first" and you're watching the Bedrock graph with sweaty palms: the data to build your own kill switch is already in your account. You just have to wire it up. Then go touch grass. You earned it.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>claude</category>
      <category>devops</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
