AI agentsMCPREST APIDealers

How do you build AI agents that price and buy cars for your dealership?

A developer's blueprint for a dealer-owned agent stack on MarketCheck APIs and MCP, with four working agents and the approval step each one needs.

MarketCheck9 min read

For developers and technical leads at dealer groups

Your dealership already owns half the data an agent needs: unit costs, days in stock and asking prices in the DMS, plus trade-in leads in the CRM. What it lacks is the market around the store: what the same car is listed for across town, what similar cars were last listed for before they sold, how many are still on the ground and how fast they turn.

MarketCheck supplies that market layer as a REST API and as MCP servers an agent can call directly. A developer, IT lead or vendor working for a dealer group can wire both into agents that run on the store's own schedule, with a person approving every change they propose.

In short. The agent reasons, MarketCheck supplies market data, your DMS and CRM supply your own data, and a person approves anything that changes a price, an offer or a bid. Build on the Free tier, and give your coding assistant the free Docs MCP so it writes calls against the documented parameter list.

The stack in one picture

TriggersWhat starts a runDaily scheduleNew CRM leadAuction run listAgentA model plus instructionsAllowed tools onlyCall budget per runCache and retryRules live in codeClaude Agent SDK, OpenAI Agents SDK,or your own function-calling loopTools the agent callsMarketCheck market dataREST API or Data MCP, same keyInventory searchPrice predictionVIN historyNeoVIN decodeRecent and soldMarket days supplyYour systemsRead-only to startDMS or inventory feedCRM leadsCosts and pricing rulesOutputsDrafts, never direct writesA person approvesPrice and offer draftsRanked buy listsSlack and email alerts
MarketCheck Your systems A person
The dealer-owned agent stack. Triggers start a run, one agent calls two kinds of tools, and every write waits for a person.

Six parts, and only one of them is AI:

  • Triggers. A scheduler (cron, a cloud scheduler, whatever you already run) or an event such as a new trade-in lead or a new auction run list. A person can also start a run from Slack.
  • The agent. A model plus instructions, running in any framework that speaks MCP or function calling. The Claude Agent SDK and the OpenAI Agents SDK both attach MCP servers, and a plain loop over function calls works too.
  • Market data tools. MarketCheck, through the REST API or the Data MCP server.
  • Your tools. The DMS or inventory feed and the CRM, plus the costs and pricing rules only you know. Expose them read-only first.
  • Guardrails. Which tools the agent may call, a call budget per run, caching, and retry rules.
  • Outputs. Drafts for a person to approve, plus alerts and reports.

Pick a connection: REST, Data MCP or Docs MCP

MarketCheck gives you four ways in. Use the one that matches who is making the calls.

  • REST API at https://api.marketcheck.com/v2. Best for scheduled jobs where your code decides what to call. Every request carries your key as the api_key query parameter, and OAuth 2.0 client credentials are also supported.
  • Data MCP, hosted at https://api.marketcheck.com/mcp?api_key=YOUR_API_KEY. Best when the agent chooses its own calls. The key sits in the URL, so keep it on your server.
  • Data MCP connector at https://developers.marketcheck.com/api/mcp. Best for staff working in Claude or ChatGPT. They sign in with OAuth and grant scoped tool access, with no key to paste.
  • Docs MCP at https://developers.marketcheck.com/api/docs-mcp. For your coding assistant while you build. It is free and keyless, and it reads public documentation only.

MCP tool calls run on the same backend as REST and count against the same plan. The Data MCP exposes search_active_cars, search_past_90_days, predict_price_with_comparables, get_car_history, decode_vin_neovin and get_server_info, plus get_sold_summary, an experimental tool for Enterprise plans.

Start with the Docs MCP. In Claude Code it is one command:

bash
claude mcp add --transport http marketcheck-docs https://developers.marketcheck.com/api/docs-mcp

Cursor, VS Code and Windsurf take the same URL, and the snippets are on the MCP page. From then on, ask your assistant for the exact parameters of /v2/search/car/active and it looks them up instead of guessing.

