Build a Trading Card Price Agent

Build a multi-agent AI system that looks up trading card prices on eBay.

Introduction

30 Second Summary

Ever wondered how much that old Pokémon card sitting in your drawer is actually worth? Prices for trading cards can swing wildly across sellers, making it tough to know the real market value.

In this project, you will build an AI-powered trading card price agent that searches eBay for active listings and produces a structured pricing report. You'll use Google's Agent Development Kit (ADK) with TypeScript to create a multi-agent system inside a Next.js app.

What You'll Build

A Next.js application powered by Google ADK that uses a coordinator agent and two specialized sub-agents to search eBay for trading card listings and produce pricing reports.

By the end of this project, you'll have:

  • A multi-agent system using Google ADK where a coordinator agent delegates to specialized sub-agents.
  • A custom FunctionTool that authenticates with eBay and searches active trading card listings via the Browse API.
  • A Next.js chat interface with an API route that runs your agent and displays pricing reports.
  • Secret Mission: Add a card comparison feature to compare prices across multiple cards in a single query.

Are there any prerequisites?

This project assumes familiarity with TypeScript and Next.js. You'll also need an eBay Developer account with app credentials and a Gemini API key.

Scaffold the Next.js Project in Cursor

Before writing any agent logic, you need a working Next.js project with the right packages installed. This step gets everything scaffolded and organized inside Cursor.

You will create a new Next.js app, install Google ADK for building multi-agent systems, and set up the file structure your agents will live in.

In this step, get ready to:
  • Scaffold a new Next.js project with TypeScript and Tailwind.
  • Install Google ADK, zod, and ADK dev tools.
  • Create environment variables and the project file structure.
Open Cursor and create a new Next.js project
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Open the built-in terminal by pressing Ctrl+` (backtick key, found above Tab on most keyboards).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold the Next.js project by running this command:
npx create-next-app@latest card-price-agent --typescript --tailwind --eslint --app --src-dir --use-npm

What does this command do?

  • npx create-next-app@latest runs the latest Next.js project scaffolder.
  • card-price-agent is the name of your project folder.
  • The flags (--typescript, --tailwind, --eslint, --app, --src-dir, --use-npm) pre-select all the options so you skip the interactive prompts entirely.
  • Wait for the scaffolding to finish. You should see a Success! message in the terminal.
  • Move into the new project folder by running:
cd card-price-agent

Now open this folder in Cursor so the Explorer sidebar shows your project files.

  • In Cursor, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the card-price-agent folder, then click Open.

The Explorer sidebar on the left now shows your project files, including src/, package.json, and next.config.ts.

Don't see any files in the sidebar?

Make sure you selected the card-price-agent folder itself, not a folder above it. The sidebar should show src/ and package.json at the top level.

If the scaffold command failed, check that you have Node.js installed by running node --version in the terminal.

help me troubleshoot my create-next-app scaffold

Install ADK and project dependencies

Your Next.js project needs three additional packages. @google/adk is Google's Agent Development Kit for building multi-agent systems. zod is the schema validation library that ADK uses for tool parameter definitions.

  • Open the terminal in Cursor again with Ctrl+` if it is not already open.
  • Install the runtime dependencies by running:
npm install @google/adk zod
  • Install the ADK dev tools as a development dependency by running:
npm install -D @google/adk-devtools

What is @google/adk-devtools?

This package provides a local web UI for testing your agents during development. You run it with npx adk web to chat with your agents in a browser before wiring them into the Next.js frontend. The -D flag saves it as a dev-only dependency so it does not ship in production.

  • Confirm both installs succeeded by checking for an added message in the terminal output for each command.

Seeing install errors?

Make sure your terminal's working directory is inside card-price-agent. Run pwd to check. You should see a path ending in /card-price-agent.

If you see permission errors, try closing and reopening Cursor, then open the card-price-agent folder again.

help me fix my npm install errors

Now create the environment file that stores your API credentials.

  • In the Explorer sidebar, right-click on the top-level card-price-agent folder name.
  • Select New File.
  • Name the file .env.local and press Enter.
  • Paste the following into the file, replacing each placeholder with your real credentials:
GEMINI_API_KEY=your-gemini-api-key-here
EBAY_CLIENT_ID=your-ebay-client-id-here
EBAY_CLIENT_SECRET=your-ebay-client-secret-here

What goes in each line?

  • GEMINI_API_KEY is the API key from your Google AI Studio account. This lets ADK call Gemini models.
  • EBAY_CLIENT_ID and EBAY_CLIENT_SECRET come from your eBay Developer account. You will use these to authenticate with the eBay Browse API.

Keep your credentials private

The .env.local file is already listed in the default .gitignore that Next.js created. This means your credentials will not be committed to Git if you push this project later.

  • Save .env.local.
  • Confirm the file appears in the Explorer sidebar at the same level as package.json (the project root, not inside src/).

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

Here is what your .env.local should look like (with your own credentials in place of the placeholders):

GEMINI_API_KEY=your-gemini-api-key-here
EBAY_CLIENT_ID=your-ebay-client-id-here
EBAY_CLIENT_SECRET=your-ebay-client-secret-here
Create the project file structure

