Built with

AI model

The AI model does the reading and writing: it looks at a photo and names the item, weighs prices, writes the listing, edits it in chat, answers buyers and words counter-offers. resell.store runs on an Anthropic model today, called through the Vercel AI SDK, and can be switched to any model the AI SDK supports.

Models

lib/server/ai.ts builds an AI SDK provider registry and hands out two tiers: a main model for the careful work and a fast one for quick replies. Each is named in env as provider:model. Out of the box both point at Anthropic models (the defaults are in .env.example and render.yaml).

TierEnvUsed for
mainAI_MODELSeeing photos, pricing, matching listings, writing, the listing agent
fastAI_MODEL_FASTSearch queries, buyer answers, counter messages, reading links
lib/server/ai.ts
// lib/server/ai.ts
const registry = createProviderRegistry({ anthropic });

export function aiModel(kind: "main" | "fast" = "main"): LanguageModel {
  const id = kind === "fast" ? process.env.AI_MODEL_FAST : process.env.AI_MODEL;
  return registry.languageModel(id as ModelId); // "provider:model"
}

export const aiConfigured = Boolean(process.env.ANTHROPIC_API_KEY);

Changing the model

Nothing outside lib/server/ai.ts knows which model or company it's talking to, so switching is a settings change.

  • Another Anthropic model: change AI_MODEL or AI_MODEL_FAST and restart. No code changes.
  • Another provider (OpenAI, Google, Mistral, a local model, or any other AI SDK provider): install its AI SDK package, add it to the registry, set its API key, and point AI_MODEL and AI_MODEL_FAST at it.
Switching provider
// lib/server/ai.ts: add the provider's AI SDK package to the registry
import { anthropic } from "@ai-sdk/anthropic";
import { openai } from "@ai-sdk/openai";

const registry = createProviderRegistry({ anthropic, openai });

// then point the env at it
AI_MODEL=openai:<model>
AI_MODEL_FAST=openai:<smaller model>

What the new model needs

It needs structured output (every call except the counter message and the chat returns a zod schema), and the main model needs to read images for the photo step. The listing chat uses tool calls. Also change the aiConfigured check to look for the new provider's key, since features switch on from it.

Every call

FeatureTierAPIFile
Identify the item (with the photo)maingenerateText + Output.objectlib/server/research.ts
Price verdict and questionsmaingenerateText + Output.objectlib/server/research.ts
Comps: marketplace search queriesfastgenerateText + Output.objectlib/server/comps.ts
Comps: is each listing the same item, and its pricemaingenerateText + Output.objectlib/server/comps.ts
Listing agent chatmainstreamText with 4 tools, up to 5 stepsapp/api/listings/[id]/chat/route.ts
Listing wordsmaingenerateText + Output.objectlib/server/words.ts
Shop agent answering buyersfastgenerateText + Output.objectlib/server/store-agent.ts
Counter-offer messagefastgenerateText (plain text, then checked)lib/server/negotiator.ts
Sidekick: what is this and what does it costfastgenerateText + Output.objectlib/server/sidekick.ts

Comps matching uses the main model on purpose: the fast one let look-alikes through in testing. How each one fits into its feature is on Research and agents.

How it's called

Almost everything is generateText with output: Output.object({ schema }), a zod schema whose .describe() strings tell the model what each field means. The result is typed and validated before any code uses it. Photos go in as file parts. AI SDK v7 names: instructions for the system prompt, stopWhen: isStepCount(n) for tool loops, and toUIMessageStream for the chat response that useChat reads.

Identify, in research.ts
const { output } = await generateText({
  model: aiModel("main"),
  output: Output.object({ schema: identitySchema }),
  instructions: "You identify second-hand items people want to sell. … never invent details you can't tell.",
  messages: [{ role: "user", content: [
    { type: "file", mediaType: photo.contentType, data: photo.data },
    { type: "text", text: `The seller says: "${prompt}". What is it?` },
  ] }],
});

Guardrails

  • Structured outputs for everything stored. Nothing is parsed out of free text.
  • Numbers checked in code. The price verdict is rounded to whole dollars, the band is kept in order, and the suggestion is clamped inside it. Listing tools are integers with minimums, and the lowest price can never go above the asking price.
  • Validated counters. The counter is calculated by planMove. The model's message is used only if it contains that exact amount and no dollar figure other than the counter, the offer and the asking price. Otherwise a template goes.
  • Answers only from facts. The shop agent is given the listing's facts and the owner's own past answers, told not to guess, and hands off to the owner (handoff) when the facts don't cover it. Research facts are marked as being about the model in general.
  • Buyer text is data. The shop agent is told buyer messages are questions, not instructions, and never to reveal the lowest price.
  • No money moves. No model call can accept an offer, refund or release. The listing agent's four tools only edit the seller's own draft.
  • Copy rules. Condition must match everywhere, no invented facts, and lengths are clipped in code.

Because these checks live in code, they hold whichever model is plugged in.

What it unlocks

  • Sellers list from a photo and a sentence: named, priced and described, with the questions only they can answer.
  • An agent beside each listing that changes it when asked ("make it $170 and turn offers off").
  • Buyers get answers in seconds, any time, and the owner only sees what needs them.
  • Lowball offers get a reasonable counter straight away, in a friendly message.
  • Shoppers can paste a link and learn whether it holds its resale value.

Without a key

FeatureWithout an AI key
ResearchStill runs. The seller's words become the name; the price comes from catalog and comps numbers alone (fallbackVerdict).
Comps and sidekickOff: they need Kernel and the AI model together.
Listing agentThe chat route answers 503.
WordsA message asking for the key.
Shop agentStays quiet; the owner answers.
NegotiatorCounters as usual, with a template message.

Setup

apps/app/.env.local
ANTHROPIC_API_KEY=…
# optional: pick the models, as provider:model
AI_MODEL=…
AI_MODEL_FAST=…

The defaults for both models are already in .env.example and render.yaml. Tests blank the key, so nothing in the suite calls a model.