# Mastra: check several quotes on one page

Last updated: 2026-10-13

A Mastra agent that checks up to 20 quotes from one web page in one call to `POST /v1/verify/quotes` before it cites them. Price: US$0.02 per call, for 1 to 20 quotes. Each quote comes back `exact`, `fuzzy` or `none`, with offsets and context, plus the page's verdict and an Ed25519-signed receipt. Payments are in USDC with x402 v2 on Base mainnet (real USDC).

The tool calls the route over HTTP and pays with `@x402/fetch`.

The same check for other frameworks: [OpenAI Agents SDK](https://vehcdj664efetfrsolne5umanq.srv.us/examples/openai-agents.md), [Claude Agent SDK](https://vehcdj664efetfrsolne5umanq.srv.us/examples/claude-agent-sdk.md), [Vercel AI SDK](https://vehcdj664efetfrsolne5umanq.srv.us/examples/vercel-ai-sdk.md), [LangChain](https://vehcdj664efetfrsolne5umanq.srv.us/examples/langchain.md), [LlamaIndex](https://vehcdj664efetfrsolne5umanq.srv.us/examples/llamaindex.md), [CrewAI](https://vehcdj664efetfrsolne5umanq.srv.us/examples/crewai.md) and [Pydantic AI](https://vehcdj664efetfrsolne5umanq.srv.us/examples/pydantic-ai.md).

## Install

Node.js 20 or later.

```sh
npm init -y && npm pkg set type=module
npm install @mastra/core zod @x402/fetch @x402/evm viem
export ANTHROPIC_API_KEY=...
export ATTESTPAGE_X402_KEY=0x...
```

Any model Mastra supports works: set `MODEL` to its `provider/model` string and the provider's API key. `ATTESTPAGE_X402_KEY` is the private key (`0x` and 64 hex digits) of a wallet holding a little USDC. The key stays in your process.

## agent-http.mjs

```js
// agent-http.mjs: checks quotes with POST /v1/verify/quotes, paying per call with x402.
import { Agent } from '@mastra/core/agent';
import { createTool } from '@mastra/core/tools';
import { AsyncLocalStorage } from 'node:async_hooks';
import { wrapFetchWithPayment, x402Client } from '@x402/fetch';
import { registerExactEvmScheme } from '@x402/evm/exact/client';
import { privateKeyToAccount } from 'viem/accounts';
import { z } from 'zod';

const ORIGIN = process.env.ATTESTPAGE_URL || 'https://vehcdj664efetfrsolne5umanq.srv.us';
const MAX_UNITS = 20000; // US$0.02 in USDC (6 decimals): the most one call may cost

const client = new x402Client();
registerExactEvmScheme(client, { signer: privateKeyToAccount(process.env.ATTESTPAGE_X402_KEY) });
const refusedScope = new AsyncLocalStorage(); // the refused list of the one call that is running
client.registerPolicy((_version, reqs) => reqs.filter((r) => {
  const keep = r.network === 'eip155:8453' && Number(r.amount) <= MAX_UNITS;
  if (!keep) refusedScope.getStore()?.push(`US$${Number(r.amount) / 1_000_000} in USDC on ${r.network}`);
  return keep;
}));
const payingFetch = wrapFetchWithPayment(fetch, client);

const verifyQuotes = createTool({
  id: 'verify_quotes',
  description: 'Use before quoting one web page several times: checks 1 to 20 quotes against the page at url. '
    + 'Returns results[i].match (exact, fuzzy or none), page.content_kind and a signed receipt. US$0.02 per call.',
  inputSchema: z.object({ url: z.string(), quotes: z.array(z.string()).min(1).max(20) }),
  execute: async ({ url, quotes }) => {
    const refused = []; // offers the policy turned down during this call
    return refusedScope.run(refused, async () => {
      try {
        const res = await payingFetch(`${ORIGIN}/v1/verify/quotes`, {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ url, quotes }),
        });
        if (res.status === 402) return { error: 'payment required', paymentRequired: res.headers.get('payment-required') };
        return await res.json();
      } catch (e) {
        // payment refused by the policy above, network error: tell the agent why
        if (refused.length) return { error: `${e.message}. Refused offer: ${refused.join('; ')}. This recipe pays at most US$0.02 in USDC on eip155:8453.` };
        return { error: e.message };
      }
    });
  },
});

const agent = new Agent({
  id: 'quote-checker',
  name: 'Quote checker',
  instructions: 'Before you quote a web page, call verify_quotes once with every quote you plan to use from it. '
    + 'Cite a quote only when its match is exact or fuzzy and page.content_kind is real. '
    + 'If the tool returns an error, say what it was and stop.',
  model: process.env.MODEL || 'anthropic/claude-sonnet-5-5',
  tools: { verify_quotes: verifyQuotes },
});

const question = process.argv.slice(2).join(' ')
  || 'Does https://www.iana.org/help/example-domains say "These domains may be used as illustrative examples in documents" and "without prior coordination with us"?';
const result = await agent.generate(question);
console.log(result.text);
```

```sh
node agent-http.mjs   # pays US$0.02 per call in USDC
```

The first request gets a 402 with the price in the `PAYMENT-REQUIRED` header; `@x402/fetch` signs the payment and sends it again. The policy keeps only offers on Base (`eip155:8453`) of at most US$0.02; anything else is refused before anything is signed, and the tool returns the reason, or a 402 that was not paid, as `error`. A request the service refuses (bad URL, empty quote) is refused before payment and not charged.

## What comes back

- `results[i].match`: `exact`, `fuzzy` or `none`, in the order of `quotes`. Read `page.content_kind` first: `none` on a `bot_wall` or `js_shell` page means the page could not be read.
- `receipt`: signed evidence of what the page said and when our fetcher saw it, as a JWS. `POST /v1/receipt/verify` checks it for free, or check it offline with [`verify-receipt.mjs`](https://vehcdj664efetfrsolne5umanq.srv.us/docs/verify-offline). A receipt records what was on the page at that time, not whether a statement on it is correct.
- Try the answer shape first with the free sample: `GET https://vehcdj664efetfrsolne5umanq.srv.us/v1/sample/quotes`.

## Terms

Pay per call, no account. See [Payments](https://vehcdj664efetfrsolne5umanq.srv.us/docs/payments), [Limits](https://vehcdj664efetfrsolne5umanq.srv.us/docs/limits) and the [Terms](https://vehcdj664efetfrsolne5umanq.srv.us/terms).