Your agent code will live in three new folders inside src/. The agents/ folder holds agent definitions. The tools/ folder holds custom FunctionTools. The lib/ folder holds utility code like the eBay auth helper.

  • Create the folders by running this command in the terminal:
mkdir -p src/agents src/tools src/lib src/app/api/agent
  • Create the placeholder files by running:
touch src/agents/agent.ts src/tools/ebay-search.ts src/lib/ebay-auth.ts src/app/api/agent/route.ts

What are all these files for?

  • agent.ts will define the coordinator agent and its two sub-agents.
  • ebay-search.ts will hold the custom FunctionTool that calls the eBay Browse API.
  • ebay-auth.ts will handle the OAuth token exchange with eBay's API.
  • route.ts will be the Next.js API route that runs the agent and returns responses.
  • Confirm the files exist by running:
ls src/agents src/tools src/lib src/app/api/agent

You should see agent.ts in agents, ebay-search.ts in tools, ebay-auth.ts in lib, and route.ts in api/agent.

Your full project structure inside src/ should now look like this:

src/
├── agents/
│   └── agent.ts
├── tools/
│   └── ebay-search.ts
├── lib/
│   └── ebay-auth.ts
├── app/
│   ├── api/
│   │   └── agent/
│   │       └── route.ts
│   └── page.tsx

Missing files or folders?

If the touch command did not work (common on some Windows setups), create the files manually. Right-click each folder in the Explorer sidebar and select New File.

Make sure page.tsx already exists inside src/app/. Next.js created it during scaffolding. You do not need to create it yourself.

help me set up the project file structure

Your project is scaffolded and every file is in place. Next up, you will build the eBay auth helper and search tool that powers your agent's ability to look up real card prices.

Build the eBay Auth Helper and Search Tool

Your project is scaffolded and your dependencies are installed. Now it is time to build the two files that give your agent the power to search eBay.

In this step, you will write an OAuth helper that authenticates with eBay's API and a custom FunctionTool that searches active trading card listings. You will also test the tool with Google ADK's built-in dev UI to confirm real data comes back.

In this step, get ready to:
  • Build an eBay OAuth token helper using the client credentials flow.
  • Create a custom FunctionTool that searches eBay for active trading card listings.
  • Test the tool with ADK dev tools to confirm it returns real listing data.
Create the eBay OAuth helper

Before your agent can search eBay, it needs to authenticate. eBay uses an OAuth client credentials flow where you exchange your app's client ID and secret for a short-lived access token.

  • In Cursor, open src/lib/ebay-auth.ts and replace any placeholder content with the following code:
let cachedToken: string | null = null;
let tokenExpiry: number = 0;

export async function getEbayToken(): Promise<string> {
  // Return the cached token if it has not expired yet
  if (cachedToken && Date.now() < tokenExpiry) {
    return cachedToken;
  }

  const clientId = process.env.EBAY_CLIENT_ID;
  const clientSecret = process.env.EBAY_CLIENT_SECRET;

  if (!clientId || !clientSecret) {
    throw new Error("Missing EBAY_CLIENT_ID or EBAY_CLIENT_SECRET in environment variables");
  }

  // Base64-encode the credentials for the Authorization header
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");

  // Request an application access token from eBay
  const response = await fetch("https://api.ebay.com/identity/v1/oauth2/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${credentials}`,
    },
    body: "grant_type=client_credentials&scope=https%3A%2F%2Fapi.ebay.com%2Foauth%2Fapi_scope",
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`eBay token request failed: ${response.status} ${errorText}`);
  }

  const data = await response.json();
  // Cache the token and set expiry 60 seconds before it actually expires
  cachedToken = data.access_token;
  tokenExpiry = Date.now() + data.expires_in * 1000 - 60000;

  return cachedToken!;
}

What does this code do?

This file exports one function, getEbayToken(), which handles the entire OAuth client credentials flow for eBay.

  • Two module-level variables (cachedToken and tokenExpiry) store the token across calls so you do not request a fresh one every time.
  • The function Base64-encodes your client ID and secret, then sends a POST to eBay's token endpoint with the client_credentials grant type.
  • The token is cached with a 60-second safety margin before its actual expiry. eBay tokens last 2 hours, so this avoids edge-case failures right at expiry.
  • Save src/lib/ebay-auth.ts.

You will not see a visible change yet. The next file you build uses this helper to authenticate before searching.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

let cachedToken: string | null = null;
let tokenExpiry: number = 0;

export async function getEbayToken(): Promise<string> {
  if (cachedToken && Date.now() < tokenExpiry) {
    return cachedToken;
  }

  const clientId = process.env.EBAY_CLIENT_ID;
  const clientSecret = process.env.EBAY_CLIENT_SECRET;

  if (!clientId || !clientSecret) {
    throw new Error("Missing EBAY_CLIENT_ID or EBAY_CLIENT_SECRET in environment variables");
  }

  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");

  const response = await fetch("https://api.ebay.com/identity/v1/oauth2/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${credentials}`,
    },
    body: "grant_type=client_credentials&scope=https%3A%2F%2Fapi.ebay.com%2Foauth%2Fapi_scope",
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`eBay token request failed: ${response.status} ${errorText}`);
  }

  const data = await response.json();
  cachedToken = data.access_token;
  tokenExpiry = Date.now() + data.expires_in * 1000 - 60000;

  return cachedToken!;
}
Create the eBay search FunctionTool