Every blueprint below shares one small REST helper. It adds the key and counts calls per endpoint, which you will want for cost per run. It also handles the two kinds of 429: a per-second rate limit that clears in a moment, and a monthly quota that stays spent until the next month.

marketcheck.tsts
const BASE = 'https://api.marketcheck.com/v2'
export const callsThisRun = new Map<string, number>()

export async function mc(path: string, params: Record<string, string | number | boolean> = {}) {
  const url = new URL(BASE + path)
  url.searchParams.set('api_key', process.env.MARKETCHECK_API_KEY ?? '')
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value))

  // Count by endpoint, not by VIN, so the totals line up with the rate card
  const endpoint = path.replace(/[A-HJ-NPR-Z0-9]{17}/, '{vin}')

  for (let attempt = 0; attempt < 4; attempt++) {
    const res = await fetch(url)
    if (res.ok) {
      callsThisRun.set(endpoint, (callsThisRun.get(endpoint) ?? 0) + 1)
      return res.json()
    }
    if (res.status !== 429) throw new Error(`${endpoint} returned ${res.status}`)
    // Monthly quota is spent: waiting will not help, so stop the run
    if (res.headers.get('Quota-Remaining') === '0') throw new Error('Monthly quota exhausted')
    const waitSeconds = Number(res.headers.get('Retry-After') ?? 2 ** attempt)
    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000))
  }
  throw new Error(`${endpoint} is still rate limited`)
}

Blueprint 1: the morning repricing agent

Goal. Each morning, a short list of used units whose asking price sits well away from the market, each with the evidence and a suggested move that waits for a manager's approval.

  1. Daily, after 11:00 UTC
  2. Units from the DMS
  3. Price + comparables
  4. Your pricing rules
  5. Manager approves
  6. DMS price update

The Inventory Search docs say the daily data update is published by 11:00 AM UTC, so schedule the run after that. Then:

  1. Pull used units from your DMS or inventory feed: VIN, miles, asking price and cost.
  2. For each unit, call the Premium tier of MarketCheck Price, /v2/predict/car/us/marketcheck_price/comparables, with vin, miles, dealer_type and zip. It returns a predicted market price plus the comparable listings behind it. The Base tier, /v2/predict/car/us/marketcheck_price, returns only the price and MSRP when you do not need the evidence.
  3. Code decides which units make the list: your gap threshold, a minimum number of comparables, a cap on any single move and a floor at cost plus minimum gross.
  4. Hand the shortlist to the agent to write a two-line reason per unit, then post it to Slack for approval.
reprice.tsts
import { mc } from './marketcheck'

interface Unit { vin: string, miles: number, askingPrice: number, cost: number }
// Set by your used-car manager, never by the model
interface PricingRules { storeZip: string, minGap: number, minComps: number, maxMove: number, minGross: number }

export async function priceCheck(unit: Unit, rules: PricingRules) {
  const p = await mc('/predict/car/us/marketcheck_price/comparables', {
    vin: unit.vin,
    miles: unit.miles,
    dealer_type: 'franchise', // or 'independent'
    zip: rules.storeZip
  })
  const market: number = p.marketcheck_price
  const gap = (unit.askingPrice - market) / market
  if (Math.abs(gap) < rules.minGap || p.comparables.num_found < rules.minComps) return null

  const capped = Math.min(unit.askingPrice + rules.maxMove, Math.max(unit.askingPrice - rules.maxMove, market))
  return {
    vin: unit.vin,
    asking: unit.askingPrice,
    market,
    compMedian: p.comparables.stats.price.median,
    comps: p.comparables.num_found,
    // A draft only: a manager approves it before anything reaches the DMS
    suggested: Math.round(Math.max(unit.cost + rules.minGross, capped))
  }
}

Here is what comes back, trimmed from the documented sample (VIN 2T3DWRFV3LW077677, 76,189 miles, ZIP 80215):

