Build Your Own Freight Bot · Episode 1 of 8

Foundations: what an AI agent actually is

Build Your Own Freight Bot, episode 1. What an AI agent really is, why a tool is just a function, and which freight jobs are worth automating.

5 scenes · about 3 minutes · every code sample in this series runs against OTT's public API.

Scene 1 · The hook1/5

A dispatcher's Tuesday

  1. 7:40Quote: Farmingdale → Newark12m
  2. 8:15Quote: Bronx → Hartford15m
  3. 9:05Check call: is it picked up?10m
  4. 9:50Quote: Brooklyn → Edison14m
  5. 10:30Check call: where is it now?12m
  6. 11:20Re-route a late pickup35m
  7. 1:10Chase a missing POD25m
  8. 2:30Check call: ETA for receiver11m
  9. 3:15Build and send an invoice20m
  10. 4:10Quote: Queens → Stamford13m

Minutes burned on repeat work

132min

  • Quote requests54m
  • Check calls33m
  • POD & invoice chasing45m
  • Actual decisions35m

Where the sample day went

  • Quote requests · 32%
  • Check calls · 20%
  • POD & invoice chasing · 27%
  • Actual decisions · 21%

Here is one freight day, start to finish. Watch where the minutes go.

Quote requests arrive all morning. Each one is small. None of them can wait.

Then the check calls. Is it picked up? Where is it now? Same question, different PRO.

And the paperwork chase: proof of delivery, invoices, follow-ups.

Most of this is not judgment. It is repetition with a phone attached.

Sample data — an illustrative day, not a measurement

Scene 2 · The idea2/5

What is an agent, really?

A script

Fixed steps. You wrote the order.

Read emailParse ZIPsCall rate API×Send reply

Email says “Newark to the Bronx, 2 pallets”. The script expected ZIP codes. It stops.

An agent

A loop. The model picks the next step.

Instructions+ the requestModeldecides×2Tool callget_quote(…)Resultdata backAnswer

Same request, handled. The model turned the words into ZIPs, called the tool, and read the result.

A script does the same steps in the same order, every time. If a step surprises it, it stops.

An agent is a loop. You give a model instructions and a set of tools.

The model decides: answer now, or call a tool. The tool runs. The result goes back to the model.

Then it decides again. The model picks the next step; your code only runs what it asks for.

Concept diagram — no figures shown

Scene 3 · The building block3/5

Tools are just functions

get-quote.ts
const BASE = process.env.OTT_API_BASE
  ?? "https://ontimetrucking.com/api/v1";

// 1. What the model reads: a name, a description, a schema.
export const getQuoteTool = {
  name: "get_quote",
  description: "Price an LTL shipment between two US ZIPs. " +
    "Returns customer-facing all-in prices.",
  input_schema: {
    type: "object",
    required: ["originZip", "destZip", "weightLbs"],
    properties: {
      originZip: { type: "string", pattern: "^[0-9]{5}$" },
      destZip: { type: "string", pattern: "^[0-9]{5}$" },
      weightLbs: { type: "number", minimum: 1, maximum: 30000 },
    },
  },
};

type Args = { originZip: string; destZip: string; weightLbs: number };

// 2. What runs when the model calls it: a plain function.
export async function getQuote(a: Args) {
  const res = await fetch(BASE + "/quotes", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.OTT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      originZip: a.originZip,
      destZip: a.destZip,
      commodities: [{
        freightClass: "100",
        weightLbs: a.weightLbs,
        pieces: 1,
        dimsIn: { length: 48, width: 40, height: 48 },
      }],
    }),
  });
  // Return errors as data so the model can read them.
  if (!res.ok) {
    return { error: res.status, detail: await res.text() };
  }
  const q = await res.json();
  return {
    quoteId: q.quoteId,
    options: q.options,
    expiresAt: q.expiresAt,
  };
}

// 3. Try it without a model: call the tool the way a model would.
const args = { originZip: "11735", destZip: "10001", weightLbs: 500 };
const result = await getQuote(args);
console.log(JSON.stringify(result, null, 2));

One tool call, end to end

Model asksget_quoteYour functionvalidates + sendsOTT v1 APIPOST /quotesOptions backsample: $100 · 2 days

Customer-facing price and transit days only. That is the whole tool.

A tool has two halves. A description the model reads, and a function your code runs.

This is real TypeScript. It calls OTT's public v1 quote endpoint. You can run it today with an API key from your OTT account.

