Paid to Be Cleaned. But Was It? How I Built an Evidence Engine for Monsoon Drain Desilting on AWS
Indian cities pay contractors by the tonne to desilt drains before the monsoon, but photos, weighbridge slips and GPS traces rarely get cross-checked. SiltProof uses Amazon Textract and Amazon Nova Pro on Bedrock to read the evidence, ten deterministic rules to verify it, and leaves the payment decision to an engineer. A deep dive into the architecture, code and six production lessons, built on Lambda, S3, DynamoDB, Amplify and Amazon Location.
Paid to Be Cleaned. But Was It? How I Built an Evidence Engine for Monsoon Drain Desilting on AWS
Amazon Textract reads the weighbridge slips. Amazon Nova Pro on Amazon Bedrock looks at the photographs. Ten deterministic rules cross-examine them. And an engineer, not a model, decides what gets paid.

TL;DR* The problem: Indian cities pay contractors by the tonne to desilt storm-water drains before the monsoon. The proof (photos, weighbridge slips, truck GPS) arrives in fragments that nobody has time to cross-check.* What I built: SiltProof, which turns a contractor's bill into verified, review and held tonnes, each with the rule, the evidence and the rupee amount behind it.* On AWS: Amplify Hosting, Amazon Location Service, API Gateway, Lambda, S3, Textract, Bedrock (Amazon Nova Pro) and DynamoDB, in one AWS SAM stack in Mumbai.
- The principle: *AI reads. Rules verify. Humans decide.*Live investigation: https://main.d1o7ayjtge649w.amplifyapp.com Try it with your own files: https://main.d1o7ayjtge649w.amplifyapp.com/trial.html (invite code required) Code: https://github.com/Somsubhra-Nandi/siltproof 3-minute demo: https://youtu.be/GF-cus2L-EE
Contents
- Why drains, and why evidence
- What SiltProof does: the Drain 14 case
- The one design rule that shaped everything
- Architecture: two paths, one rules engine
- Reading slips with Textract Queries
- Reading photos with Nova Pro and forced tool use
- Catching a reused photo: perceptual hashes and a DynamoDB index item
- Ten rules as pure functions
- The live trial: letting strangers upload evidence safely
- The map: Amazon Location Service with a browser-safe key
- Testing it all without an AWS bill
- Six things that broke, and what they taught me
- What it costs, what it can't do, and what's next
1. Why drains, and why evidence
Every year before the monsoon, Indian cities pay contractors to clear silt from storm-water drains. A drain that is paid for but never properly cleared carries less water when the rain arrives. Urban flooding is often a drainage problem before it is a rainfall problem.
The work is usually billed by the tonne, and the proof arrives in pieces:
- Site photographs: a "before" showing the silt, an "after" showing a clear channel.
- Weighbridge slips: one per truck, with gross, tare and net weight and a time stamp.
- Truck GPS traces: where the vehicle actually went.
Each document can look perfectly reasonable on its own. The problems appear only when you put them side by side: a slip stamped before the truck left, a route that never reaches the disposal site, the same photograph filed for two different drains.
Now picture the person who has to sign off. A ward engineer approving a single bill might face 18 drain sections, more than a hundred truck trips, and a few weeks before the rains. Checking every slip against every trace by hand isn't realistic, and a single photo can't prove on its own that silt was removed.
That's the gap SiltProof fills: evidence before expenditure. I built it with my teammate for AWS Environmental Hacks 2026, Heat & Water track.
2. What SiltProof does: the Drain 14 case
SiltProof takes a contractor's bill and its evidence and sorts every tonne into three buckets:
| Meaning | What happens |
|---|---|---|
Verified | The evidence supports the claim | Payable |
Review | Something is off but explainable | A person looks |
Held | Independent sources contradict the claim | Payment withheld pending answers |
Our illustrative case study, Bill B1, is one ward, 18 drains and 117 truck trips, at ₹22.32 lakh claimed for 1,240 t. One click on Run verification checks all 117 trips against ten rules. The result is 805 t verified, 65 t for review and 370 t held, worth ₹6.66 lakh.
The headline case is Drain 14, which claims 192 t over 18 trips. Every trip fails three independent hard checks:
- R5, the route never arrives. Every GPS trace stops about 2.2 km short of the approved dump site. Replay haul redraws the truck's recorded route against the haul the bill implies.
- R8, the slip predates the trip. Trip 001's slip records time-in at 06:53. The truck's own GPS says it left the drain at 07:12, nineteen minutes later.
- R3, a reused photograph. The after-photo is the same image already filed for Drain 9. Their perceptual hashes differ in 0 of 64 bits.