Response (trimmed)json
{
  "marketcheck_price": 29014,
  "msrp": 38000,
  "comparables": {
    "num_found": 114,
    "listings": [
      { "vin": "2T3DWRFV8LW066769", "year": 2020, "make": "Toyota", "model": "RAV4", "trim": "Limited",
        "price": 29101, "miles": 79372, "dos_active": 16, "dist": 3.78 }
    ],
    "stats": {
      "price": { "min": 22672, "max": 39995, "mean": 30131.43, "median": 29980 },
      "dos_active": { "median": 35 }
    }
  },
  "recent_comparables": { "num_found": 19 }
}

What a person approves. Every price change. The agent drafts it, and your existing DMS integration writes it once a manager approves.

Blueprint 2: the trade-in appraisal agent

Goal. When a trade-in lead lands in the CRM, the appraiser opens a worksheet that is already filled in: what the car is, where it has been listed before, what the market says, and what similar cars were last listed for before they sold nearby.

  1. New lead in the CRM
  2. NeoVIN decode
  3. Listing history
  4. Price + comparables
  5. Recent sold nearby
  6. Appraiser sets the number

The calls, in order:

  1. /v2/decode/car/neovin/{vin}/specs for trim, installed options, colors and MSRP, so the worksheet starts from the right build.
  2. /v2/history/car/{vin} for earlier listings of this VIN: where and when it was listed, and at what asking price.
  3. /v2/predict/car/us/marketcheck_price/comparables for the market price at your ZIP.
  4. /v2/search/car/recents with vins, match=year,make,model,trim, zip, radius, sold=true, last_seen_days=30-*, stats=price and rows=0. That returns the spread of last listed prices on similar cars MarketCheck infers as sold in the past 30 days. The docs cap this search at a 100-mile radius and one stats field per request.

This is a good first agent to run over MCP, because the path varies: a car with no listing history needs fewer steps than one that was listed across town last month. Here is the whole agent in the Claude Agent SDK, with your CRM exposed as one read-only tool:

appraise.tsts
import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'
import { z } from 'zod'
import { getTradeLead } from './crm' // your CRM lookup

const crm = createSdkMcpServer({
  name: 'crm',
  version: '1.0.0',
  tools: [
    tool('get_trade_lead', 'Trade-in VIN, miles, ZIP and notes for one CRM lead', { leadId: z.string() },
      async ({ leadId }) => ({ content: [{ type: 'text', text: JSON.stringify(await getTradeLead(leadId)) }] }))
  ]
})

export async function appraise(leadId: string) {
  for await (const message of query({
    prompt: `Prepare an appraisal worksheet for CRM lead ${leadId}. Always pass miles and ZIP to pricing tools. Do not suggest an offer.`,
    options: {
      mcpServers: {
        marketcheck: { type: 'http', url: `https://api.marketcheck.com/mcp?api_key=${process.env.MARKETCHECK_API_KEY}` },
        crm
      },
      tools: [], // no built-in file or shell tools
      allowedTools: [
        'mcp__crm__get_trade_lead',
        'mcp__marketcheck__decode_vin_neovin',
        'mcp__marketcheck__get_car_history',
        'mcp__marketcheck__predict_price_with_comparables',
        'mcp__marketcheck__search_past_90_days'
      ]
    }
  })) {
    if (message.type === 'result' && message.subtype === 'success') return message.result
  }
}

Why the instruction about miles and ZIP: the MCP pricing tool's docs list defaults of 50,000 miles and ZIP 50501 (Des Moines, Iowa) for calls that leave them out.

A history entry looks like this, trimmed from the documented sample:

Response (trimmed)json
[
  {
    "price": 29995,
    "miles": 112158,
    "seller_type": "dealer",
    "source": "hattiesburgcars.com",
    "city": "Hattiesburg",
    "state": "MS",
    "first_seen_at_date": "2025-07-17T02:34:27.000Z",
    "last_seen_at_date": "2025-07-22T02:45:10.000Z"
  }
]

What a person approves. The number. The appraiser walks the car and sets the offer. The agent drafts the evidence and never sends anything to the customer.

