← Blog

Chat agents

Chat agents for port logistics: a Rotterdam case study

It is 06:47 in Rotterdam, the Maersk ETA has slipped four hours overnight, and a dispatcher is on her third coffee. We replaced that morning with one chat agent.

Jacob Molkenboer· Founder · A Brand New Company· 7 Jun 2026· 8 min
Brass ship's telegraph, folded paper manifest, green signal flag, iron tag and wax seal on ivory desk blotter.

It is 06:47 in Rotterdam. Linda, a dispatcher at a port-logistics broker we will call Havenlijn (not their real name; the deal lets us share the build, not the brand), is on her third browser tab and second coffee. The Maersk ETA for the MSC Aniello has slipped four hours overnight. Her cross-dock partner in Maasvlakte II needs to know by 07:30 whether the 14:00 window still holds. Her spreadsheet has 38 rows. Two are red. She has done this every Tuesday for nine years.

When Havenlijn called us in February, they did not ask for an AI project. They asked for "a thing that stops people checking Maersk by hand at 6am." Four full-time dispatchers, each owning a slice of the carrier book, each running their own version of the same Excel file. Cross-dock slots at the partner facility booked over the phone, confirmed by email, re-confirmed in WhatsApp. Three months later, one Telegram chat agent handles the same throughput. The four dispatchers still work there. Their job changed.

The job before the agent

A port-logistics broker like Havenlijn does not move boxes. They sit between the deepsea carriers (Maersk, MSC, CMA CGM, Hapag-Lloyd) and the inland operator who picks the container up at the cross-dock and trucks it to a warehouse in Tilburg or Venlo. The whole business is the gap between "the vessel berths at ECT Delta" and "the truck arrives at the right dock at the right hour." Get the timing wrong and either the cross-dock charges demurrage or the truck driver sits idle. Both lose the broker money.

The four dispatchers each watched one or two carriers. Every 30 to 45 minutes during their shift they would open the carrier's tracking portal, paste in the container number, read the ETA, copy it into the master spreadsheet, recompute the slot booking, then phone or message the cross-dock to adjust if anything had moved. On a busy Tuesday they handled around 80 containers between them. The spreadsheet had 14 tabs.

This is not a problem of intelligence. It is a problem of patience. Spreadsheets do not raise alarms when a number slips by four hours overnight; humans have to keep looking. By month three, the dispatchers were openly asking the founder whether they could automate "at least the staring part."

Why Telegram beat the custom dashboard

Our first sketch was a web dashboard. It always is. The dispatchers killed it in the first workshop. "We are not at a desk. We are on the dock, in the cab, walking to the print room. We need this on our phone, in the chat we already use." Havenlijn's operations group lived in a single Telegram group called Loods (Dutch for warehouse). Booking confirmations, photos of damaged seals, voice notes from drivers stuck at customs. Twelve people, around 200 messages a day.

We argued for a moment. A dashboard gives you filters, search, history. A chat agent gives you presence. The dispatchers were right. Information that does not show up where they already look does not get read. We built into the Telegram Bot API and kept a thin web view as backup for the founder, who likes spreadsheets.

Three carrier feeds, one truth

Maersk, MSC, and CMA CGM all expose container tracking APIs. The interfaces could not be more different. Maersk's developer portal publishes a clean OAuth2 flow and a documented Track & Trace endpoint that returns predictable JSON. MSC's myMSC API is narrower and rate-limited harder than the docs let on. CMA CGM's eBusiness portal needs per-customer credentials and still returns SOAP-flavoured XML in places.

The first job of the build was a normaliser. Everything downstream of it (the agent, the slot booker, the cross-dock notifier) reads the same shape:

// carriers/normalize.ts
type CanonicalEta = {
  containerNo: string
  carrier: 'MAERSK' | 'MSC' | 'CMA'
  vessel: string
  port: string          // UN/LOCODE, e.g. NLRTM
  etaUtc: string        // ISO 8601, always UTC
  confidence: 'high' | 'medium' | 'low'
  fetchedAt: string
}

export function fromMaersk(raw: any): CanonicalEta {
  const arrival = raw.events.find((e: any) => e.eventCode === 'ARRI')
  return {
    containerNo: raw.equipmentReference,
    carrier: 'MAERSK',
    vessel: arrival.transport.vesselName,
    port: arrival.location.UNLocationCode,
    etaUtc: arrival.eventDateTime,           // already UTC
    confidence: arrival.classifierCode === 'EST' ? 'high' : 'medium',
    fetchedAt: new Date().toISOString(),
  }
}

The normaliser hides the ugliness. When MSC silently changed their date format in week three (from 2026-03-14T08:00:00Z to 14/03/2026 08:00 UTC), only one file needed editing. The rest of the system kept running.

Three carriers, three quirks worth knowing. Maersk returns ETAs in UTC but timestamps its own internal events in Hamburg local time, so the same vessel arrival can appear twice in the feed with different zones if you are not careful. MSC will return a 200 OK with an empty body when its backend is overloaded; we treat empty as failure, not success. CMA CGM stops emitting new events for a container the moment it is gated out at the terminal, so the dispatchers had to learn that "no update for two hours" sometimes means "everything went fine and you can stop watching."

