ShipDoc, part 1: a perfect score locally, 0.9869 on Lambda
Our Averis × Monash Hackathon entry checks shipping documents. Getting it onto AWS was my job, and the cloud scored lower than my laptop for a reason I didn't expect.
On this page
At the Averis × Monash Hackathon 2026 I was the registered team lead of Claude's Plan, a five-person team. Our entry is ShipDoc (shipdoc.org, docs at docs.shipdoc.org). My part was the cloud deployment, the Android build, the rename to ShipDoc and the documentation site. Most of the pipeline itself was written by my teammates.
What ShipDoc does
A shipping-documentation team works from a mixed inbox: requests for Shipping Instructions, invoice queries, automated notices, spam, and the emails that matter most. In those, a carrier sends a draft Bill of Lading (BL) that someone has to check, field by field, against the customer's Shipping Instruction (SI). A wrong port or container count that survives into the issued BL is expensive to fix.
ShipDoc reads that inbox and, for every email:
- sorts it into one of five categories: BL comparison, SI request, invoice query, general or spam;
- for BL checks, reads the SI and BL attachments (
.txt,.pdf,.docxor.xlsx) and finds seven fields, whatever they're labelled (POD and Discharge Port are the same field); - compares them and names the exact fields that differ;
- escalates instead of guessing when an attachment is missing, unreadable, the wrong document or has a blank value.
The rule the whole design follows is "AI reads, code decides." Rules handle classification and extraction first, and an AI model is only asked when they're unsure. The comparison itself is plain, unit-tested code, so a mismatch can always be re-derived from the two values on screen.
On the organisers' synthetic 520-email dataset, the rules alone scored 1.000 on their formula: 46 of 46 defective BLs caught with the exact wrong fields, no false alarms, and all 20 escalations with the right reason.
Getting it onto AWS
I'd never deployed anything on AWS. The setup we ended up with, all in ap-southeast-1:
| Piece | Runs on |
|---|---|
| API (FastAPI) | Lambda, from a container image in ECR, wrapped by Mangum, with a public Function URL |
| Results | DynamoDB, pay per request |
| Website (React) | Amplify Hosting, rebuilt from GitHub on every push |
| Android app | The same React code wrapped by Capacitor, as an APK pointed at the Function URL |
| AI | Gemini on the live deployment; the same interface also supports Bedrock and the Claude API |
The DynamoDB part exists because of how Lambda works. The local version kept results in memory or in a JSON file. Lambda runs several short-lived copies of the function side by side, so a result written by one copy is invisible to the next. The storage layer already sat behind an interface, so the cloud got its own DynamoDBStore.
Some first-timer lessons from the deploy, all of which went into the guide on the docs site:
- Lambda's default timeout is 3 seconds. Ours runs at 2 minutes with 1024 MB.
- Container images need
--provenance=falsewhen built with a recent Docker, or Lambda rejects them. - CORS configured in two places (the Function URL and the app) sends duplicate headers, and browsers reject that.
- The AWS console quietly switches region between pages, which makes resources you just created look missing.
Why the cloud scored 0.9869
With everything deployed, we ran the full dataset against the live API and compared it with a local run. The scores came out as:
| Where | Score |
|---|---|
| Local | 1.000 |
| Lambda | 0.9869 |
Same code, same data. Comparing the two runs email by email showed exactly one PDF that read fine locally but failed on Lambda.
The cause was git, not AWS. Some of us develop on Windows, and git was set up to convert line endings between Windows (CRLF) and Linux (LF). It treated some PDFs as text and rewrote the line-ending bytes inside them. A PDF records the byte position of every object, so changing a few bytes is enough to corrupt it. The copy on the laptop was fine. The copy that went through git into the container wasn't.
The fix, in .gitattributes:
*.pdf binary
*.docx binary
*.xlsx binary
*.png binary
*.apk binary
binary tells git never to touch those files. We also pinned the PDF library versions so Lambda and the laptops parse PDFs the same way, and made the reader retry a damaged file with its line endings restored. After that, Lambda scored 1.000 as well.
If the cloud and your laptop disagree, diff the results per item before suspecting the cloud.
Renaming, the APK and the docs site
- The name came from the domain. We picked ShipDoc partly because
shipdoc.orgwas available. I bought it through Cloudflare, pointed it at Amplify, and renamed the website and the Android app. - The APK was rebuilt against the live API. It also has a setting for changing the API address, so one build works against a laptop, a phone on the same network or the cloud.
- The docs site is a VitePress site deployed with Cloudflare Workers Builds, separate from the app, so a broken docs build can't take the product down. It publishes from an allow-list of files: internal notes like the build plan, the frontend/backend handoff and the compliance review can never end up public by accident.
What I took away
- Deploy early. The corrupted PDF was invisible until the code ran somewhere that wasn't a laptop.
- Mark binary files as binary. One line of
.gitattributeswould have saved a lot of confusion. - Let code decide what code can decide. Keeping AI out of the comparison meant every result could be explained and reproduced.
The AWS account came with the hackathon and had an end date. Part 2 is about moving ShipDoc to my homelab before it expired.