Inbound Carrier Sales Automation

Deployment Document · Acme Logistics · Vijay Eswar · HappyRobot Platform · March 2026 · POC Live


What the Problem Looks Like Today

Acme Logistics runs a carrier sales desk. Carriers call in looking for available freight. A rep picks up, asks for the carrier's MC number, checks whether they are authorized to haul, searches for a load that matches the carrier's lane and equipment, pitches the rate, negotiates back and forth a few times, and either books the load or moves on. Most reps do this somewhere between 50 and 100 times per day.

The problem is not that the work is hard. It is that the work is the same conversation repeated hundreds of times. The reps who are best at building long-term carrier relationships, the ones who remember that a particular carrier likes running Dallas to Kansas City loads and has a daughter who plays soccer, spend most of their day doing the same transactional work as everyone else. The brokerage wants to grow load volume without proportionally growing headcount. The current model does not allow that.

The goal of this deployment is to move the transactional 90 percent of those calls to an AI agent. Human reps step in only where judgment, relationship building, or exception handling is actually needed.


How the System Works

The system has three parts. A voice agent on the HappyRobot platform that handles the live phone conversation. An API backend that handles the business logic, stores the data, and connects to external services. And a dashboard that gives the operations team visibility into what is happening across all calls.

End-to-End Call Flow
Carrier Calls in AI Agent Senthil GPT-5.2-instant TOOLS (5) 1. Verify MC via FMCSA 2. Search loads by lane + equipment 3. Start negotiation 4. Counter-offer 5. Carrier history lookup API Backend Fastify + SQLite FMCSA Federal DB Call ends. Post-call processing begins. AI Extract 7 fields from transcript AI Classify Outcome + Sentiment Webhook Records to API Dashboard Live metrics Rate Negotiation (up to 3 rounds) offer ≤ rate within ~15% above 15% 3 rounds hit Accept Split difference +5% bump Expire Transfer to rep New counter sent Hold firm Thank + close

When a carrier calls in, the agent greets them and asks for their MC number, which is a six-digit identifier assigned by the federal government to every carrier authorized to haul freight in the US. The system takes that number and sends it to the FMCSA database, which is the Federal Motor Carrier Safety Administration's public registry. The check happens in real time, and the result tells us whether the carrier's operating authority is active. If it is not, the agent politely ends the call. No human time spent on unauthorized carriers.

If the carrier is authorized, the agent asks what lane they are running and what equipment they have. A "lane" in freight is just origin to destination, like Dallas to Kansas City. Equipment is the trailer type: a dry van for general freight, a reefer for temperature-controlled goods, or a flatbed for oversized items. The system searches a database of available loads and pitches the closest match with the full details: origin, destination, miles, commodity, weight, rate, and pickup and delivery windows. All of that comes from the database. The agent does not make anything up. If there is nothing on the lane, it says so.

If the carrier is interested but wants a different rate, the agent negotiates. The pricing strategy is simple: if the carrier's ask is at or below the listed rate, it accepts immediately. If the ask is within about 15 percent above the listed rate, the agent splits the difference, which is the most common negotiation pattern in carrier sales. If the ask is too far above, the agent offers a small bump, about 5 percent, and holds firm. Up to three rounds of back and forth are allowed. If a rate is agreed, the agent says it will transfer the carrier to a human rep to finalize, which is exactly how this works in production. The transfer is simulated in the demo because actual phone transfers do not work through web calls.

After the call ends, the HappyRobot workflow runs three processing steps on the transcript. It extracts structured data: the carrier's MC number, company name, which load was discussed, and what rate was agreed. It classifies the outcome, whether the load was booked, whether the carrier rejected it, whether there was no capacity, and so on. And it classifies the carrier's sentiment: positive, neutral, negative, or frustrated. All of this is sent to the API via a webhook, where it is stored and made available to the dashboard.


Two Integration Paths

The backend supports two ways for HappyRobot to connect to it. The first is the standard approach: five custom tools, each with its own webhook that sends an HTTP request to a specific API endpoint. This gives full control over how each tool is configured, what message the agent says while waiting for a response, and how the response is used in the conversation.

The second approach uses MCP, which stands for Model Context Protocol. It is an open standard that lets an AI platform connect to a server and automatically discover what tools are available. Instead of configuring five separate webhooks, you point HappyRobot at a single URL and it finds all five tools on its own. HappyRobot requires this to use the Streamable HTTP transport, which means the server responds to POST requests with streaming JSON-RPC messages.

Both paths call the same business logic inside the backend. The carrier verification code, the load search, the negotiation engine, they are all identical regardless of whether the request came through a webhook or through MCP. The only difference is how the request arrives. The backend tracks which path each call used through a source field, so the dashboard can show the breakdown.

Webhook vs MCP: Same Backend, Two Entry Points
Webhook Path 5 custom tools configured Each has URL + headers + body POST /api/carriers/verify POST /api/negotiations/start GET /api/loads?origin=... Manual setup. Full control. MCP Path 1 credential configured POST /mcp Platform auto-discovers all 5 tools via JSON-RPC Single URL. Auto-discovery. API Backend Same service functions Same database Same response format

What the Dashboard Shows

The dashboard is a separate web application that reads from the same API the agent writes to. It was built from scratch using HappyRobot's own frontend interview template, which uses Next.js and a component library called shadcn. It was not built using the platform's built-in analytics, which was one of the requirements in the challenge.