We poll Maersk every 15 minutes, MSC every 30, CMA every 20. The cadence is set by the carrier's rate limits, not our preference. Maersk publishes its limits openly; MSC and CMA both quietly throttle and stop returning data if you push past a threshold they will not name.

From ETA to booked slot in 110 seconds

When an ETA shifts by more than 90 minutes, the agent moves. The flow is short:

  1. Detect the shift in the normalised feed.
  2. Look up the affected container in the broker's booking system, a 2019 PHP backoffice we did not touch.
  3. Compute the new cross-dock window using the house rule: berth time plus four-hour unload buffer, rounded up to the next 30 minutes.
  4. Post into the Loods group: "Container TCNU 4632181 (MAERSK) ETA moved 14:00 to 18:30. Proposed new cross-dock slot: 22:30 Maasvlakte II, Lane 4. Reply OK to confirm, ALT to suggest another."
  5. On OK from any allow-listed dispatcher, submit the new booking via the cross-dock's API.
  6. Post the confirmation receipt as a threaded reply.

Median time from ETA shift to confirmed new slot, measured across the first eight weeks: 110 seconds. The previous median, taken from the dispatchers' own spreadsheet timestamps, was 47 minutes. The agent is not faster because it thinks faster. It is faster because it is already looking.

The 90-minute threshold was not our number. The dispatchers gave it to us. Anything under 90 minutes they wanted to absorb silently inside the existing four-hour buffer; anything over and they wanted to be told. The agent honours their threshold and posts a quiet 18:00 summary listing every sub-90 shift it absorbed, so nobody loses track of what was happening underneath.

Takeaway

The chat-agent layer is the easy half. The real work in this build sits in the carrier-API normaliser, where three vendor quirks collapse into one canonical shape.

The week-two scare

In week two we had our first incident. A dispatcher's phone was compromised through an unrelated phishing message, and the attacker tried to issue cross-dock booking commands inside the Loods group. The agent rejected the first attempt because the command came from a Telegram user ID it did not have on its allow-list. The dispatcher noticed within minutes, we revoked the session, no booking went through, no money moved.

This is the small lesson behind the story circulating on Hacker News this week, where Meta confirmed thousands of Instagram accounts were compromised by abusing its own support chatbot. When the agent is the interface, the agent's trust model is the security model. The model in the loop does not protect you; the rules around it do. We had three from day one and they earned their keep that week:

  • Only allow-listed Telegram user IDs (not usernames; usernames can be changed by their owner) can issue commands.
  • Every command, the user ID who sent it, the resolved canonical action, and the carrier response are written into an append-only Postgres table the founder can audit.
  • The agent never executes a booking it cannot reverse with a single command. If a flow needs three steps to undo, it needs human eyes to do.

What the four dispatchers do now

None of them lost their job. This is the part founders get wrong when they read case studies like this one. The four dispatchers now do the work the spreadsheet was crowding out: relationship calls with carrier reps, exception handling when the agent flags something it cannot resolve, planning the next quarter's volume with the founder. The agent took the staring; the people kept the judgement.

Numbers from the first 90 days, taken from the access logs and the broker's monthly report:

  • Containers handled per week: around 410, up from around 310.
  • Demurrage charges per month: down 38%.
  • Cross-dock slot reschedules per week: same total, executed in minutes instead of hours.
  • Dispatcher overtime hours per month: down from 64 to 11.

The Hacker News chatter this week about designing with Claude more than with Figma rings true here in a smaller way: most of the back-and-forth on this build happened in plain English inside the Telegram group, with the agent itself showing us what worked. The dashboard mockups we did at the start went unused. The chat transcript was the design document.

Where the build sits today

Three months in, the agent runs on a forty-euro-a-month VPS, talks to three carrier APIs, books slots at one cross-dock, and posts into one Telegram group. Hapag-Lloyd and ONE are scoped for the second phase, once the first three carriers have run clean for ninety consecutive days. The founder is now asking us about voice: can a dispatcher in the cab phone the agent at 70 km/h and get the same answer? Probably. Different post.

When we built this for Havenlijn, the thing we underestimated was how much of the work was the normaliser, not the model. Three vendor APIs, three quirks, one canonical shape: that is where the build was won or lost. If you are looking at a similar build, the smallest useful thing you can do today is open one of your carrier's developer portals, sketch the JSON shape it returns, and sketch next to it the four fields your operation actually cares about. That two-column drawing is your spec. The kind of AI agents work we do tends to live or die on it.

Key takeaway

When the chat is the interface, the agent's trust model is the security model. The model in the loop does not protect you; the rules around it do.

FAQ

Does the agent ever book a slot without human approval?

No. Every booking and reschedule needs an OK reply in the Loods Telegram group from an allow-listed dispatcher. The agent proposes, a human confirms, then the booking goes through.

Which carriers does the agent cover today?

Maersk, MSC and CMA CGM. Hapag-Lloyd and ONE are scoped for a second phase, once the first three have run clean for ninety consecutive days in production.

What happens when a carrier API goes down?

The agent falls back to the last cached ETA, posts a clearly labelled stale-feed warning in the group, and the dispatcher decides whether to call the carrier directly. No silent failures.

How long did the build take end to end?

Nine weeks from kickoff to first production run, including two weeks negotiating API access with one carrier and one week rewriting the normaliser after a vendor changed its date format mid-build.

chat agentsai agentsautomationintegrationscase studyworkflow

Building something?

Start a project