
Weekend Annoying Task Challenge: CostWrap
How I built CostWrap - an app that explains your AWS bill in plain English on demand, using Cost Explorer and Amazon Bedrock Nova Lite.
This is my submission for the AWS Builder Center's Weekend Annoying Task Challenge - turn one annoying, repetitive task into an app.
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.
Vision and what the app does
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

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
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, andPOST /run(runs the pipeline for a given period). - Cost data: One
ce:GetCostAndUsagecall 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 keyUSER#default, sort key an ISO timestamp - every run is a new item, so history is just a query sorted newest-first. - Frontend: Vanilla HTML/CSS/JS - no framework, no build step. Cost deltas are colour-coded (up = red, down = green). Stored in S3, served over HTTPS via CloudFront - 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:GetCostAndUsageaction doesn't support resource-level scoping, so that policy statement is necessarilyResource: "*".
AWS services used
| Service | Role |
|---|---|
| Lambda (Python 3.12) | Backend - plain handler behind a Function URL |
| Lambda Function URL | HTTP endpoint with CORS, no API Gateway |
| Cost Explorer API | ce:GetCostAndUsage - one call per Analyze click |
| Amazon Bedrock - Nova Lite | Turns 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 |
| CloudFront | HTTPS CDN in front of S3 - fixes HTTP-only limitation of S3 website endpoints |
| IAM | Task role scoped to the DynamoDB table and the Nova Lite model ARN; Cost Explorer access is necessarily unscoped |
| CDK v2 Python | IaC - 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.
- 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.
1
2
3
4
5
6
7
8
9
Browser
|
|-- HTTPS --> CloudFront --> S3 bucket (static frontend)
|
'-- HTTPS --> Lambda Function URL
|
+-- Cost Explorer (GetCostAndUsage)
+-- Bedrock Nova Lite (digest narrative)
+-- DynamoDB (digest history)What I learned
- 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. - 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. - Cost Explorer's IAM actions don't support resource-level scoping.
ce:GetCostAndUsagehas to be granted againstResource: "*"- unlike the DynamoDB and Bedrock grants, which scope cleanly to a specific table and model ARN. - 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. - Colour is doing most of the interpretive work in the UI.
Once cost deltas are colour-coded (red for up, green for down), the eye finds the biggest mover before reading a single number - the numbers confirm what the colour already flagged.
Link to the live app
Notes
- 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).
- The DynamoDB table uses
RemovalPolicy.RETAIN, so digest history survives a stack teardown - delete it manually from the console for a clean removal.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article