At the top it shows the numbers that a carrier sales operations manager would check each morning: total calls, how many loads were booked, the conversion rate, and the average agreed rate. Below that it shows a donut chart breaking down call outcomes, a bar chart showing carrier sentiment across all calls, a timeline of call volume by hour, and a table of the most active lanes. There is also a section that shows how many calls came through the webhook integration versus MCP, displayed as progress bars with percentages.

Everything updates automatically. When a call finishes on HappyRobot, the workflow fires a webhook that records the data. The dashboard reads from the same API. No manual data entry exists anywhere in the chain.


API Endpoints

MethodPathWhat It Does
POST/api/carriers/verifyTakes an MC number, checks it against the FMCSA database, returns whether the carrier is authorized
GET/api/loadsSearches available loads by origin city, destination city, or equipment type
POST/api/negotiations/startBegins a rate negotiation on a specific load, returns the pitch message with all load details
POST/api/negotiations/counterTakes the carrier's counter-offer, decides whether to accept, counter, or reject, returns the response
POST/api/callsRecords a completed call with outcome, sentiment, agreed rate, and which integration path was used
GET/api/calls/carrier/:mcReturns all past calls for a specific carrier, so the agent can reference previous bookings
GET/api/dashboard/metricsReturns the full dashboard payload: KPIs, outcome distribution, sentiment, timeline, lanes, sources
POST/mcpMCP endpoint. Same 5 tools, auto-discovered via Streamable HTTP protocol

All /api endpoints require an x-api-key header. The /mcp endpoint uses the same key via HappyRobot's MCP credential configuration. The /health endpoint is public.


Project Structure

carrier-sales-platform/ ├── src/ │ ├── main.ts server, routes, MCP endpoint, auth hook │ ├── config/ │ │ ├── environment.ts validated env vars (Zod schema) │ │ └── database.ts SQLite setup, table creation, 10 seed loads │ ├── shared/ │ │ ├── result.ts typed success/failure returns │ │ └── errors.ts domain-specific error classes │ └── modules/ │ ├── carriers/ FMCSA check + 24h cache + fallback │ ├── loads/ search by origin/dest/equipment │ ├── negotiation/ 3-round pricing engine │ ├── calls/ call recording + source tracking │ ├── dashboard/ aggregated queries for all metrics │ └── mcp/ MCP server (Streamable HTTP) ├── dashboard/ Next.js 15, shadcn/ui, Recharts │ ├── src/app/page.tsx full dashboard UI │ ├── src/app/api/route.ts proxy to backend (avoids CORS) │ └── Dockerfile multi-stage: build + run ├── Dockerfile API backend: node:22-alpine ├── docker-compose.yml local dev: both services together └── .railwayignore keeps dashboard out of API deploy

Technical Decisions and Why

SQLite instead of Postgres

For a proof of concept, SQLite means zero infrastructure dependencies. There is no separate database to provision, no connection strings to manage, no credentials to rotate. The trade-off is that Railway, the hosting provider, uses containers with ephemeral storage. Every deployment creates a fresh container and the database resets. In production, this would need to be either Postgres or SQLite with a persistent volume attached.

Threshold array instead of conditional branches for negotiation

The pricing logic is not a chain of if-else statements. It is a sorted list of thresholds. Each entry in the list has a condition, like "carrier is asking for less than 15 percent above the rate," and a function that calculates the response. When a counter-offer comes in, the system walks the list and uses the first matching entry. Adding a new pricing tier means adding one entry to the list, not editing conditional logic. This follows a principle where adding new behavior should not require changing existing code.

Record map for load search filters

The load search does not have a switch statement that checks each possible filter. Instead, each filter, like origin or equipment type, is defined as an entry in a map. Each entry contains a SQL fragment and a function that transforms the input. Adding a new search parameter means adding one line to the map. The search function walks the map and builds the query automatically.

Separate dashboard service

The dashboard runs as its own deployment on Railway rather than being served by the API. This keeps the API lightweight and means the dashboard can be rebuilt and redeployed without touching the API. The dashboard's API route acts as a server-side proxy, which avoids cross-origin request issues.

Both webhook and MCP from the same codebase

The MCP server and the REST API endpoints call the same service functions. The carrier verification code, the load search, the negotiation engine, they are all shared. The MCP server just wraps them in the protocol that HappyRobot expects. This means there is one codebase to maintain, not two. And if we add a sixth tool, it is available through both paths immediately.


Gaps and What Would Come Next

Honest Gaps

The database resets on every deployment. For production, this needs persistent storage.

The negotiation logic is rule-based. It does not factor in live market rates from DAT or Truckstop, time of day, or how tight capacity is on a given lane. In a production deployment, these would make the agent significantly smarter about when to hold firm and when to concede.

The load data is test data seeded at startup. In production, this would connect directly to the customer's TMS so the agent always works from the live board.

The agent runs on web calls for the demo. Connecting to actual phone lines is a separate step and straightforward once the setup is agreed.

What would come next in a real deployment

Connect to the customer's TMS for live load data. Integrate a market rate provider for real-time pricing context. Add persistent storage so call history survives deployments. Build the carrier CRM view, where the system remembers that a specific carrier typically runs certain lanes and prefers certain equipment types, so the next time they call the agent can reference that history. Connect to actual phone lines.


What Is Live Right Now

ComponentLocationStatus
API Backendindustrious-blessing-production-d446.up.railway.appRunning
Dashboarddashboard-production-bb64.up.railway.appRunning
MCP Serverindustrious-blessing-production-d446.up.railway.app/mcpRunning
Codegithub.com/eswvijay/carrier-sales-platform-happyrobotPublic
Webhook WorkflowInbound Carrier SalesPublished
MCP WorkflowInbound Carrier Sales - MCPPublished