# Vercel AI SDK: check a quote before citing it

Last updated: 2026-10-12

A Vercel AI SDK (`ai`) agent with two AttestPage tools: `verify_quote` (US$0.01 a call) and `check_links` (US$0.005 per batch of up to 10 URLs). It uses the `attestpage.mjs` client from [openai-agents-sdk.md](/docs/examples/openai-agents-sdk): with no key it calls the free trial, which allows 5 calls per IP address per UTC day and up to 3 URLs per `check_links` call. With `ATTESTPAGE_X402_KEY` set, it pays each call with x402 on Base, in USDC. When a call needs payment and no key is set, the tool returns the price to the agent instead of raising, so the agent can say what the call would cost.

With no `ANTHROPIC_API_KEY`, the file skips the model and calls `verify_quote` once itself, printing the JSON the agent would get.

For the same check in other frameworks, see [LangChain](/docs/examples/langchain), [LlamaIndex.TS](/docs/examples/llamaindex), [OpenAI Agents SDK](/docs/examples/openai-agents-sdk) and [Claude Agent SDK](/docs/examples/claude-agent-sdk).

Node.js 20 or later. Save the client as `attestpage.mjs` next to `vercel-ai-sdk.mjs`.

## Install

```sh
npm init -y && npm pkg set type=module
npm install ai@7 @ai-sdk/anthropic zod
npm install @x402/fetch @x402/evm viem   # only needed to pay
export ANTHROPIC_API_KEY=...             # leave unset to call the tool without a model
```

Any model the AI SDK supports works: install its provider package and pass its model as `model`.

## vercel-ai-sdk.mjs

```js
// Vercel AI SDK agent that checks a quote with AttestPage before it cites it.
// With no ANTHROPIC_API_KEY it skips the model and calls verify_quote itself, so you can see what the tool returns.
import { z } from 'zod';
import { createClient } from './attestpage.mjs';

const attestpage = await createClient();

// tool() from 'ai' only adds types, so plain objects work. Tools return the route's JSON.
const tools = {
  verify_quote: {
    description: 'Use before quoting a web page: is this exact or near-exact quote on the page at url? '
      + 'Returns result.match (exact, fuzzy or none), page.content_kind and a signed receipt. US$0.01.',
    inputSchema: z.object({ url: z.string(), quote: z.string() }),
    execute: ({ url, quote }) => attestpage.verifyQuote({ url, quote }),
  },
  check_links: {
    description: 'Use before putting links in an answer: status, final URL and content_kind for each URL. '
      + 'Up to 10 URLs (3 on the free trial). US$0.005 per batch.',
    inputSchema: z.object({ urls: z.array(z.string()).min(1).max(10) }),
    execute: ({ urls }) => attestpage.checkLinks({ urls }),
  },
};

const args = process.argv.slice(2);
if (!process.env.ANTHROPIC_API_KEY) {
  // No model: node vercel-ai-sdk.mjs [url] [quote]
  const [url = 'https://www.iana.org/help/example-domains', quote = 'These domains may be used as illustrative examples in documents'] = args;
  console.log(JSON.stringify(await tools.verify_quote.execute(tools.verify_quote.inputSchema.parse({ url, quote })), null, 2));
} else {
  const [{ generateText, isStepCount }, { anthropic }] = await Promise.all([import('ai'), import('@ai-sdk/anthropic')]);
  const question = args.join(' ')
    || 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents"?';
  const { text } = await generateText({
    model: anthropic(process.env.MODEL || 'claude-sonnet-5-5'),
    tools,
    stopWhen: isStepCount(5),
    instructions: 'Before you quote a web page, call verify_quote. Cite a quote only when result.match is exact or fuzzy '
      + 'and page.content_kind is real. If a tool returns payment_required, give the price and stop.',
    prompt: question,
  });
  console.log(text);
}
```

```sh
node vercel-ai-sdk.mjs                              # free trial
ATTESTPAGE_X402_KEY=0x... node vercel-ai-sdk.mjs    # pays US$0.01 per verify_quote in USDC
```

With no `ANTHROPIC_API_KEY`, pass a URL and a quote instead of a question: `node vercel-ai-sdk.mjs <url> "<quote>"`.

## What comes back

`verify_quote` returns the route's JSON. The parts an agent needs:

- `result.match`: `exact`, `fuzzy` or `none`, with `result.score` from 0 to 1.
- `page.content_kind`: read it before trusting `match`. A `none` on a `bot_wall` or `js_shell` page means the page could not be read, not that the quote is missing.
- `receipt`: keep it with the answer. `POST /v1/receipt/verify` checks it for free.
