Quickstart

Wuzzy is provable, decentralized search. Every result carries onchain proof of what was crawled and when, so an agent can check what it bought instead of trusting the operator.

There is no account and no API key. A signed payment is the only credential.

The handshake

Ask without paying. The API answers 402 with what it will take, in both versions of the x402 protocol at once: version 2 in a PAYMENT-REQUIRED header, version 1 in the body. Use whichever your client speaks. The quote is the same.

curl -sS -i -X POST https://api.wuzzy.io/search \
  -H 'content-type: application/json' \
  -d '{"query":"how do I deploy a contract on Base"}'

The PAYMENT-REQUIRED header is base64 encoded JSON. Decoded, it reads:

{
  "x402Version": 2,
  "error": "X-PAYMENT or PAYMENT-SIGNATURE header is required",
  "resource": {
    "url": "https://api.wuzzy.io/search",
    "description": "One Wuzzy search query with onchain provenance",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "10000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x0Deb462437ab46F703fcd15F9cf9c9Ea6472EAcB",
      "maxTimeoutSeconds": 60,
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ]
}

The body carries the same quote for version 1 clients:

{
  "x402Version": 1,
  "error": "X-PAYMENT or PAYMENT-SIGNATURE header is required",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "10000",
      "resource": "https://api.wuzzy.io/search",
      "description": "One Wuzzy search query with onchain provenance",
      "mimeType": "application/json",
      "payTo": "0x0Deb462437ab46F703fcd15F9cf9c9Ea6472EAcB",
      "maxTimeoutSeconds": 60,
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "extra": { "name": "USD Coin", "version": "2" }
    }
  ]
}

Sign an authorization for that quote, base64 encode the payment, and retry with it in the header for its version: PAYMENT-SIGNATURE for version 2, X-PAYMENT for version 1. A payment sent in the other version's header is refused, and so is a request carrying both.

curl -sS -X POST https://api.wuzzy.io/search \
  -H 'content-type: application/json' \
  -H "PAYMENT-SIGNATURE: $PAYMENT" \
  -d '{"query":"how do I deploy a contract on Base"}'

The settlement comes back in the same version: PAYMENT-RESPONSE, or X-PAYMENT-RESPONSE for a version 1 payment.

amount, which version 1 calls maxAmountRequired, is in USDC's atomic units, six decimals, so 10000 is one cent.

Two ordering guarantees are worth knowing, because they are what make the meter safe to point an autonomous agent at:

  • Settlement happens after the response body exists. A query that fails is never charged for.
  • Access control runs before settlement. A caller who is not permitted to read an index gets their 403 without being charged to find out.
  • A query that finds nothing is still a query, and is charged for. "Failed" means the request errored: a blank query, a rejected payment, a wallet that may not read the index. Zero results is a successful search of a corpus that does not contain the answer, and it costs the same as one that does. Probe accordingly.

Let a client do it

Any x402 client handles the handshake for you. The signing is an EIP-3009 authorization, which is gasless for the payer: you need USDC, not ETH.

The reference client is the demo agent in the Wuzzy repository. It is deliberately small and imports nothing from the server, so it is readable as an example of what an outsider can build against the public API alone:

git clone https://github.com/Memetic-Block/wuzzy && cd wuzzy && bun install

bun run demo wallet                       # creates a wallet outside the repo
bun run demo search "how do I deploy a contract on Base"

Under the hood it is @x402/fetch, which is the shortest path if you are writing your own.

The scoped @x402/* packages speak protocol version 2 by default, and so does this example. Version 2 names Base by its CAIP-2 id, eip155:8453, rather than as base. Clients still on version 1, such as the unscoped x402-fetch, are answered too, with nothing to configure on either side.

If you configured @x402/fetch for version 1 only, add the version 2 scheme. These docs used to show ExactEvmSchemeV1 on its own. That client now throws No client registered for x402 version: 2 before paying anything, because the scoped packages read the PAYMENT-REQUIRED header before the body and sign in the version the header names. Register ExactEvmScheme on eip155:8453 as below, alongside the version 1 scheme or instead of it, or use registerExactEvmScheme, which registers both.

spendControls is worth setting rather than leaving off. It is a ceiling on what one request may spend without asking again, and there is no default worth trusting: quote first, then set it deliberately.

import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm/exact/client';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`);

const paid = wrapFetchWithPaymentFromConfig(fetch, {
  schemes: [{ network: 'eip155:8453', client: new ExactEvmScheme(account) }],
  spendControls: { maxAmountPerPayment: '$0.10' },
});

const response = await paid('https://api.wuzzy.io/search', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ query: 'how do I deploy a contract on Base' }),
});

Reading a result

{
  "query": "how do I deploy a contract on Base",
  "index": "global",
  "offset": 0,
  "topK": 10,
  "total": 103,
  "exhaustive": false,
  "hasMore": true,
  "results": [
    {
      "url": "https://docs.base.org/get-started/deploy-smart-contracts",
      "title": "Deploy a smart contract",
      "snippet": "Deploying a contract to Base requires a funded wallet and a...",
      "score": 0.0323,
      "ranks": { "lexical": 3, "vector": 1 },
      "provenance": {
        "protocol": "wuzzy/crawl-experimental",
        "protocolVersion": 1,
        "contentHash": "92628793bca6441354ccf481673e5d4b597ee52fa849e66356044a3df96cf126",
        "fetchedAt": "2026-09-06T04:28:39.359Z",
        "attestationUid": null,
        "attestationUrl": null
      }
    }
  ]
}

provenance is the point. It says which procedure produced the hash, what the hash is, when the page was fetched, and where to check the attestation. Verify a result walks through confirming it yourself.

total is a floor, not a count, whenever exhaustive is false. Both retrieval arms take a fixed number of candidates and fusion reorders those, so the corpus can hold more matches than the window saw. Render 103+ and page with hasMore rather than comparing offset against total.

Paging

Pass offset. Every page of a query is served from the same fixed retrieval window, so pages cannot overlap or shift under a reader between requests.

{ "query": "...", "topK": 10, "offset": 10 }

Next