ADK gives agents capabilities through FunctionTool objects. Each tool has a name, a description the LLM reads to decide when to use it, a zod parameter schema, and an execute function that runs when the LLM calls it.

  • Open src/tools/ebay-search.ts and add the imports and the EbayListing interface at the top of the file:
import { FunctionTool } from "@google/adk";
import { z } from "zod";
import { getEbayToken } from "../lib/ebay-auth";

// Shape of each listing returned by the tool
interface EbayListing {
  title: string;
  price: string;
  currency: string;
  condition: string;
  imageUrl: string;
  itemUrl: string;
}

What does this code do?

  • FunctionTool is the ADK class that wraps a plain function so the LLM can call it by name.
  • zod defines the parameter schema. ADK reads this schema to tell the LLM what arguments the tool accepts.
  • The EbayListing interface describes the shape of each listing your tool returns. It keeps the data clean and predictable for the price analyzer agent later.
  • Below the interface in the same file, add the FunctionTool definition:
export const ebaySearchTool = new FunctionTool({
  name: "search_ebay_listings",
  description:
    "Searches eBay for active Buy It Now trading card listings by keyword. Returns listing titles, prices, conditions, and URLs.",
  parameters: z.object({
    query: z.string().describe("The trading card name to search for, e.g. 'Charizard Base Set 1st Edition'"),
    max_results: z
      .number()
      .optional()
      .default(10)
      .describe("Maximum number of listings to return. Defaults to 10."),
  }),
  execute: async ({ query, max_results }) => {
    // Get an OAuth token from the auth helper
    const token = await getEbayToken();

    // Build the query parameters for the Browse API
    const params = new URLSearchParams({
      q: query,
      limit: String(max_results ?? 10),
      filter: "buyingOptions:{FIXED_PRICE}",
      category_ids: "261328",
    });

    const response = await fetch(
      `https://api.ebay.com/buy/browse/v1/item_summary/search?${params.toString()}`,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          "X-EBAY-C-MARKETPLACE-ID": "EBAY_US",
        },
      }
    );

    if (!response.ok) {
      const errorText = await response.text();
      return { error: `eBay API error: ${response.status} ${errorText}` };
    }

    const data = await response.json();

    // Return early if eBay found no matching listings
    if (!data.itemSummaries || data.itemSummaries.length === 0) {
      return { listings: [], message: "No listings found for this card." };
    }

    // Map the raw eBay response into clean EbayListing objects
    const listings: EbayListing[] = data.itemSummaries.map(
      (item: Record<string, unknown>) => ({
        title: (item as { title?: string }).title ?? "Unknown",
        price: ((item as { price?: { value?: string } }).price?.value) ?? "0",
        currency: ((item as { price?: { currency?: string } }).price?.currency) ?? "USD",
        condition: (item as { condition?: string }).condition ?? "Unknown",
        imageUrl: ((item as { image?: { imageUrl?: string } }).image?.imageUrl) ?? "",
        itemUrl: (item as { itemWebUrl?: string }).itemWebUrl ?? "",
      })
    );

    return {
      listings,
      total: data.total ?? listings.length,
      query,
    };
  },
});

What does this code do?

This is the core tool your agent calls when a user asks about a trading card.

  • parameters uses a zod schema with query (the card name) and an optional max_results (defaults to 10). ADK passes this schema to the LLM so it knows what arguments to supply.
  • The execute function calls getEbayToken() for authentication, then hits the eBay Browse API search endpoint.
  • filter: "buyingOptions:{FIXED_PRICE}" restricts results to Buy It Now listings. category_ids: "261328" narrows to the Collectible Card Games category on eBay.
  • The response is mapped into clean EbayListing objects with title, price, condition, image URL, and item URL.
  • Save src/tools/ebay-search.ts.

You will not see a change yet. In the next substep you will test this tool with a real eBay query.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

import { FunctionTool } from "@google/adk";
import { z } from "zod";
import { getEbayToken } from "../lib/ebay-auth";

interface EbayListing {
  title: string;
  price: string;
  currency: string;
  condition: string;
  imageUrl: string;
  itemUrl: string;
}