Blueprint 3: the auction run-list buyer

This agent turns tomorrow's run list into a ranked buy list with a ceiling bid per car, based on how fast each car turns in your market and what it should retail for at your store.

  1. Run list (VIN, miles)
  2. Market days supply
  3. Retail price at your ZIP
  4. Recon, transport, target gross
  5. Buyer places every bid
  1. Load the run list from your auction platform's export: VIN and miles. To source cars instead, MarketCheck's Auction Search (/v2/search/car/auction/active) takes the same filters as Inventory Search. The docs note it covers the US only and is billed at a premium rate.
  2. Call /v2/mds/car with vin, exact=true, car_type=used, zip and radius for the market days supply of that exact build near you. MDS is active supply divided by average daily sales over the last 45 days, and it comes back null when nothing sold in that window.
  3. Call the Base tier, /v2/predict/car/us/marketcheck_price, for the retail estimate at your store's ZIP.
  4. Subtract your own numbers (recon, transport, target gross) to get a ceiling bid, then rank by days supply. The agent ranks and explains; your buyer places every bid.
buy_list.pypython
import os
import requests

BASE = "https://api.marketcheck.com/v2"
KEY = os.environ["MARKETCHECK_API_KEY"]

def get(path, **params):
    # 120 s matches the API gateway timeout
    r = requests.get(BASE + path, params={"api_key": KEY, **params}, timeout=120)
    r.raise_for_status()
    return r.json()

def score(car, store):
    supply = get("/mds/car", vin=car["vin"], exact="true", car_type="used",
                 zip=store["zip"], radius=store["radius"])
    retail = get("/predict/car/us/marketcheck_price", vin=car["vin"], miles=car["miles"],
                 dealer_type=store["dealer_type"], zip=store["zip"])["marketcheck_price"]
    ceiling = retail - store["recon"] - store["transport"] - store["target_gross"]
    return {**car, "mds": supply["mds"], "retail": retail, "ceiling_bid": ceiling}

def buy_list(run_list, store):
    # store holds your zip, radius, dealer_type, recon, transport and target_gross
    rows = []
    for car in run_list:
        try:
            rows.append(score(car, store))
        except requests.HTTPError as err:
            # A 400 or 422 from the price call means an invalid or undecodable VIN
            print(f"skipping {car['vin']}: {err}")
    # Fastest-turning first; no recent sales (mds is None) goes last
    return sorted(rows, key=lambda row: float("inf") if row["mds"] is None else row["mds"])

The MDS response is small. This is the documented sample for an exact build:

Responsejson
{
  "mds": 29,
  "total_active_cars_for_ymmt": 3157,
  "total_cars_sold_in_last_45_days": 4843
}

Blueprint 4: the competitor watch

The competitor watch sends a morning note on the cars you stock: which competitors cut their asking price in the last 24 hours, and which competing units just arrived.

  1. Daily, after 11:00 UTC
  2. VINs you stock
  3. Similar cars with price drops
  4. New arrivals nearby
  5. Note in Slack or email
  1. Once, find how MarketCheck lists your store: /v2/dealers/car?inventory_url=yourstore.com returns your dealer record. Pass that domain as exclude_sources so the watch skips your own listings.
  2. For each VIN you stock, search similar active cars nearby that dropped their price in the last 24 hours: vins with match, plus zip, radius, price_change=negative and first_seen_days=1-*. The docs pair price_change with first_seen_days for exactly this.
  3. For new competing arrivals, swap the price filter for first_seen_at_source_days=1-*.
  4. The agent groups results by model and writes the note, with each competing listing set against your asking price.
watch.tsts
import { mc } from './marketcheck'

interface Listing { vin: string, heading: string, price: number, ref_price: number, source: string, dom_active: number }

