
We Open-Sourced an AI Agent That Vets Freight Carriers Against FMCSA
agent-for-logistics checks a carrier's authority, insurance, and safety rating in one call and flags the double-brokering pattern before you tender a load. Here's how it's built.
Key takeaways
- Double-brokering, a carrier accepting a load and handing it to an unvetted third party, is a fraud pattern the insurance does not respond to, and vetting a carrier by hand means checking authority, insurance, and safety across separate FMCSA lookups.
- agent-for-logistics is an open-source Eve.dev agent with seven tools that vets a carrier by MC or USDOT number and returns a do_not_tender, verify_before_tender, or no_flags_found verdict, with the exact FMCSA field or regulation behind every flag.
- Every capability is split into a pure, unit-tested function in agent/lib/ and a thin Zod-schema wrapper in agent/tools/, so risk rules like NO_POWER_UNITS and INSURANCE_UNKNOWN are tested without a model in the loop.
- The red-flag rules treat missing data as unresolved, not clean, with one deliberate exception: a missing FMCSA safety rating is scored info rather than warning, because most legitimate carriers are unrated and a rating-shaped warning would just teach brokers to ignore warnings.
We build a lot of AI agents for clients, and most of them never see daylight outside a private repo. This one we open-sourced: agent-for-logistics, a copilot for freight brokers and 3PL dispatchers built on the Eve.dev framework. It runs in your own Vercel project on your own API keys, and the point of it is one line from the README: "Vet a carrier in 15 seconds instead of 10 minutes across four tabs."
This post covers what the agent does, how it's built, and the one design decision worth stealing for your own tools.
The problem: vetting a carrier is a fraud check, not a lookup
A freight broker's job, reduced to its core, is matching a shipper's load to a carrier who will actually move it. Before tendering a load to a carrier they haven't worked with, a broker is supposed to check it out: active operating authority, the insurance it claims to carry, any FMCSA safety flags, whether it even has trucks. In practice that means opening the FMCSA SAFER lookup, cross-referencing the insurance filing, checking the safety rating, and eyeballing the fleet size: four separate lookups, done under time pressure, often skipped when a load needs to move now.
The fraud pattern this is supposed to catch is called double-brokering: a carrier accepts a load, then hands it off to a second, unvetted carrier without the broker's knowledge. If that freight gets lost, damaged, or stolen, the broker's cargo insurance typically doesn't respond, because the carrier who actually hauled it was never on the paperwork. The tell is often visible right in the FMCSA record (an entity accepting freight with zero power units and zero drivers is not the one driving the truck), but only if someone actually looks.
What the agent does
The hero workflow is a single message: paste an MC or USDOT number, get back authority status, insurance, safety rating, fleet size, and double-brokering red flags, each one tied to the specific FMCSA field or regulation it came from, so the broker can defend the call to their customer.
Under the hood that's the vet_carrier tool, backed by agent/lib/fmcsa.ts (the FMCSA QCMobile API client) and agent/lib/redflags.ts (the risk rules). I ran it against a synthetic carrier record (zero power units, no insurance figure supplied, no safety rating on file, the exact double-brokering shape the tool exists to catch) through the actual vetCarrier() function in the repo:
{
"status": "ok",
"carrier": {
"dotNumber": 3312890,
"legalName": "SILVER RIVER FREIGHT LLC",
"dbaName": null,
"location": "Laredo, TX",
"powerUnits": 0,
"drivers": 1,
"safetyRating": null
},
"assessment": {
"verdict": "do_not_tender",
"flags": [
{
"code": "INSURANCE_UNKNOWN",
"severity": "warning",
"message": "Insurance on file could not be confirmed. Verify the certificate directly with the insurer before tendering.",
"basis": "No insurance figure was supplied; absence of data is not evidence of compliance."
},
{
"code": "NO_POWER_UNITS",
"severity": "critical",
"message": "Carrier reports zero power units. A carrier with no trucks accepting a load is the classic double-brokering pattern.",
"basis": "FMCSA totalPowerUnits is 0."
},
{
"code": "SAFETY_RATING_UNKNOWN",
"severity": "info",
"message": "No FMCSA safety rating on file. This is normal — FMCSA rates a carrier only after a compliance review, and most carriers are unrated. It is not itself a problem.",
"basis": "FMCSA safetyRating is absent."
}
]
},
"insuranceEvaluatedDollars": null,
"dataAsOf": "2026-07-21T14:00:00.000Z"
}
Three flags, three different severities, and the verdict (do_not_tender) is driven entirely by the one critical flag: NO_POWER_UNITS. The agent's system prompt tells it to lead with the verdict, then explain the flags with their basis, never do arithmetic itself, and never describe a degraded lookup as clean. That last rule matters: if FMCSA can't be reached or the web key is missing, vet_carrier returns status: "degraded" with guidance instead of a verdict, and the instructions forbid the model from softening that into "looks fine."
Carrier vetting isn't the only thing the agent does. Six other tools cover the rest of a broker's day-to-day math and paperwork:
| Ask it | It uses |
|---|---|
| "Should I tender to MC-123456?" | vet_carrier |
| "What's the all-in RPM on $2,400 for 1,050 miles?" | rate_per_mile |
| "Driver sat 6 hours. What's the detention?" | accessorial_calculator |
| "Can I make a 9am delivery 780 miles out?" | transit_estimator |
| "Convert this 2% quick-pay discount to an annual rate." | calculate |
| "Check this rate confirmation before I sign." | ingest_document + a skill |
| "Write the detention dispute letter." | export_document + a skill |
I ran rate_per_mile's underlying logic on a $2,400 linehaul over 1,050 miles with a $180 fuel surcharge:
{
"linehaulRpm": 2.29,
"allInRpm": 2.46,
"totalDollars": 2580,
"totalFormatted": "$2,580.00"
}
transit_estimator runs a day-by-day simulation of FMCSA's hours-of-service rules (49 CFR 395) rather than a closed-form division. The 14-hour on-duty window can bind before the 11-hour driving limit does once you add dock dwell time, and a naive miles / speed / 11 calculation will happily report an appointment as achievable when it legally isn't. On top of the seven tools, four skill files in agent/skills/ carry longer-form playbooks: double-brokering red flags, Carmack Amendment cargo claims, rate confirmation review, and detention dispute letters.
How it's built: lib for logic, tools for wiring
The codebase is organized around one rule, stated in docs/ARCHITECTURE.md: every capability the agent can call is split into a pure function in agent/lib/ and a thin wrapper in agent/tools/ that exposes it to the model via defineTool and a Zod schema.
The reason is testability without a model in the loop. Every file under tests/ imports from agent/lib/, never agent/tools/. npm test runs over a hundred test cases against estimateTransit, assessCarrier, evaluate, ratePerMile, roundAwayFromZero, and the rest, with plain inputs and plain assertions, and it passes with zero environment variables and zero network calls. A defineTool wrapper stays deliberately too thin to need its own tests: parse input with Zod, call the lib function, return the result. Here's the whole of agent/tools/rate_per_mile.ts:
import { defineTool } from "eve/tools";
import { z } from "zod";
import { ratePerMile, brokerMargin } from "../lib/pricing.ts";
export default defineTool({
description:
"Compute rate per mile and, when both sides of the rate are known, broker margin. Always use this instead of calculating rates yourself.",
inputSchema: z.object({
linehaulDollars: z.number().describe("Linehaul rate in dollars"),
miles: z.number().describe("Loaded miles"),
fuelSurchargeDollars: z.number().optional(),
accessorialsDollars: z.number().optional(),
customerRateDollars: z
.number()
.optional()
.describe("All-in rate billed to the customer, if computing broker margin"),
carrierRateDollars: z
.number()
.optional()
.describe("All-in rate paid to the carrier, if computing broker margin"),
}),
async execute(input) {
const rate = ratePerMile(input);
const margin =
input.customerRateDollars !== undefined &&
input.carrierRateDollars !== undefined
? brokerMargin({
customerRateDollars: input.customerRateDollars,
carrierRateDollars: input.carrierRateDollars,
})
: null;
return { rate, margin };
},
});
No business logic lives in that file. Every number the agent states has to come from a tool like this one. agent/instructions.md explicitly forbids the model from doing arithmetic in its own head, and everything that touches a dollar figure routes through agent/lib/money.ts, which does the math in integer cents rather than floating-point dollars and rounds ties away from zero rather than using Math.round's toward-positive-infinity bias, so a negative credit and its positive twin round to mirror-image cent values.
vet_carrier is the one tool that breaks the "schema-plus-one-call" pattern, because it has a decision to make before it can call the lib layer: is FMCSA_WEB_KEY even configured. So agent/tools/vet_carrier.ts exports a vetCarrier() function that takes an injected lookup client, specifically so tests/vet_carrier.test.ts can test that branch without a real network call: the same pattern I used above to generate that JSON output myself, by injecting a fake FMCSA response into the real function.
The risk rules themselves live in agent/lib/redflags.ts, and they're a flat list of checks, each producing a typed flag:
export type Severity = "critical" | "warning" | "info";
export type RedFlag = {
code: string;
severity: Severity;
message: string;
/** Why this rule exists — shown to the user so the verdict is defensible. */
basis: string;
};
A sample of the actual rules, with their real codes and severities:
NOT_AUTHORIZED(critical): FMCSA'sallowedToOperateflag isn'tY.OUT_OF_SERVICE(critical): an out-of-service date is present on the record.NO_ACTIVE_AUTHORITY(critical): neither common nor contract authority is active.INSUFFICIENT_INSURANCE(critical): a supplied coverage figure is below the $750,000 statutory minimum in 49 CFR 387.9.NO_POWER_UNITS(critical):totalPowerUnitsis exactly 0.UNSATISFACTORY_RATING(critical): FMCSA'ssafetyRatingisU.INSURANCE_UNKNOWN(warning): no coverage figure was supplied at all.POWER_UNITS_UNKNOWN(warning):totalPowerUnitsis absent from the record.CONDITIONAL_RATING(warning):safetyRatingisC.HOLDS_BROKER_AUTHORITY(warning): the carrier also holds active broker authority, meaning it's legally allowed to re-broker your freight.SAFETY_RATING_UNKNOWN(info): no rating on file at all.
Three outcomes, driven only by the blocking findings: do_not_tender if any flag is critical, verify_before_tender if the worst flag is a warning, no_flags_found if nothing critical or warning fired. info flags ride along in the array without moving the verdict. That's the design decision worth digging into.
The design decision: absent data is never a pass, except once
The interesting trade-off in this codebase is how it treats the gap between "we checked and it's fine" and "we don't know." Most of the rules resolve that gap in one direction, deliberately: absence of data is never treated as evidence of compliance.
Look at how INSURANCE_UNKNOWN is written in agent/lib/redflags.ts:
const insurance = opts.insuranceOnFileDollars;
if (insurance === null || insurance === undefined) {
flags.push({
code: "INSURANCE_UNKNOWN",
severity: "warning",
message:
"Insurance on file could not be confirmed. Verify the certificate directly with the insurer before tendering.",
basis:
"No insurance figure was supplied; absence of data is not evidence of compliance.",
});
} else if (insurance < STATUTORY_MINIMUM_INSURANCE) {
flags.push({
code: "INSUFFICIENT_INSURANCE",
severity: "critical",
message: `Insurance on file ($${insurance.toLocaleString("en-US")}) is below the $750,000 statutory minimum for general freight.`,
basis: "49 CFR 387.9 minimum public liability coverage.",
});
}
Not having an insurance figure and having a bad one are two different flags with two different severities, but neither is silence. I confirmed the branching by running assessCarrier against the same carrier with different insuranceOnFileDollars values: at 500000 the flags include INSUFFICIENT_INSURANCE (critical); at 1000000 neither insurance flag fires; supply nothing and you get INSURANCE_UNKNOWN (warning). There's no fourth path where missing data quietly resolves to "assume compliant." The same logic covers fleet size: POWER_UNITS_UNKNOWN fires when totalPowerUnits is null, distinct from NO_POWER_UNITS, which fires when the count is confirmed zero. Absent and zero are both bad, but not the same bad, and the code doesn't collapse them into one message.
Then there's the one place the codebase does the opposite, and the comment explaining why is the most interesting six lines in the repo:
// A missing safety rating is NOT treated as a warning. FMCSA only assigns a
// rating after a compliance review, so the majority of entirely legitimate
// carriers are unrated. Warning here would fire on most honest lookups and
// train users to ignore warnings, which costs more safety than it buys.
// It is surfaced at `info`, which does not move the verdict.
if (carrier.safetyRating === null) {
flags.push({
code: "SAFETY_RATING_UNKNOWN",
severity: "info",
...
FMCSA doesn't rate every carrier: it assigns Satisfactory, Conditional, or Unsatisfactory only after a compliance review, and most small and mid-size carriers have simply never been reviewed. If a missing rating carried the same warning severity as missing insurance, verify_before_tender would fire on the majority of ordinary lookups, and a broker staring at a warning on nearly every carrier stops reading warnings. That's the exact failure mode the red-flag design exists to prevent. So this one absence gets info severity: it still shows up in flags, and the agent still has to mention it (the instructions forbid saying "no flags found" in the same breath as listing one), but it doesn't block the tender on its own.
That's a genuine trade-off, not a shortcut. Getting it wrong in either direction has a real cost: too strict and brokers learn to click through the tool; too lenient and an unrated carrier with a real safety problem slides through unremarked. The repo picked "surface it, don't block on it" and wrote down exactly why.
Deploying it
There's no hosted version of this and no account to create. You deploy it into your own Vercel project. The README leads with a Deploy-to-Vercel button that prompts for four environment variables: an AI_GATEWAY_API_KEY from Vercel's AI Gateway (the model is pinned in agent/agent.ts to anthropic/claude-sonnet-5, so the gateway key just needs routing permission), a free FMCSA_WEB_KEY from FMCSA's own developer portal, and an AGENT_BASIC_AUTH_USER / AGENT_BASIC_AUTH_PASSWORD pair you pick yourself.
That last pair isn't optional in practice, even though the build succeeds without it. agent/channels/eve.ts gates every channel route behind HTTP basic auth, and leaving those two blank still deploys: it just rejects every browser request in production. That's a deliberate fail-closed default: eve's placeholder auth refuses to serve unauthenticated traffic rather than quietly running open. A fifth variable, AGENT_PUBLIC, removes auth entirely for a public demo, but it's opt-in and left out of the button's prompt list on purpose, so nobody flips it by accident mid-setup.
Running it locally is the same repo, no basic-auth keys required:
git clone https://github.com/FoundrySoftHQ/agent-for-logistics
cd agent-for-logistics
cp .env.example .env # fill in AI_GATEWAY_API_KEY and FMCSA_WEB_KEY
npm install
npm run dev
Node 24 or later is required: the codebase ships plain .ts files with no compile step, and npm test runs node --test "tests/**/*.test.ts" directly against them using Node's native TypeScript support. I used that same mechanism (node --experimental-strip-types) to generate the JSON in this post. On an older Node, eve start fails to start rather than degrading.
What it will get wrong
The README has a section called "What it will get wrong," worth repeating rather than glossing over. It's the same discipline that produced the SAFETY_RATING_UNKNOWN trade-off above:
- No market rate data. No load boards, no rate indexes, no lane history. Ask it what a lane pays and it says it can't see that.
- Insurance is only checked if you supply the figure.
vet_carrierreads authority from FMCSA directly, but it never fetches an insurance dollar amount on its own: you have to hand it the number from the certificate, or it flags the coverage as unverified rather than assuming you're covered. - A clean FMCSA record is the floor, not the ceiling. FMCSA data reflects what a carrier filed. Fraudsters buy dormant authorities with clean histories on purpose. Call the phone number on the FMCSA record, not the one in the email.
- The FMCSA response schema is unverified against a live call. This build ships with no API key of its own, so
agent/lib/fmcsa.ts's field mapping has only ever been exercised against a hand-written fixture, not a real QCMobile response. It's written defensively (every field is optional, the raw response is preserved oncarrier.raw), but if FMCSA's real field names differ from what's coded, values could come backnullsilently instead of erroring. If you run this with your own web key, the highest-value contribution back to the repo is spot-checking a few known carriers against that fixture and filing what's wrong.
That last one is a real gap, disclosed rather than hidden: the whole point of open-sourcing something like this instead of shipping it as a black-box SaaS product.
Try it
The repo is at github.com/FoundrySoftHQ/agent-for-logistics, MIT licensed. Clone it, run npm test (passes with zero environment variables), and read docs/ARCHITECTURE.md if you want to add a tool or skill of your own: eve auto-discovers anything you drop into agent/tools/ or agent/skills/, no separate registration step. If you find a live FMCSA response that doesn't match the fixture, that's the single most useful pull request you could send.
Related reading
Trusting AI generated code was never the right goal, and the 4 percent of developers who say they fully trust it prove nothing is broken: the fix is an AI code review process that makes verification cheap instead of asking how much to trust the output.
We built and shipped five open-source vertical AI agents. Every single one had the same class of defect: absent or unreadable input rendered as a confident, clean answer. Here is what that bug looks like, why tests miss it, and what actually catches it.
A practical look at the best open weight LLMs for agents in 2026, organized by which constraint, cost, latency, or data residency, should actually decide the pick.
Let's build something great.
Have a project in mind? We are an elite software and AI development studio ready to bring your ideas to production. Let's talk about your roadmap.