export const ebaySearchTool = new FunctionTool({
  name: "search_ebay_listings",
  description:
    "Searches eBay for active Buy It Now trading card listings by keyword. Returns listing titles, prices, conditions, and URLs.",
  parameters: z.object({
    query: z.string().describe("The trading card name to search for, e.g. 'Charizard Base Set 1st Edition'"),
    max_results: z
      .number()
      .optional()
      .default(10)
      .describe("Maximum number of listings to return. Defaults to 10."),
  }),
  execute: async ({ query, max_results }) => {
    const token = await getEbayToken();

    const params = new URLSearchParams({
      q: query,
      limit: String(max_results ?? 10),
      filter: "buyingOptions:{FIXED_PRICE}",
      category_ids: "261328",
    });

    const response = await fetch(
      `https://api.ebay.com/buy/browse/v1/item_summary/search?${params.toString()}`,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          "X-EBAY-C-MARKETPLACE-ID": "EBAY_US",
        },
      }
    );

    if (!response.ok) {
      const errorText = await response.text();
      return { error: `eBay API error: ${response.status} ${errorText}` };
    }

    const data = await response.json();

    if (!data.itemSummaries || data.itemSummaries.length === 0) {
      return { listings: [], message: "No listings found for this card." };
    }

    const listings: EbayListing[] = data.itemSummaries.map(
      (item: Record<string, unknown>) => ({
        title: (item as { title?: string }).title ?? "Unknown",
        price: ((item as { price?: { value?: string } }).price?.value) ?? "0",
        currency: ((item as { price?: { currency?: string } }).price?.currency) ?? "USD",
        condition: (item as { condition?: string }).condition ?? "Unknown",
        imageUrl: ((item as { image?: { imageUrl?: string } }).image?.imageUrl) ?? "",
        itemUrl: (item as { itemWebUrl?: string }).itemWebUrl ?? "",
      })
    );

    return {
      listings,
      total: data.total ?? listings.length,
      query,
    };
  },
});
Test the tool with ADK dev tools

Before wiring the tool into the full multi-agent system, you should confirm it returns real eBay data. ADK ships with a dev UI that lets you chat with an agent in your browser.

The dev tools need an agent to load, but the full agent system is not built yet. You will create a temporary test agent in src/agents/agent.ts that wraps the search tool. You will replace it with the real multi-agent system in the next step.

  • Open src/agents/agent.ts and replace any placeholder content with a temporary test agent:
import { LlmAgent } from "@google/adk";
import { ebaySearchTool } from "../tools/ebay-search";

// Temporary test agent to verify the eBay search tool works
// You will replace this file entirely in the next step
export const rootAgent = new LlmAgent({
  name: "card_search_agent",
  model: "gemini-2.5-flash",
  description: "Searches eBay for active trading card listings by card name.",
  instruction: `You are a trading card search specialist. When given a card name, use the search_ebay_listings tool to find active Buy It Now listings on eBay. Pass the card name as the query. Return the raw search results without modification.`,
  tools: [ebaySearchTool],
  outputKey: "search_results",
});

Why a temporary agent?

ADK's npx adk web command scans for a rootAgent export to load into the dev UI. By exporting a minimal agent that wraps your eBay search tool, you can test the tool in a real conversation without building the full coordinator system first.

This agent gets completely replaced in the next step when you build the multi-agent system.

ADK dev tools load environment variables from a .env file, not .env.local. Your project currently stores credentials in .env.local (a Next.js convention).

  • Create a .env file in your project root (the card-price-agent folder) by running this command:
cp .env.local .env

Add .env to your .gitignore

Both .env and .env.local contain secrets. Make sure .env is listed in your .gitignore file so it never gets committed. Next.js already ignores .env.local by default.

  • Save src/agents/agent.ts.
  • Launch the ADK dev UI by running this command:
npx adk web src/agents
  • Open http://localhost:8000 in your browser.
  • In the top-left corner of the UI, select your agent from the dropdown. It should appear as card_search_agent.
  • Type a query like Charizard Base Set and press Enter.

You should see the agent call the search_ebay_listings tool and return a list of active eBay listings with titles, prices, and URLs.

Not seeing any listings?

  • Check that your .env file exists in the project root and contains GEMINI_API_KEY, EBAY_CLIENT_ID, and EBAY_CLIENT_SECRET with valid values.
  • If you see an eBay API error with status 401, your eBay client ID or secret may be incorrect. Double check them in your eBay Developer account dashboard.
  • If the agent dropdown is empty, make sure you ran npx adk web src/agents from the card-price-agent project root. ADK scans the directory you pass for agent files.

Help me debug why my eBay search tool is not returning listings in the ADK dev UI.

  • Press Ctrl+C in the terminal to stop the ADK dev server when you are done testing.

Your eBay integration is confirmed working. Next up, you will build the full multi-agent system with a coordinator that delegates to specialized search and analysis agents.

Define the Multi-Agent System

Your eBay search tool is tested and returning real listing data. Now you need agents that know how to use it.

In this step, you will define three agents that work together in a multi-agent architecture. A coordinator receives the user's query and delegates to two specialist sub-agents: one that searches eBay and one that analyzes the pricing data.

In this step, get ready to:
  • Create a card search agent and a price analyzer agent as sub-agents.
  • Create a coordinator agent that delegates work to the sub-agents.
  • Test the multi-agent system using ADK's built-in dev tools.
Create the card search agent and price analyzer agent

Each sub-agent in Google ADK is an LlmAgent with its own name, model, instructions, and optional tools. The key property that connects them is outputKey, which saves an agent's final response into shared state so the next agent can read it.

  • Open src/agents/agent.ts in Cursor. Replace any placeholder content with the following code:
import { LlmAgent } from "@google/adk";
import { ebaySearchTool } from "../tools/ebay-search";

