-
-
Tap Arrived and WhatsApp asks for a location pin. Its server timestamp starts the clock, and the pin is checked against the yard.
-
The WhatsApp Flow opens the camera only, so a saved photo can't stand in for the signed bill of lading. Claude on Bedrock reads the times.
-
On a real phone: 14 min 53 s at the dock on a contract with 5 minutes free, so it owes $9.88. Plain code sets the amount, not the model.
-
The evidence pack is signed with AWS KMS. Raise the amount from $9.88 to $9,999 and the signature check in your browser fails.
-
Both pins were about 4.6 km outside the facility, so DockClock filed nothing and handed the load to a person.
-
Nothing goes out until dispatch taps Approve. Then an AgentCore agent emailed the claim through Amazon SES, 11.5 s after the tap.
-
One Step Functions run per load. Bedrock reads the photo; code and a Cedar policy decide. Drivers chat in English, Spanish or French.
A truck driver can sit at a dock for hours past the free time in the load's contract. That wait is owed as detention pay, and much of it never gets paid: carriers are paid on only 55% of the detention invoices they send (ATRI, 2024), and 17% of OOIDA members said they collect no detention pay at all. I built DockClock so a small fleet can prove the wait from the driver's WhatsApp, in English, Spanish or French, and claim exactly what the contract owes. The model reads the paperwork, plain code sets the amount, and the dispatcher taps Approve before anything is sent.
On 24 test loads, the usual rule, "bill every stop over 2 hours", asks for $2,554.18, and $1,314.18 of that (51%) wouldn't hold up under the contracts. DockClock files the $1,245.00 they owe, in 9 claims, and nothing they deny. I wrote the 24 loads and their right answers myself, from each contract's text, so this checks DockClock against the contracts, not against real payouts. npm run scorecard recomputes it with no AWS account.
On a real phone (load LIVE-mvpoqheg), the truck sat 14 min 53 s at the dock, the contract owed $9.88, and the claim email went out 11.5 s after the dispatcher tapped Approve. The amount is small because the demo contract gives 5 minutes free at $60 an hour. On another real-phone load (LIVE-mvlaajde), both pins were about 4.6 km (4,620 m) outside the facility, so DockClock filed nothing and handed the load to a person.
- Who pays: a freight factor, the company that buys a small carrier's invoices and collects from the broker. I propose 10% of the detention collected, so nobody pays unless money comes in. No factor has been contacted yet.
- Try it in 3 minutes, no login: https://dockclock.live/judges walks one recorded load in seven steps. Your browser checks the AWS KMS signature, then you raise the amount to $9,999 and watch the check fail.
Inspiration
I started from that 55%: the driver waited, the fleet billed, and it still often goes unpaid.
Each load's contract, the rate confirmation, allows some free waiting, usually 2 hours. After that, detention is owed by the hour, under a clause each broker and shipper writes its own way. ATRI estimates $5,103.95 of unpaid detention per truckload driver per year, so a 40-truck carrier loses about $204,000 a year. The US DOT Inspector General linked detention to $1.1 to $1.3 billion a year in lost driver earnings, and found that 15 more minutes at a facility raises the expected crash rate by 6.2% (report ST2018019, 2018, from 2013 data).
Claims mostly fail on conditions the clause spells out: no notice before detention started, no signed bill of lading (BOL, the delivery paper with the in and out times), a late arrival, or a claim sent after the window closed. A small fleet has nobody to track every clause, and ATRI found many carriers don't bill at all because they expect a refusal. A claim whose evidence matches the clause is hard to refuse. One that doesn't should never be sent.
What it does
One load, start to finish
- Dispatch. The driver gets the load on WhatsApp with an Arrived button.
- Arrival. The driver taps Arrived and shares a location pin. WhatsApp stamps it with server time, DockClock checks it against the facility's geofence, and the clock starts.
- Notice. Before free time runs out, Amazon EventBridge Scheduler fires the required notice and Amazon SES emails it.
- Release. The driver taps Released, which stops the clock, and a second pin within 10 minutes confirms it. A WhatsApp Flow opens the camera for the signed BOL (camera only, so an old photo can't be used), then asks for the paper times and the reason for the wait.
- Verdict. Claude Sonnet 4.5 on Amazon Bedrock reads the BOL photo as a cross-check. The clause engine, plain TypeScript with no model in it, checks the pins and paper times against the contract. It answers BILLABLE with an amount, NOT_BILLABLE with the clause's reason, or NEEDS_HUMAN.
- Seal. The evidence and verdict are signed with AWS KMS and stored in Amazon S3 Object Lock. Anyone with the public key can check the pack.
- Approval. The dispatcher gets Approve claim and Hold on WhatsApp.
- Claim. On Approve, the claims agent emails the claim through SES with the signed pack attached.
Drivers without WhatsApp get the same loop over RCS. If a WhatsApp send fails, the load switches to RCS once.
What the AI can't do
The model reads the photo and writes the opening line of the claim email. That's all. Code sets the amount, a person approves it, and a Cedar policy plus a check in the claim tool refuse the send unless the amount and digest match the sealed pack.
Who pays, and who uses it
- Who pays: a US freight factor. Factors already hold the BOLs, a provable claim is one more invoice to advance on, and one factor serves hundreds of fleets.
- Who uses it: small US truckload carriers, about 10 to 50 trucks, and owner-operators. Drivers stay in WhatsApp, with no app to install.
- Business model (proposed, not validated): about $15 per truck per month, so a 40-truck carrier pays 40 × $15 × 12 = $7,200 a year. Or, through a factor, 10% of detention collected: if that carrier recovers a quarter of its $204,000 (my assumption), it collects $51,000, the fee is $5,100, and it keeps $45,900.
What I've measured
- The scorecard above, rerun by CI on every push.
- The whole loop from a real phone (
LIVE-mvpoqheg, 2026-10-26, on the demo yard, which accepts any location). The notice reached my test mailbox 3 min 51 s before free time ended. A second pin 14 s after the Released tap confirmed it. Claude read the times off a fictional BOL (out 10 minutes after the tap, inside the 15-minute tolerance). The claim email and a PDF summary followed the Approve tap. - A photo it couldn't read (
LIVE-mvlb0qzq, $17.28): marked "not legible" instead of guessed. - The photo reader: 26 of 26 fields right on 7 fictional test photos.
- Earlier pieces: a real Approve tap to a claim email in 14 s (
SMOKE-mve8yfv3, made-up driver messages replayed through the deployed system).
What's real and what isn't
- Fictional companies. The carrier, factor, brokers, shippers and loads are made up. The five contract clauses are modelled on published shipper and broker terms.
- Test data, and I wrote the 24 right answers myself from each clause. It's a consistency check, not paid or denied claims.
- A pin can be moved, so when pins, paper times and photo disagree, the load goes to a person.
- The signature covers what I received. It shows the record hasn't changed since sealing, not that the paper is honest.
- The demo contract (5 minutes free, $60 an hour, a $30 cap) is not in the scored set.
- RCS has reached a filed claim only on smoke loads. On a real phone it got from dispatch to a BOL photo (
LIVE-mve1ve6d). My RCS agent is a Google testing agent, so judges can't try it on their own phones.
How we built it
Everything runs in one AWS account, defined with the AWS CDK in TypeScript as five AWS CloudFormation stacks.