When the model calls get_quote, your code sends the request, then hands the answer back.

The model never touches the network. It asks; your function acts.

Runs against OTT's public v1 API. Figures in the diagram are sample data.

Scene 4 · Where this goes4/5

The freight bot map

QuoteEp 3Lane inPrice outTrackEp 4Poll a PROException alertInbox & dispatchEp 5–6Email triageKill check callsBack officeEp 7POD chaseInvoice buildA/RFreight botmany tools

One tool is a demo. A bot is a set of them, each with its own job.

Quoting: a lane goes in, a price comes out. That is episode 3.

Tracking: poll a PRO and raise an alert when something slips.

Inbox and dispatch: read the mail, stop the check calls.

Back office: chase the POD, build the invoice, follow the money.

This series builds toward all of it, one piece at a time.

Series roadmap — episode titles are working titles

Scene 5 · The honest part5/5

What breaks

Invented identifier

The hallucinated PRO

  1. BOT

    User: where is my load?

  2. MODEL

    Sure — tracking PRO 123456789.

  3. API

    GET /shipments/123456789/tracking → 404 not_found

The fix you would build

Never let the model type an identifier. Pull PROs from your own records, and pass them to the tool from code.

Stale data

The stale rate

  1. BOT

    Quoted Monday. Customer says yes Friday.

  2. API

    POST /quotes/{id}/book → 409 conflict (quote expired)

  3. BOT

    Fix: re-quote, show the new price, then ask again.

The fix you would build

Store expiresAt with every quote. Check it before booking, and re-quote when it has passed.

Silent failure

The carrier that stops moving

  1. API

    Tracking event: Departed service center

  2. CLOCK

    … 26 hours, no new event …

  3. BOT

    Nothing to react to. No alert fires.

The fix you would build

Reactive bots miss silence. Add a scheduled watchdog that compares the last event time against a threshold you choose.

Demos work. Production is the part where the world pushes back. Here are three real failure shapes.

One: a model asked for a PRO it was never given can produce one that looks right.

Two: a quote is a snapshot. Quotes carry an expiry for a reason. Book on an old one and it is refused.

Three: a carrier stops moving and nobody is watching. A bot that only reacts will never notice silence.

Each one has a fix. Each fix is code you have to write and keep running.

Illustrative traces — sample values, not real shipments

Or skip the build.

Broker OS is OTT's all-in-one tool for brokers: find shippers, quote, book, track and bill from one screen. It is coming soon. Until then, you can pull a live rate on your own lane and the account is set up for you.

Broker OS is coming soon. Get a live rate in the meantime

Next up · Episode 2

Your first tool call

We wire a language model to a real rate API and let it price a lane. Bring your own key.

Read the transcript

A dispatcher's Tuesday

Here is one freight day, start to finish. Watch where the minutes go.

Quote requests arrive all morning. Each one is small. None of them can wait.

Then the check calls. Is it picked up? Where is it now? Same question, different PRO.

And the paperwork chase: proof of delivery, invoices, follow-ups.

Most of this is not judgment. It is repetition with a phone attached.

What is an agent, really?

A script does the same steps in the same order, every time. If a step surprises it, it stops.

An agent is a loop. You give a model instructions and a set of tools.

The model decides: answer now, or call a tool. The tool runs. The result goes back to the model.

Then it decides again. The model picks the next step; your code only runs what it asks for.

Tools are just functions

A tool has two halves. A description the model reads, and a function your code runs.

This is real TypeScript. It calls OTT's public v1 quote endpoint. You can run it today with an API key from your OTT account.

When the model calls get_quote, your code sends the request, then hands the answer back.

The model never touches the network. It asks; your function acts.

The freight bot map

One tool is a demo. A bot is a set of them, each with its own job.

Quoting: a lane goes in, a price comes out. That is episode 3.

Tracking: poll a PRO and raise an alert when something slips.

Inbox and dispatch: read the mail, stop the check calls.

Back office: chase the POD, build the invoice, follow the money.

This series builds toward all of it, one piece at a time.

What breaks

Demos work. Production is the part where the world pushes back. Here are three real failure shapes.

One: a model asked for a PRO it was never given can produce one that looks right.

Two: a quote is a snapshot. Quotes carry an expiry for a reason. Book on an old one and it is refused.

Three: a carrier stops moving and nobody is watching. A bot that only reacts will never notice silence.

Each one has a fix. Each fix is code you have to write and keep running.