// Sub-agent that searches eBay for card listings
const cardSearchAgent = new LlmAgent({
  name: "card_search_agent",
  model: "gemini-2.5-flash",
  description: "Searches eBay for active trading card listings by card name.",
  instruction: `You are a trading card search specialist. When given a card name, use the search_ebay_listings tool to find active Buy It Now listings on eBay. Pass the card name as the query. Return the raw search results without modification.`,
  tools: [ebaySearchTool], // Attach the eBay FunctionTool from the previous step
  outputKey: "search_results", // Saves this agent's output to shared state
});

// Sub-agent that reads search results from state and produces a pricing report
const priceAnalyzerAgent = new LlmAgent({
  name: "price_analyzer_agent",
  model: "gemini-2.5-flash",
  description: "Analyzes trading card listing data and produces a pricing summary report.",
  instruction: `You are a trading card price analyst. Read the search results from {search_results} and produce a structured pricing report with:
- Total number of active listings found
- Lowest price
- Highest price
- Median price (sort all prices and pick the middle value)
- Average price
- Top 3 cheapest listings with their titles, prices, and eBay URLs

Format the report clearly with labels and values. If no listings were found, say so and suggest broadening the search terms.`,
  outputKey: "price_report", // Saves the final report to shared state
});

What does this code do?

You are defining two specialist agents that each handle one job.

  • cardSearchAgent has the ebaySearchTool attached via tools. When called, it searches eBay and writes its response to the state key search_results.
  • priceAnalyzerAgent has no tools. It reads {search_results} from shared state (injected via the instruction template) and produces a pricing summary.
  • The description field on each agent tells the coordinator's LLM when to delegate to that agent. Make these descriptions clear and specific.

You won't see any output from these agents yet. The next substep adds the coordinator that ties them together.

Create the coordinator agent as the root agent

The coordinator is the entry point for your agent system. It receives user messages and decides which sub-agent to call based on the query. ADK's AutoFlow handles the actual transfer_to_agent calls behind the scenes.

  • In the same src/agents/agent.ts file, add the following code below the priceAnalyzerAgent definition:
// Coordinator agent that delegates to the two sub-agents
export const rootAgent = new LlmAgent({
  name: "card_price_coordinator",
  model: "gemini-2.5-flash",
  description: "A trading card pricing assistant that coordinates searching and price analysis.",
  instruction: `You are a trading card pricing assistant. When a user asks about a trading card's price or value, you coordinate two specialist agents:

1. First, delegate to card_search_agent to search eBay for active listings of the card.
2. Then, delegate to price_analyzer_agent to analyze the search results and produce a pricing report.

Always delegate to card_search_agent first before price_analyzer_agent. If the user asks a general question that is not about card pricing, answer it directly without delegating.

Be conversational and helpful. After the price report is generated, present it to the user and ask if they want to look up another card.`,
  subAgents: [cardSearchAgent, priceAnalyzerAgent], // Register both sub-agents
});

What does this code do?

  • subAgents registers the card search and price analyzer agents under this coordinator. ADK reads each sub-agent's description field to decide when to delegate.
  • The instruction explicitly tells the coordinator the delegation order: search first, then analyze. This guides the LLM's decision-making.
  • export const rootAgent makes this the entry point. ADK and your Next.js API route will import rootAgent to run the entire system.
  • Save src/agents/agent.ts.

✔️ Awesome, I've got everything!

Double check you have saved your file before moving on to testing.

ⓧ I'd like to double check the full code

Here is the complete src/agents/agent.ts file for comparison.

import { LlmAgent } from "@google/adk";
import { ebaySearchTool } from "../tools/ebay-search";

const cardSearchAgent = new LlmAgent({
  name: "card_search_agent",
  model: "gemini-2.5-flash",
  description: "Searches eBay for active trading card listings by card name.",
  instruction: `You are a trading card search specialist. When given a card name, use the search_ebay_listings tool to find active Buy It Now listings on eBay. Pass the card name as the query. Return the raw search results without modification.`,
  tools: [ebaySearchTool],
  outputKey: "search_results",
});

const priceAnalyzerAgent = new LlmAgent({
  name: "price_analyzer_agent",
  model: "gemini-2.5-flash",
  description: "Analyzes trading card listing data and produces a pricing summary report.",
  instruction: `You are a trading card price analyst. Read the search results from {search_results} and produce a structured pricing report with:
- Total number of active listings found
- Lowest price
- Highest price
- Median price (sort all prices and pick the middle value)
- Average price
- Top 3 cheapest listings with their titles, prices, and eBay URLs

Format the report clearly with labels and values. If no listings were found, say so and suggest broadening the search terms.`,
  outputKey: "price_report",
});

export const rootAgent = new LlmAgent({
  name: "card_price_coordinator",
  model: "gemini-2.5-flash",
  description: "A trading card pricing assistant that coordinates searching and price analysis.",
  instruction: `You are a trading card pricing assistant. When a user asks about a trading card's price or value, you coordinate two specialist agents:

1. First, delegate to card_search_agent to search eBay for active listings of the card.
2. Then, delegate to price_analyzer_agent to analyze the search results and produce a pricing report.

Always delegate to card_search_agent first before price_analyzer_agent. If the user asks a general question that is not about card pricing, answer it directly without delegating.

Be conversational and helpful. After the price report is generated, present it to the user and ask if they want to look up another card.`,
  subAgents: [cardSearchAgent, priceAnalyzerAgent],
});