- In: WhatsApp and RCS messages arrive through AWS End User Messaging, then Amazon SNS, Amazon SQS and a Lambda that records each event once in DynamoDB.
- One AWS Step Functions run per load: the Arrived pin starts the clock, EventBridge Scheduler sends the notice through SES, and the Released tap, second pin and BOL photo (read by Bedrock) go to the clause engine.
- Seal: signed with AWS KMS, kept in S3 Object Lock.
- Out: after the Approve tap, an AgentCore agent, allowed only through a Gateway with a Cedar policy, emails the claim through SES.
- Console: CloudFront serves the React app and a read-only API that shows demo loads only.
The details that matter
- AWS End User Messaging sends WhatsApp with
SendWhatsAppMessageand RCS withSendRcsMessage. - Amazon Bedrock AgentCore. A Harness runs the claims agent on Claude Sonnet 4.5. A Gateway serves its three tools over MCP with IAM auth. AgentCore Policy (Cedar, enforce mode, default deny) allows the claim tool only with an approval id and an amount above $0 and at most $5,000.
- AWS KMS signs each pack with an ECDSA P-256 key. S3 Object Lock keeps the packs, photos and raw emails for 30 days.
- The console's read API serves only demo loads and refuses any response containing phone numbers, user ids or coordinates.
How DockClock uses WhatsApp
| WhatsApp feature | Its job |
|---|---|
| Utility templates (3 templates in 3 languages, all approved) | Start the load, confirm the notice went out, ask the dispatcher to approve |
| Quick-reply buttons | Carry the load id and a one-time code, so an old message can't act on a new load |
| Location requests | Arrival and release pins with server time, checked against the geofence |
| A Flow with a camera-only photo picker | The signed BOL and its paper times; a saved photo can't stand in |
| Business-scoped user IDs (BSUID) | The driver stays matched when WhatsApp sends a BSUID instead of a phone number |
| Opt-in record and STOP/START | No message without opt-in; STOP works in all three languages |
Challenges we ran into
- One driver, two identities. I dispatched to a phone number, but WhatsApp sent replies with a BSUID, so taps matched no load. Now every button and Flow carries its own load id and one-time code.
- A pre-filled time isn't evidence. The Flow pre-fills the BOL times from the pins, so only times the driver changes to match the paper count as proof.
- Keeping the model away from the money. Telling it to copy the amount doesn't stop it writing another. Now the amount reaches Cedar as integer cents, and a second filing for the same load is refused.
- The agent got a date wrong. It paired a local time with a UTC date. The tool now hands it times labelled with their zone.
- WhatsApp message ids carry the sender's phone number. Packs now store a short sha256 reference instead.
Accomplishments that we're proud of
- A number anyone can recompute: the scorecard, one command, no AWS account.
- The agent can't change the money. A person's tap, a Cedar policy and the tool's own check stand between the model and the claim.
- It says no, with a reason. A late arrival, a missing notice, an unsigned BOL or pins 4.6 km away each stop the claim, and the driver is told why.
What we learned
- Detention is a contract problem. The wait only pays if the clause's conditions are met, so the clause has to drive the workflow.
- A location pin is a claim, not proof. Agreement between pins, server time, a camera-only photo and typed paper times is.
- Let the model read and write, and keep the deciding in code.
What's next for DockClock
- Real outcomes. Replay a factor's real loads, so paid and denied claims replace my hand-written answers.
- Broker replies. Read inbound email and draft a response for dispatch to approve.
- More evidence. Read ELD (electronic logging device) data when a carrier has it.
Built With
- amazon-bedrock
- amazon-bedrock-agentcore
- amazon-cloudfront
- amazon-cloudwatch
- amazon-dynamodb
- amazon-eventbridge-scheduler
- amazon-ses
- amazon-sns
- amazon-sqs
- amazon-web-services
- aws-cdk
- aws-cloudformation
- aws-end-user-messaging-sms
- aws-end-user-messaging-social
- aws-kms
- aws-lambda
- aws-secrets-manager
- aws-step-functions
- cedar
- claude-sonnet-4.5
- rcs
- whatsapp-business-platform
- whatsapp-flows
Log in or sign up for Devpost to join the conversation.