Any one of these could be a clerical error or a faulty GPS unit. Three independent sources contradicting the same claim is a reason to hold ₹3,45,600 and ask questions. SiltProof never says "fraud"; it shows the engineer the evidence and the amount at stake.
Just as important, not every flag means hold. On Drain 8 a photo was taken 45 m from the drain, within the error of a consumer phone's GPS. On Drain 6 a GPS trace goes dark for four minutes. Both are amber. The engineer reviews them, approves them, and verified rises from 805 t to 870 t while ₹6.66 lakh stays held.
3. The one design rule that shaped everything
Before writing any code, I set one constraint: the AI never makes the judgment.
- AI reads. Textract reads slips and Nova Pro describes photos. This happens once, when a file arrives.
- Rules verify. Ten deterministic Python rules compare those readings. Every finding names its rule, its evidence and its measurement.
- Humans decide. The engineer approves or holds each flagged drain.
An opaque "fraud risk: 0.87" is useless to an engineer who has to defend a payment to an auditor. "The slip says 06:53, the GPS says the truck left at 07:12, and the tolerance is 10 minutes" is something you can defend, appeal, or correct.
This split also turned out to be the best engineering decision in the project:
- Fast verification. Expensive AI runs at ingestion; verification is pure Python over stored readings and checks 117 trips in under a second.
- Repeatable results. The same evidence always gives the same verdict.
- Testable offline. The rules need no model, so the whole engine runs under pytest without an AWS account.
4. Architecture: two paths, one rules engine

SiltProof runs two execution paths that share the same rules code:
A. The prepared investigation (Bill B1). A static React 19 + MapLibre GL app on AWS Amplify Hosting, with the basemap from Amazon Location Service (Maps v2, Monochrome Light). The bill was run through the real ingest handler and rules before deployment, and the results ship as a JSON snapshot. Every visit is instant and costs nothing per page view: no Lambda, no Textract, no Bedrock.
B. The live trial ("Try SiltProof Yourself"). The full pipeline running on files you upload:
Service | Its job in SiltProof |
|---|---|
Amazon API Gateway | HTTP API with per-route throttling |
AWS Lambda ( api ) | Routes, upload validation, presigned URLs, the rules |
AWS Lambda ( ingest ) | EXIF, perceptual hash, Textract and Bedrock calls; Pillow and imagehash in a layer |
Amazon S3 | One private evidence bucket, with a lifecycle expiry on trial files |
Amazon Textract | AnalyzeDocument with Queries, for the slips |
Amazon Bedrock | Amazon Nova Pro via the apac inference profile, for the photos |
Amazon DynamoDB | One on-demand table: trials, evidence state, readings, quotas, results, with TTL |
AWS IAM / CloudWatch Logs / AWS SAM | Per-function roles, structured JSON logs, infrastructure as code |
Both Lambdas run Python 3.12 on arm64 and share one
backend/ package, so the rules the API runs are byte-for-byte the rules the snapshot was built with.Here's the live flow, because it is not "S3 triggers a Lambda":