Seeing a TypeScript error in Cursor?

  • Check that your import path matches exactly: ../tools/ebay-search. A wrong relative path will cause a module-not-found error.
  • Make sure ebaySearchTool is exported from src/tools/ebay-search.ts with export const. If the export is missing, the import will fail.
  • help me fix a TypeScript import error in my agent.ts file

Why LLM-driven delegation instead of hard-coded orchestration?

ADK also supports explicit orchestration patterns like SequentialAgent where you hard-code the agent execution order. The coordinator pattern used here lets the LLM decide when and whether to delegate based on the user's query.

This means your agent can answer general questions directly without calling the sub-agents. It also makes the system easier to extend with new sub-agents later.

Test the multi-agent system with ADK dev tools

ADK ships with a browser-based dev UI that lets you chat with your agents, inspect events, and see how delegation flows between agents. This is the fastest way to confirm the coordinator is delegating correctly.

  • Open a terminal in Cursor by pressing Ctrl+` (macOS and Windows).
  • Make sure you are in the card-price-agent project folder.
  • Launch the ADK dev UI by running this command:
npx adk web src/agents

What does this command do?

npx adk web starts ADK's built-in web server for testing agents. The src/agents argument tells it where to find your agent definitions. The server runs at http://localhost:8000 by default.

  • Open http://localhost:8000 in your browser.
  • In the top-left corner of the UI, select your agent from the dropdown. It should appear as agent (based on the filename).
  • Type a test query in the chat box, like How much is a Charizard Base Set worth? and press Enter.

You should see the coordinator delegate to card_search_agent first, then to price_analyzer_agent. The final response should include a pricing summary with lowest, highest, and median prices.

  • Click the Events tab on the left to inspect the delegation flow. You should see separate events for the coordinator, the card search agent's tool call, and the price analyzer's response.

Agent not showing in the dropdown?

  • Make sure you ran npx adk web src/agents from inside the card-price-agent project folder. Running it from the wrong directory means ADK cannot find your agent file.
  • Verify that agent.ts exports rootAgent. ADK looks for this specific export name to detect the entry-point agent.
  • If you see an eBay API error in the response, double check that your .env.local file has valid EBAY_CLIENT_ID and EBAY_CLIENT_SECRET values.
  • help me debug my ADK agent not showing up or not delegating correctly
  • Once you have confirmed the delegation flow works, stop the ADK dev server by pressing Ctrl+C in the terminal.

Your multi-agent system is working. The coordinator delegates search queries to the right sub-agent, and pricing data flows through shared state. Next up, you will wire this agent into a Next.js API route and build a chat UI so anyone can use it from a browser.

Wire the Agent into Next.js

Your multi-agent system is built and tested. The coordinator delegates to the card search agent, which talks to eBay, and then hands off to the price analyzer for a summary. Right now, you can only interact with it through ADK's dev tools.

In this step, you'll wire the agent into your Next.js app so anyone can use it from the browser. You'll create an API route that runs the agent, then build a simple chat UI to send queries and display pricing reports.

In this step, get ready to:
  • Create an API route that runs the multi-agent system.
  • Build a chat interface for sending card price queries.
  • Test the full end-to-end flow in the browser.
Create the API route

The API route is the bridge between your Next.js frontend and the ADK agent system. It receives a user message, runs it through the coordinator agent, and returns the final response as JSON.

  • Replace the placeholder code in src/app/api/agent/route.ts with the following:
import { InMemoryRunner, createUserContent } from "@google/adk";
import { rootAgent } from "../../../agents/agent";
import { NextResponse } from "next/server";

// Initialize the runner at module scope so it persists across requests
const runner = new InMemoryRunner({
  agent: rootAgent,
  appName: "card_price_agent",
});

export async function POST(request: Request) {
  try {
    // Extract message and optional session identifiers from the request body
    const { message, userId = "user-1", sessionId = "session-1" } = await request.json();

    if (!message || typeof message !== "string") {
      return NextResponse.json({ error: "Message is required" }, { status: 400 });
    }

    // Retrieve or create a session to maintain conversation context
    const session = await runner.sessionService.getOrCreateSession({
      appName: "card_price_agent",
      userId,
      sessionId,
    });

    // Run the agent and collect the stream of events
    const events = runner.runAsync({
      userId,
      sessionId: session.id,
      newMessage: createUserContent(message),
    });

    let finalResponse = "";

    // Iterate through events and capture the last text response from the agent
    for await (const event of events) {
      if (
        event.content &&
        event.content.parts &&
        event.content.parts.length > 0 &&
        event.author !== "user"
      ) {
        const textPart = event.content.parts.find(
          (part: Record<string, unknown>) => typeof (part as { text?: unknown }).text === "string"
        );
        if (textPart && "text" in textPart) {
          finalResponse = textPart.text as string;
        }
      }
    }

    return NextResponse.json({ response: finalResponse });
  } catch (error) {
    console.error("Agent error:", error);
    return NextResponse.json(
      { error: "Failed to process request" },
      { status: 500 }
    );
  }
}

What does this code do?

  • InMemoryRunner manages agent execution and session state in memory. It is initialized outside the handler so it persists across requests during development.
  • getOrCreateSession() retrieves an existing session or creates a new one. This keeps conversation context intact across multiple messages.
  • runner.runAsync() returns an async iterator of events. The loop captures the last text response from the coordinator agent, filtering out user messages.
  • If anything goes wrong, the catch block logs the error and returns a 500 response.

You won't see a change yet. The next substep builds the chat UI that calls this route.

✔️ Awesome, I've got everything!

Great. Double check you've saved route.ts before moving on.

ⓧ I'd like to double check the full code

import { InMemoryRunner, createUserContent } from "@google/adk";
import { rootAgent } from "../../../agents/agent";
import { NextResponse } from "next/server";

const runner = new InMemoryRunner({
  agent: rootAgent,
  appName: "card_price_agent",
});

export async function POST(request: Request) {
  try {
    const { message, userId = "user-1", sessionId = "session-1" } = await request.json();

    if (!message || typeof message !== "string") {
      return NextResponse.json({ error: "Message is required" }, { status: 400 });
    }

    const session = await runner.sessionService.getOrCreateSession({
      appName: "card_price_agent",
      userId,
      sessionId,
    });

    const events = runner.runAsync({
      userId,
      sessionId: session.id,
      newMessage: createUserContent(message),
    });

    let finalResponse = "";

    for await (const event of events) {
      if (
        event.content &&
        event.content.parts &&
        event.content.parts.length > 0 &&
        event.author !== "user"
      ) {
        const textPart = event.content.parts.find(
          (part: Record<string, unknown>) => typeof (part as { text?: unknown }).text === "string"
        );
        if (textPart && "text" in textPart) {
          finalResponse = textPart.text as string;
        }
      }
    }

    return NextResponse.json({ response: finalResponse });
  } catch (error) {
    console.error("Agent error:", error);
    return NextResponse.json(
      { error: "Failed to process request" },
      { status: 500 }
    );
  }
}
Build the chat UI

The chat UI sends POST requests to the API route and displays the agent's responses in a scrollable message list. It uses Tailwind for styling.

  • Replace the placeholder code in src/app/page.tsx with the following:
"use client";

import { useState, useRef, useEffect } from "react";

// Define the shape of a chat message
interface Message {
  role: "user" | "agent";
  content: string;
}

export default function Home() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  // Auto-scroll to the latest message when the list updates
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!input.trim() || loading) return;

    const userMessage = input.trim();
    setInput("");
    // Add the user's message to the chat immediately
    setMessages((prev) => [...prev, { role: "user", content: userMessage }]);
    setLoading(true);

    try {
      // Send the message to the agent API route
      const res = await fetch("/api/agent", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: userMessage }),
      });

      const data = await res.json();

      // Add the agent's response to the chat
      setMessages((prev) => [
        ...prev,
        { role: "agent", content: data.response || data.error || "No response" },
      ]);
    } catch {
      setMessages((prev) => [
        ...prev,
        { role: "agent", content: "Something went wrong. Please try again." },
      ]);
    } finally {
      setLoading(false);
    }
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-4 max-w-2xl mx-auto">
      <div className="w-full flex flex-col h-screen">
        <h1 className="text-2xl font-bold py-4 text-center">
          Trading Card Price Agent
        </h1>

        {/* Message list */}
        <div className="flex-1 overflow-y-auto space-y-4 pb-4">
          {messages.length === 0 && (
            <p className="text-gray-500 text-center mt-8">
              Ask about any trading card to get eBay pricing data.
              <br />
              Try: &quot;How much is a Charizard Base Set worth?&quot;
            </p>
          )}

          {messages.map((msg, i) => (
            <div
              key={i}
              className={`p-3 rounded-lg whitespace-pre-wrap ${
                msg.role === "user"
                  ? "bg-blue-600 text-white ml-8"
                  : "bg-gray-100 text-gray-900 mr-8"
              }`}
            >
              {msg.content}
            </div>
          ))}

          {/* Loading indicator while the agent processes */}
          {loading && (
            <div className="bg-gray-100 text-gray-500 p-3 rounded-lg mr-8">
              Searching eBay and analyzing prices...
            </div>
          )}

          <div ref={messagesEndRef} />
        </div>

        {/* Input form */}
        <form onSubmit={handleSubmit} className="flex gap-2 py-4">
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Enter a trading card name..."
            className="flex-1 p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            disabled={loading}
          />
          <button
            type="submit"
            disabled={loading || !input.trim()}
            className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
          >
            Send
          </button>
        </form>
      </div>
    </main>
  );
}

What does this code do?

  • The Message interface tracks whether each message came from the user or the agent.
  • handleSubmit POSTs the user's message to /api/agent, then pushes the response onto the messages array.
  • The useEffect auto-scrolls to the latest message using messagesEndRef.
  • A loading state disables the input and shows a "Searching eBay..." indicator while the agent processes the query.
  • Save page.tsx.

You won't see the UI working yet. The next substep starts the dev server so you can test the full flow.

✔️ Awesome, I've got everything!

Great. Double check you've saved page.tsx before moving on.

ⓧ I'd like to double check the full code

"use client";

import { useState, useRef, useEffect } from "react";

interface Message {
  role: "user" | "agent";
  content: string;
}

export default function Home() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!input.trim() || loading) return;

    const userMessage = input.trim();
    setInput("");
    setMessages((prev) => [...prev, { role: "user", content: userMessage }]);
    setLoading(true);

    try {
      const res = await fetch("/api/agent", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: userMessage }),
      });

      const data = await res.json();

      setMessages((prev) => [
        ...prev,
        { role: "agent", content: data.response || data.error || "No response" },
      ]);
    } catch {
      setMessages((prev) => [
        ...prev,
        { role: "agent", content: "Something went wrong. Please try again." },
      ]);
    } finally {
      setLoading(false);
    }
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-4 max-w-2xl mx-auto">
      <div className="w-full flex flex-col h-screen">
        <h1 className="text-2xl font-bold py-4 text-center">
          Trading Card Price Agent
        </h1>

        <div className="flex-1 overflow-y-auto space-y-4 pb-4">
          {messages.length === 0 && (
            <p className="text-gray-500 text-center mt-8">
              Ask about any trading card to get eBay pricing data.
              <br />
              Try: &quot;How much is a Charizard Base Set worth?&quot;
            </p>
          )}

          {messages.map((msg, i) => (
            <div
              key={i}
              className={`p-3 rounded-lg whitespace-pre-wrap ${
                msg.role === "user"
                  ? "bg-blue-600 text-white ml-8"
                  : "bg-gray-100 text-gray-900 mr-8"
              }`}
            >
              {msg.content}
            </div>
          ))}

          {loading && (
            <div className="bg-gray-100 text-gray-500 p-3 rounded-lg mr-8">
              Searching eBay and analyzing prices...
            </div>
          )}

          <div ref={messagesEndRef} />
        </div>

        <form onSubmit={handleSubmit} className="flex gap-2 py-4">
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Enter a trading card name..."
            className="flex-1 p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            disabled={loading}
          />
          <button
            type="submit"
            disabled={loading || !input.trim()}
            className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
          >
            Send
          </button>
        </form>
      </div>
    </main>
  );
}
Run the app and test it

Both files are ready. Time to start the dev server and test the complete flow from the browser to eBay and back.

  • Start the Next.js dev server by running this command:
npm run dev
  • Open http://localhost:3000 in your browser.
  • Type Charizard Base Set into the input field.
  • Click Send.

You should see a "Searching eBay and analyzing prices..." loading message. After a few seconds, the agent responds with a pricing report showing listing counts, price ranges, and links to the cheapest listings.

  • Try a few different cards to confirm the agent handles varied queries.

Not seeing a response?

  • Check your terminal for errors. If you see Missing EBAY_CLIENT_ID, make sure your .env.local file has the correct eBay credentials and your dev server was restarted after creating it.
  • If you see eBay token request failed, verify that your eBay Client ID and Client Secret are from a production keyset, not sandbox.
  • If the page loads but nothing happens when you click Send, open your browser's developer console and check for network errors on the /api/agent request.

Help me debug my trading card agent in Next.js

Secret mission

Card Comparison Mode

Your agent can look up one card at a time. What if you could compare prices across multiple cards in a single query? Add comparison mode to your coordinator and find out which card is the better deal.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with free-tier API access, so there are no ongoing costs.

Resources you used:

  • Next.js project folder (card-price-agent) on your Desktop.
  • Environment variables in .env.local (Gemini API key, eBay client ID, eBay client secret).
  • eBay Developer account and app credentials.
  • Gemini API key from Google AI Studio.
  • npm packages (@google/adk, zod, @google/adk-devtools).

Keep everything running

No action needed. Choose this if you are still actively building or want to extend the project with the Secret Mission.

  • Your card-price-agent folder stays on your Desktop.
  • Your .env.local file keeps your API credentials ready for the next session.
  • Run npm run dev any time to start the app again.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files and credentials stay intact for next time.

  • Stop the Next.js dev server by pressing Ctrl+C in the terminal where it is running.
  • If you ran npx adk web in a separate terminal, stop that process with Ctrl+C as well.
  • Your project folder and .env.local file remain on disk. When you return, open the folder in Cursor and run npm run dev to pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes local files and optionally revokes your API credentials.

  • Stop the Next.js dev server by pressing Ctrl+C in the terminal.
  • Delete the card-price-agent folder from your Desktop.
  • If you no longer need eBay API access, log into the eBay Developer Portal and delete your application keyset.
  • If you no longer need Gemini API access, visit Google AI Studio and delete or revoke your API key.

Mission accomplished

That's a Wrap!

Nice work! You just built a fully functional multi-agent trading card pricing system using Google ADK TypeScript and the eBay Browse API.

You've learned how to:

  • Built a multi-agent system with a coordinator agent that uses LLM-driven delegation to route queries to specialized sub-agents.
  • Created a custom FunctionTool that authenticates with eBay's OAuth client credentials flow and searches active trading card listings via the Browse API.
  • Wired ADK agents into a Next.js API route using InMemoryRunner and built a chat UI that lets you look up trading card prices with natural language.
  • Added a card comparison mode so the agent can compare prices across multiple cards in a single query.

Ready to quiz yourself?