export async function overnightDrops(vin: string, store: { zip: string, domain: string }) {
  const res = await mc('/search/car/active', {
    vins: vin,
    match: 'year,make,model,trim',
    zip: store.zip,
    radius: 50, // your market; Free and Basic cap it at 100 miles
    price_change: 'negative',
    first_seen_days: '1-*',
    exclude_sources: store.domain,
    rows: 50 // the largest page the search returns
  })
  return (res.listings as Listing[])
    .filter(l => l.price && l.ref_price)
    .map(l => ({ ...l, drop: l.ref_price - l.price }))
    .sort((a, b) => b.drop - a.drop)
}

The fields to read are price, ref_price (the price previously listed at the same source) and price_change_percent. This documented sample is an increase, and a drop reads the same way with price below ref_price:

Response (trimmed)json
{
  "num_found": 2506,
  "listings": [
    {
      "vin": "7YAKRDDC7SY032516",
      "heading": "Hyundai IONIQ 5 Limited",
      "price": 60515,
      "ref_price": 60465,
      "price_change_percent": 0.08,
      "dom_active": 35,
      "source": "alexandriahyundai.com",
      "inventory_type": "new"
    }
  ]
}

Search pages hold at most 50 rows, and you page with start plus rows. How deep you can page is capped per plan, and going past the cap returns a 422, so narrow the filters rather than paging further. The docs also warn that price-change results can include corrections of earlier crawl errors, so drop implausible moves before they reach the note. The watch only reads, so it needs no approval step; any price response goes back through the repricing agent.

Guardrails that keep agents boring

  • A person on every write. The agent drafts prices, offers and bids, and a named person approves them. The integration you already trust writes the approved change.
  • Rules in code. Price floors and move caps live in configuration your managers own, and the model explains each decision within them.
  • Least privilege. List the exact tools the agent may call. In the developer portal, restrict each API key to the endpoints it needs and put an expiry date on keys you hand to a vendor. The account-level IP allowlist then limits calls to your own servers.
  • Respect both 429s. RateLimit-* headers describe the per-second limit and Quota-* headers the monthly quota. Honor Retry-After, and stop the run when the quota is spent.
  • Cache per day. Inventory data is published daily, so caching a VIN's response for the rest of the day costs little freshness and saves calls.
  • Cost per run. Multiply the counts in callsThisRun by the per-call rates on your plan's rate card, then log that total with your model provider's token cost for every run.
  • Timeouts. The API gateway ends requests at 120 seconds and does not charge for them. Set your tool timeouts to match.

Plans for building and for going live

Build and test on the Free tier: 500 calls a month at 5 calls per second, a 100-mile radius cap and no per-call charges. The cap is hard: calls return a 429 until the next month, and the quota is shared by every key on the account. Free is licensed for evaluation and development, not as a production data source. Spread over a 30-day month, 500 calls is about 16 a day, so point development runs at a handful of units and cache as you go.

When an agent goes live, move to a paid plan. Current plans and per-endpoint rates are on the pricing page, and usage alerts in the portal warn you before a quota runs out.

No developer on staff? Start with AI Desk

Staff who already work in ChatGPT or Claude can use MarketCheck AI Desk to run vehicle reports on the same data without writing code, and the AI Desk use cases show report runs written up end to end. A sensible split is AI Desk for one-off questions at the desk and your own agents for the jobs that run every day.

What this does not cover

  • UK stores. Every call here is a US endpoint. The UK API has its own endpoints, listed in the UK docs.
  • Writing back to your DMS. Each DMS integration differs, so the approve-then-write step is yours to build.
  • Model choice and token cost. Both depend on your provider and your prompts.
  • Your own pricing policy. Backtest the suggestions against your own sold units before they shape a rule.

Start this week

  1. Create a free account and copy your API key.
  2. Add the Docs MCP to your coding assistant.
  3. Drop in the REST helper and run Blueprint 1 against a few units.
  4. Post the drafts to Slack with an approve step before you schedule anything.
  5. Move to a paid plan before an agent works on live inventory.

Start building on the Free tier.

The Free tier is for development and testing, on the same endpoints you will use in production. When you go live, pick a plan on the pricing page.

Building with a coding assistant? Give it the free Docs MCP, so it looks up documented parameters instead of guessing them.

More use cases

All use cases