- Upload. The browser asks the
apiLambda for a presigned POST and uploads straight to the private bucket; the file never passes through Lambda.
- Validate. On
/complete,apire-checks size, content type and the file's magic bytes.
- Extract.
apiinvokesingestasynchronously.ingestreserves the model call against a quota in DynamoDB, then calls Textract or Bedrock exactly once per file.
- Cross-check.
/analyzeruns only deterministic code. No model is called at decision time.
- Decide. Each check comes back with its status, its evidence and the service that produced each reading. A person takes it from there.
5. Reading slips with Textract Queries
Weighbridge slips have no standard layout; every weighbridge prints its own. Rather than writing a template per printer, I used Textract
AnalyzeDocument with QUERIES : you ask questions in plain English, and Textract returns an answer and a confidence for each.Python
QUERIES = [("TICKET_NO", "What is the ticket number?"),("VEHICLE_NO", "What is the vehicle number?"),("GROSS_WEIGHT", "What is the gross weight?"),("TARE_WEIGHT", "What is the tare weight?"),("NET_WEIGHT", "What is the net weight?"),("TIME_IN", "What is the time in?"),("TIME_OUT", "What is the time out?"),("SITE", "What is the site?"),]client.analyze_document(Document={"Bytes": image_bytes},FeatureTypes=["QUERIES"],QueriesConfig={"Queries": [{"Text": text, "Alias": alias} for alias, text in QUERIES]},)
The aliases become the keys of the parsed reading, so the rest of the system never touches Textract's block structure. Three details made this robust:
- Confidence travels with every field. Anything under 80% is marked low-confidence, and the case file shows the engineer exactly how sure the OCR was about "06:53" (96.5%, as it happens).
- Digit repair is scoped. OCR confuses
O/0,l/1andS/5. The repair runs only on text that already contains a real digit, so a word like "none" is never quietly turned into a number.
- Unreadable is a status, not a guess. If the time-in can't be read, R8 says so and sends the trip to review. It never assumes a value.
6. Reading photos with Nova Pro and forced tool use
For photographs I needed a structured answer: is the channel cleared, and is the load silt or construction debris? Parsing prose for that is fragile. The cleanest pattern I found is the Bedrock Converse API with a single tool and a forced
toolChoice . The tool's JSON Schema is the output format, so the model has to fill it in:Python
PHOTO_TOOL = {"toolSpec": {"name": TOOL_NAME,"description": "Record the assessment of one desilting evidence photo. Call this exactly once.","inputSchema": {"json": {"type": "object","properties": {"cleared": {"type": "boolean"},"load_type": {"type": "string", "enum": ["silt", "debris", "unclear"]},"confidence": {"type": "number", "minimum": 0, "maximum": 1},"notes": {"type": "string"},},"required": ["cleared", "load_type", "confidence", "notes"],}},}}client.converse(modelId="apac.amazon.nova-pro-v1:0",messages=[{"role": "user", "content": [{"text": PHOTO_PROMPT},{"image": {"format": "jpeg", "source": {"bytes": image_bytes}}},]}],toolConfig={"tools": [PHOTO_TOOL], "toolChoice": {"tool": {"name": TOOL_NAME}}},inferenceConfig={"maxTokens": 512, "temperature": 0, "topP": 1.0},)
What I learned along the way:
- Pin the sampling. Before I set
temperature: 0, Nova Pro called one photounclearon one run andsilton the next. An evidence tool can't change its mind between runs.
- The parser distrusts the model. A missing field or an out-of-enum value degrades to
unclearwithok: false. It never crashes, and never becomes a silent pass.
- Scope the rule by photo role. A "before" photo is supposed to show silt, and a picture of a channel contains no truck load, so
load_typeis usuallyunclearfor it. So R4 judges load photos for debris (hard), after photos for clearance (soft), and doesn't judge before photos at all. Without that scoping, every drain would have landed in review.
- The model's notes are observations, not findings. In the case file, Nova's sentence ("Dry concrete channel; bed and invert are visible") is labelled Photo observation, not a verification finding. A vision model can say what a photo shows. It cannot tell you the photo was filed twice; R3 can.
Why Nova Pro? It's first-party, so there's no AWS Marketplace subscription. It's available from Mumbai through the
apac cross-region inference profile, and it handled the forced tool call with zero changes to my parser. Switching models is a single SAM parameter.7. Catching a reused photo: perceptual hashes and a DynamoDB index item
R3 asks: has this image already been submitted on this bill? A cryptographic hash only catches byte-identical files. Crop or brighten a photo and SHA-256 changes completely. So
ingest computes a 64-bit perceptual hash (pHash), which changes only a little when the picture changes a little.Two design decisions made it work:
- Measured threshold. On our test images an exact copy is 0 bits apart, a cropped and brightened copy about 6–10, and the closest unrelated pair 16.
PHASH_DUPLICATE_MAX = 12sits in that gap. It was measured, not guessed.
- One query, not a scan. Evidence items are keyed
EVID#<s3 key>, one partition each, so comparing a photo against every other photo on the bill would mean scanning the table. Instead,ingestwrites a tiny second item per photo,pk = BILL#B1, sk = PHASH#<hash>#<key>, and R3 becomes a singlebegins_withquery on one partition.
There was also a subtle logic bug worth sharing. Two identical photos prove reuse, but not which copy is the original. R3 treats the earliest timestamp as genuine and flags every later appearance. My first dataset accidentally stamped the copies before their source, and the rule cleared the cheating drain and flagged the honest one. The tests caught it. Time is the only honest order you have.
8. Ten rules as pure functions
All ten rules live in one file,
backend/common/rules.py , as pure functions over dictionaries. The API assembles the context; the rules only judge it. They never touch AWS.Rule | Checks | Hard (hold) | Soft (review) |
|---|---|---|---|
R1 | Photo GPS vs the drain's 30 m geofence | > 60 m away | 30–60 m, or no GPS |
R2 | Photo time vs the bill's work window | Outside it | No timestamp |
R3 | Photo reuse (pHash) | ≤ 12 of 64 bits from an earlier photo | |
R4 | What the vision model saw | Load is construction debris | Unclear, or after-photo not cleared |
R5 | GPS trace vs approved dump site | Never enters it | |
R6 | Consecutive trips by one truck | Needs > 40 km/h, or overlaps | |
R7 | Slip net weight vs truck capacity | Over capacity | Unreadable |
R8 | Slip time-in vs GPS | > 10 min apart | Unreadable, or nothing to compare |
R9 | Slip vehicle vs the trip's truck | Different vehicle | Unreadable |
R10 | Drain total vs length × width × depth × 1.4 t/m³ × 1.2 | | Claim exceeds the ceiling |
A hard finding puts the trip on HOLD; a soft one sends it to REVIEW:
Python
def verdict_for(findings):if any(item["severity"] == HARD for item in findings):return HOLDif findings:return REVIEWreturn VERIFIED
Every threshold lives in one place so the UI can explain it:
Python
SPEED_LIMIT_KMH = 40.0 # R6SLIP_TOLERANCE_MIN = 10.0 # R8GPS_GAP_SOFT_S = 180.0 # a trace that goes dark for 3+ minutesPHASH_DUPLICATE_MAX = 12 # R3: measured, see DECISIONS.mdR10_ALLOWANCE = 1.2 # R10's headroom
Four judgment calls mattered more than any code:
Consumer GPS needs a soft band. A strict geofence would punish honest contractors whose phones drifted. So R1 has a soft band: 30–60 m from the drain goes to review, and only beyond 60 m is it a hard fail. That's exactly why Drain 8's 45 m photo reads amber, not red.
Missing evidence is review, never hold, never silence. No slip, an unreadable trace or a failed ingestion each raises its own soft finding. This fixed a nasty bug: a trace that failed to download produced zero GPS points, which looked exactly like "the truck never reached the dump site." An S3 permissions problem must never read as "the contractor stole the money."
Photo findings taint the drain; trip findings taint the trip. A reused after-photo doesn't discredit one lorry load; it discredits the claim the photo was submitted to support. So R1–R4 and R10 apply to every trip on the drain, while R5–R9 judge a single trip.
Messages name their measurements. R8 doesn't just say "time mismatch". It says: "The slip records time-in at 06:53, 19 minutes before the truck left the drain at 07:12. The GPS trace never reaches the dump site; its last fix, at 07:37, is 44 minutes after the slip's time-in." Writing that sentence exposed a real inconsistency in my own numbers (40 vs 44 minutes), because the rule had to say which GPS time it compared against.
And what does a decision do to the money? Approve releases everything the rules withheld on that drain, because an engineer who has visited the site can overrule the evidence; that's the point of showing it. Hold moves the pending review tonnage to held.
9. The live trial: letting strangers upload evidence safely
Bill B1 proves the experience at the scale of a real bill. But judges, rightly, want to try their own files, and a public upload feature with paid AI behind it needs real engineering. My teammate built the trial screens and the isolated trial backend; I integrated and hardened it for launch.

