For the AWS Builder Center’s Weekend Annoying Task Challenge, I built CostWrap - an app that turns your AWS bill into a plain-English digest on demand.

The contest theme was:

Build a simple app that automatically handles one annoying, repetitive part of your week. Ideas to spark your creativity:

  • An app that sorts or triages something for you
  • An app that drafts a routine message or update
  • An app that gathers what you need in one place
  • An app that automates a weekly chore

What was that one annoying task?

Opening Cost Explorer and squinting at graphs to figure out why the bill moved is a weekly chore I kept putting off, then paying for later when a spike went unnoticed too long. CostWrap kills that task.

Pick a period, hit Analyze, and a digest tells you what moved and by how much - “AWS costs this year to date are 100% lower compared to last year” - instead of a graph you have to interpret yourself. The output is plain English on purpose - readable by a non-technical stakeholder, not just someone who already knows how to read a Cost Explorer chart.


What I built

CostWrap logoCostWrap is live at d2akizukj8wjei.cloudfront.net - pick a period, hit Analyze, and your bill comes back in plain English.

CostWrap is a single-page app with one job: explain what changed in your AWS spend.

  • Period picker - last 3 days, last 7 days, trailing quarter, or year-to-date
  • Analyze - pulls that window plus the equivalent window before it from Cost Explorer, computes per-service deltas, and Bedrock narrates the result
  • History - every digest is stored, so past runs stay browsable below the current one

Period picker with 3 days, 7 days, Quarter, and YTD options, and the Analyze button

The period picker before hitting Analyze. Each run costs about a cent - one real Cost Explorer call plus one Bedrock call.

CostWrap digest showing a YTD headline, total cost comparison, and per-service movers with percent deltas

The digest after Analyze - a headline, the total cost comparison, and the biggest movers, each with a plain-English note.

History list showing past digests with their period and date range

The history list. Every past run stays browsable, tagged with its period and date range.

The model never does arithmetic - Python computes every delta first, and the prompt hands Bedrock the finished numbers with an explicit instruction not to invent or recompute anything. Bedrock’s only job is turning a diff table into a headline, a short summary, and a one-line note per mover.



How I built it

Same constraint as before: free tier only, deployed in a weekend. The stack:

  • Backend: A plain Lambda handler (Python, no framework) behind a Function URL - no API Gateway. Three routes: GET /digests (list, newest first), GET /digests/latest, and POST /run (runs the pipeline for a given period).

  • Cost data: One ce:GetCostAndUsage call per Analyze click, paginated, grouped by service. Each period compares a window against the equivalent window immediately before it - 3 days vs the prior 3, 7 vs the prior 7, trailing 90 days vs the prior 90. Year-to-date compares against the same span last year, with a composition-only fallback (total and top services, no delta) if Cost Explorer’s 13-month history can’t reach back far enough. Windows end at the most recent complete day, not today - Cost Explorer data lags about 24 hours, so including today would deflate the current period and skew every delta.

  • The diff: Python sums each service’s cost per window, then computes absolute and percent deltas and sorts by the size of the move. This table is the only thing Bedrock ever sees as “fact” - the prompt hands it the diff as authoritative JSON and tells the model not to compute or invent numbers, and not to invent specific causes beyond naming what kind of usage typically drives a service’s cost.

  • AI: Amazon Bedrock, Nova Lite, called through the Converse API. The response is JSON only - a headline under 80 characters, a 2-4 sentence summary, and up to 5 movers each with a one-line note. Same defensive code-fence stripping as before, since Nova occasionally wraps JSON output in markdown fences.

  • Storage: One DynamoDB table, costwrap-digests, on-demand billing. Partition key USER#default, sort key an ISO timestamp - every run is a new item, so history is just a query sorted newest-first.

  • Frontend: Same vanilla HTML/CSS/JS approach as WeekWrap, reusing its design tokens and component styles with cost deltas re-themed (up = red, down = green). Stored in S3, served over HTTPS via CloudFront for the same reason as before - S3 website endpoints are HTTP-only.

  • IaC: CDK v2 Python, one stack. The Lambda’s IAM role is scoped to the one DynamoDB table and the specific Nova Lite model ARN - Cost Explorer’s ce:GetCostAndUsage action doesn’t support resource-level scoping, so that policy statement is necessarily Resource: "*".


AWS services used

ServiceRole
Lambda (Python 3.12)Backend - plain handler behind a Function URL
Lambda Function URLHTTP endpoint with CORS, no API Gateway
Cost Explorer APIce:GetCostAndUsage - one call per Analyze click
Amazon Bedrock - Nova LiteTurns the computed diff into a plain-English digest
DynamoDB (on-demand)costwrap-digests table - stores every digest as history
S3 (static website hosting)Frontend - HTML/CSS/JS
CloudFrontHTTPS CDN in front of S3 - fixes HTTP-only limitation of S3 website endpoints
IAMTask role scoped to the DynamoDB table and the Nova Lite model ARN; Cost Explorer access is necessarily unscoped
CDK v2 PythonIaC - single stack deploys everything

Everything except Cost Explorer API calls and Bedrock inference tokens is within the AWS Free Tier. Cost Explorer charges $0.01 per API call - one per Analyze click - and Bedrock Nova Lite tokens run a fraction of a cent per digest.


Architecture overview

  • The app is not agent-triggered - it runs on demand, from a manual click
  • The frontend is stored in S3 and served over HTTPS via CloudFront - the same pattern as WeekWrap
  • All API calls go directly from the browser to the Lambda Function URL over HTTPS - no API Gateway, no load balancer
  • Lambda handles every route: DynamoDB reads for history, a Cost Explorer call plus local computation for the diff, and a synchronous Bedrock call for the narrative
  • No ALB, no API Gateway, no Fargate - the only per-use costs are Cost Explorer calls and Bedrock tokens

CostWrap architecture - browser to CloudFront/S3 for static frontend, browser to Lambda Function URL for Analyze, Lambda to Cost Explorer, DynamoDB, and Bedrock Nova Lite


What I learned

  1. Let the model narrate, never calculate. Every number in the digest comes from Python’s diff computation, handed to Bedrock as authoritative JSON with an explicit instruction not to compute or invent figures. Language models are unreliable at arithmetic; the fix is to never ask them to do any.
  2. Cost Explorer data lags about a day. Windows have to end at the most recent complete day, not “today” - including a partial day would understate the current period and throw off every delta calculation.
  3. Cost Explorer’s IAM actions don’t support resource-level scoping. ce:GetCostAndUsage has to be granted against Resource: "*" - unlike the DynamoDB and Bedrock grants, which scope cleanly to a specific table and model ARN.
  4. A dry-run fixture mode pays for itself immediately. Cost Explorer charges per call, so local development replays a captured response instead of hitting the real API - zero-cost iteration on the diff and narration logic.
  5. A shared design system speeds up the next build. Reusing WeekWrap’s CSS tokens and component styles meant CostWrap’s frontend was mostly a copy-and-retheme job - the biggest frontend decision was just remapping cost deltas to red-for-up, green-for-down.

Notes

  1. All AWS services used are within the Free Tier at personal usage volumes, aside from per-use Cost Explorer API calls ($0.01 each) and Bedrock inference tokens (fractions of a cent per digest).
  2. The DynamoDB table uses RemovalPolicy.RETAIN, so digest history survives a stack teardown - delete it manually from the console for a clean removal.