Access without accounts. Creating a trial requires an invite code, held as a
NoEcho stack parameter that is never in the frontend bundle. Each trial then gets a 256-bit bearer token, and DynamoDB stores only its SHA-256. A wrong token and an unknown trial get the same 404, so nothing leaks.Presigned POST, not PUT. A presigned PUT URL can't bound the file size. A POST policy can, so S3 itself refuses an oversized or retyped file:
Python
post = s3().generate_presigned_post(Bucket=bucket,Key=key,Fields={"Content-Type": request["contentType"]},Conditions=[{"Content-Type": request["contentType"]},["content-length-range", 1, request["sizeBytes"]],],ExpiresIn=limits.UPLOAD_URL_TTL_S, # 300 s)
Async invoke, not an S3 trigger. Trial files live under
trials/ , outside every ingest notification prefix, so an upload alone can never cost a model call. Only after /complete re-validates the bytes does api invoke ingest asynchronously. No new Lambda and no queue: the existing ingest function already had Pillow and the AI permissions.Quotas that fail closed. Every Bedrock and Textract call is reserved before it is made, per trial and per day, with a single conditional DynamoDB write:
Python
store.table().update_item(Key={"pk": f"QUOTA#{day}", "sk": kind},UpdateExpression="SET #n = if_not_exists(#n, :zero) + :one, #t = :ttl",ConditionExpression="attribute_not_exists(#n) OR #n < :limit",ExpressionAttributeNames={"#n": "count", "#t": "ttl"},ExpressionAttributeValues={":zero": 0, ":one": 1, ":limit": int(limit),":ttl": now_epoch() + limits.QUOTA_TTL_S},)
Reservations are never refunded, because a call that failed may still have been billed. The caps are 15 Bedrock and 8 Textract calls per trial, and 200 Bedrock and 100 Textract calls per day across all trials. A file whose bytes were already read is never sent again.
Honest statuses for partial evidence. A judge might upload one photo and nothing else. The trial adapter checks each rule's prerequisites before calling the same
rules.py functions, and reports one of six statuses:Status | Meaning |
|---|---|
PASS | Ran on adequate evidence; nothing against the claim |
CONSISTENT | Ran against an approximate reference; no contradiction, not proof |
REVIEW / FAIL | Soft or hard failure under the rule's own threshold |
INCONCLUSIVE | Evidence exists but can't decide (an unreadable field, a circular geometry) |
NOT_EVALUATED | A prerequisite is missing, and it says which |
It never reports a pass it could not test. One example: a drain location drawn from the same photos makes R1
INCONCLUSIVE , because checking photos against a line drawn through them proves nothing. We designed the trial around a real case we captured: 11 photographs of an unlined channel in New Town, Kolkata, with five GPS fixes of unknown accuracy and no slips or traces. The honest answer for evidence like that is mostly "insufficient evidence", and the system is built to say so. The trial also adds three arithmetic checks: quantity × rate = amount (T1), gross − tare = net (T2), and whether the slips add up to the claim (T3).Retention. Trial objects expire through an S3 lifecycle rule, and trial records through DynamoDB TTL, about 48 hours after creation. Delete trial removes everything at once.
10. The map: Amazon Location Service with a browser-safe key
The investigation draws drain rings, GPS traces, the dump-site geofence and the replayed haul over an Amazon Location Service Maps v2 basemap (Monochrome Light), tinted to match the app's survey-sheet palette.
The browser holds no AWS credentials. It holds a Location API key, which is public by nature, so it has to be scoped tightly:
Bash
aws location create-key --region ap-south-1 \--key-name siltproof-maps-browser \--expire-time 2026-11-30T00:00:00Z \--restrictions '{"AllowActions":["geo-maps:*"],"AllowResources":["arn:aws:geo-maps:ap-south-1::provider/default"],"AllowReferers":["https://main.<app-id>.amplifyapp.com/*"]}'
Every
geo-maps action is a read, so the key can fetch map styles and tiles and nothing else: no places, no routes, no resource changes. It only works from our site's referrer, and it expires. If the style can't load, the app falls back to a bundled OpenStreetMap basemap after 6 seconds and says so on the map.Replay haul is my favourite detail in the frontend. It draws only the recorded GPS points, from first fix to last, and nothing past the last fix. When the trace stops short, it measures the gap and labels it ("2.2 km short") instead of animating a truck to a place it never went. A demo animation must not invent evidence either.
11. Testing it all without an AWS bill
The project has 472 backend tests (pytest, with moto for S3 and DynamoDB and
MOCK_AWS=1 for Textract and Bedrock) and 114 frontend tests (Vitest + jsdom). No AWS account is needed to run any of them.Three habits made this work:
- Mocks use the real parsers. Mock mode doesn't branch around the Textract and Bedrock parsing code. It feeds recorded responses (fixtures) into the same parsers that live mode uses, so a parser bug can't hide behind a mock.
- There's a ground truth.
ground_truth.jsonrecords the expected verdict of every rule for the generated dataset. When a rule and the ground truth disagree, one of them is wrong, and we decide which before changing either.
- Every script that can spend money defaults to not spending it. Seeding, resetting and trip generation are dry runs unless you pass
--live, and--liveprints the call plan and a billable-call estimate first.
The prepared investigation itself is built by
scripts/make_demo_fixtures.py . It runs the dataset generators, a moto S3 and DynamoDB, the real ingest handler and the real POST /verify , then saves the API's responses as the snapshot. The public demo is the actual pipeline's output, frozen.12. Six things that broke, and what they taught me
1. The API answered 500 on every route the night we deployed the trial backend. The
api Lambda imported a module that pulled in Pillow, which only exists in the ingest function's layer. Unit tests passed, because locally everything is installed. Luckily nothing public used the API yet. Fix: a packaging test that imports the API handler with only its own dependencies. Test what you actually ship.2. My first
sam deploy rolled back. I'd reserved concurrency for ingest to keep Bedrock from throttling during seeding. A new account's Lambda concurrency quota is 10, and Lambda refuses any reservation that leaves fewer than 10 unreserved. It's now a parameter ( IngestReservedConcurrency , default 0). Check your account quotas before your template assumes them.3. A model worked once, then never again. My first vision model answered one call, then returned
403 AccessDeniedException on every call after. It was sold through an AWS Marketplace subscription that our credits-based account couldn't complete. Switching to the first-party Amazon Nova Pro fixed it, with no parser changes. First-party models remove a whole class of billing surprises.4. Phone photos were too big for Bedrock. Converse accepts images up to 3.75 MB, and modern phone photos are often bigger. Above 3.5 MB (or 8,000 px),
ingest now sends Bedrock a 1,568 px processing copy, while EXIF and the perceptual hash always come from the untouched original. Know your API's input limits before your users find them.5. The map was blank, and no unit test noticed. The production build never emitted MapLibre's web worker, so the map rendered nothing in any mode. I found it by screenshotting the running app, the first thing the screenshot showed. The fix was importing the worker through Vite's
?url and calling setWorkerUrl . Screenshot your real build.6. Seeding from Windows would have ingested nothing. S3 keys built with
str(Path) became photos\B1\... on Windows, so no ingest prefix matched. The offline oracle failed 27 tests the same way, which is how we caught it. Keys are now built with as_posix() . Cross-platform bugs hide in string conversion.13. What it costs, what it can't do, and what's next
Cost. The public investigation calls no Lambda, Textract or Bedrock at all; it's static files plus map tiles. A live trial costs a few cents: our end-to-end smoke test, one Nova Pro call and one Textract call, was about $0.02. The daily caps bound the worst case.
Honest limitations.
- Bill B1 is illustrative. Real contractor bills and their evidence aren't public. So B1's drains, trips, slips and GPS traces are prepared case-study data, and its 40 photographs are licensed illustrative images (CC0 and Pexels), not field captures. The rules and the totals are real, though: the verdicts and amounts come from running the actual rules code.
- Thresholds need real data. The pHash threshold of 12 was measured on our test images. Real photos of the same drain from slightly different angles might land closer together.
- It can't prove a drain was cleaned. No rule proves fraud. Each one records a contradiction, with its evidence, for a person to weigh.
What's next. I'd like real bills and evidence from a municipal partner, thresholds tuned on real field photos, and a richer review workspace where the engineer can request evidence from the contractor directly.
What I'd tell anyone building with AI on AWS
- Put the AI at the edges. Let models read messy, real-world inputs and let deterministic code judge them. Explainability, repeatability and cheap tests come for free.
- Use forced tool calls for structured output. A JSON Schema in a Converse
toolConfigbeats parsing prose every time.
- Design for missing data before you design for fraud. Most anomalies are faulty devices and clerical errors. The most valuable thing a checker can say is "I couldn't check this".
- Make expensive calls happen once, on purpose. Run them at ingestion, quota-reserve them, and never on page load.
- Write your decisions down. Our
DECISIONS.mdrecorded why R1 has a soft band, why the threshold is 12 bits and why Nova Pro. It saved us more than once, and it made this post easy to write.
SiltProof doesn't try to prove a drain was cleaned. It makes sure that before public money is paid, someone has checked whether the evidence agrees with itself. Because flood preparedness begins before the rain.
Explore the live investigation: https://main.d1o7ayjtge649w.amplifyapp.com
Read the code: https://github.com/Somsubhra-Nandi/siltproof
Built for AWS Environmental Hacks 2026 (Heat & Water). I designed the architecture and SAM infrastructure, the evidence ingestion pipeline (EXIF, perceptual hashing, Textract, Bedrock), the verification rules and API, the dataset and prepared case, the investigation interface and its Amazon Location map, and the AWS deployment. My teammate @Subhra-Nandi built Try SiltProof Yourself: the trial API contract, the isolated trial backend, its screens and its tests. I integrated and hardened it for launch.
Tags: #AWS #AmazonBedrock #AmazonNova #AmazonTextract #AWSLambda #AmazonDynamoDB #AmazonLocationService #AWSAmplify #Serverless #AWSSAM #GenerativeAI #Sustainability #CivicTech
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article