# Make your first paid API call You get a working paid API call in about two minutes. ## What you need - For x402, use a wallet with about 0.05 USDC on Base. - For MPP, use a wallet with about 0.05 USDC.e on Tempo. - Keep each private key in an environment variable. Do not put a key in a file that you commit. ## Lesson A: x402 on Base ### 1. Read the free discovery document Run this command: ```bash curl -fsS 'https://grant-search.krimskrams.xyz/.well-known/x402?src=learn' ``` Real output, shortened. Run time: 2026-08-20 22:50:32 UTC. ```json {"x402Version":2,"accepts":[{"scheme":"exact","network":"eip155:8453","amount":"20000"}]} ``` The free request returned HTTP 200. The `src=learn` parameter works. ### 2. Read the 402 challenge Run this command: ```bash curl -i -sS 'https://grant-search.krimskrams.xyz/v1/search?q=education&src=learn' ``` Real output, shortened. Run time: 2026-08-20 22:45:18 UTC. ```text HTTP/2 402 payment-required: eyJ4NDAyVmVyc2lvbiI6Mi... {"x402Version":2,"error":"Payment required","accepts":[{"scheme":"exact","network":"eip155:8453","amount":"20000","maxAmountRequired":"20000"}]} ``` Read `amount` as atomic USDC units. `20000` is 0.02 USDC because USDC has 6 decimal places. Make sure that `network` is `eip155:8453`. This value identifies Base mainnet for a version-2 client. This 402 body is hybrid. It keeps version-1 field names, but the route serves `network: "eip155:8453"`. A version-1 client needs `network: "base"`. One key has two incompatible values, so use a version-2 client here. Install trap: `x402-fetch@1.2.0` requests `x402@^1.2.1`, which is not on the registry. If your package includes that legacy client, add `"overrides": {"x402": "1.2.0"}`. ### 3. Make the payment call Save this `package.json`: ```json { "name": "learn-x402", "private": true, "type": "module", "dependencies": { "@x402/evm": "2.23.0", "@x402/fetch": "2.23.0", "viem": "2.55.19", "x402-fetch": "1.2.0" }, "overrides": {"x402": "1.2.0"} } ``` Save this as `pay.mjs`: ```javascript import { ExactEvmScheme } from "@x402/evm"; import { decodePaymentResponseHeader, wrapFetchWithPaymentFromConfig } from "@x402/fetch"; import { privateKeyToAccount } from "viem/accounts"; const url = "https://grant-search.krimskrams.xyz/v1/search?q=education&src=learn"; const key = process.env.X402_PRIVATE_KEY; if (!key) throw new Error("Set X402_PRIVATE_KEY."); const account = privateKeyToAccount(key.startsWith("0x") ? key : `0x${key}`); const select = (_version, accepts) => { const item = accepts.find((entry) => entry.scheme === "exact" && entry.network === "eip155:8453" && entry.asset.toLowerCase() === "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" && entry.payTo.toLowerCase() === "0x276efa09388fb1578a6c415b8f90d26fdfce0cf2" && entry.amount === "20000" ); if (!item) throw new Error("The route did not offer the approved $0.02 payment."); return item; }; const paidFetch = wrapFetchWithPaymentFromConfig(fetch, { schemes: [{network: "eip155:8453", client: new ExactEvmScheme(account)}], paymentRequirementsSelector: select, }); const response = await paidFetch(url); const data = await response.json(); const header = response.headers.get("payment-response"); const settlement = header ? decodePaymentResponseHeader(header) : null; console.log(JSON.stringify({ status: response.status, result_count: data.result_count, first_title: data.results?.[0]?.title, transaction: settlement?.transaction, }, null, 2)); ``` Set `X402_PRIVATE_KEY` to the private key for your funded Base wallet. Then run: ```bash npm install X402_PRIVATE_KEY="$X402_PRIVATE_KEY" node pay.mjs ``` ### 4. Read the answer Real output, shortened. Run time: 2026-08-20 22:46:49 UTC. ```json { "status": 200, "result_count": 10, "first_title": "BJA FY 2025/2026 Project Safe Neighborhoods Formula Grant Program", "transaction": "0x0c3c17f4aadfe8118e6a9752eb3c52c3d0ee1c6583923850f19165f84b556ea8" } ``` The paid request returned HTTP 200. The Base wallet balance decreased from 0.970000 USDC to 0.950000 USDC. The route accepts `q`, but it does not use it as a search filter. Use `query` when you need a keyword filter. ## Lesson B: MPP on Tempo ### 1. Read the free discovery document Run this command: ```bash curl -fsS 'https://grant-search.krimskrams.xyz/.well-known/mpp.json?src=learn' ``` Real output, shortened. Run time: 2026-08-20 22:50:32 UTC. ```json {"spec_version":"0.1","payment_methods":[{"method":"tempo","chain_id":4217}],"routes":[{"path":"/mpp/v1/search","price_usd_per_call":"0.02"}]} ``` The free request returned HTTP 200. The `src=learn` parameter works. ### 2. Read the 402 challenge Run this command: ```bash curl -i -sS 'https://grant-search.krimskrams.xyz/mpp/v1/search?q=education&src=learn' ``` Real output, shortened. Run time: 2026-08-20 22:45:18 UTC. ```text HTTP/2 402 www-authenticate: Payment id="...", realm="grant-search.krimskrams.xyz", method="tempo", intent="charge", request="..." ``` Decode the base64url `request` value. Make sure that `amount` is `20000` and `methodDetails.chainId` is `4217`. Make sure that `currency` is `0x20C000000000000000000000b9537d11c60E8b50`. This contract is USDC.e on Tempo. ### 3. Make the payment call The proven client is `pympp==0.10.1`. The payment code pins the amount, currency, receiver, and Tempo chain. Create an environment and install the tested client: ```bash python3 -m venv .venv .venv/bin/pip install 'pympp[tempo]==0.10.1' ``` Save this as `pay_mpp.py`. It copies the payment path from the tested source. ```python import asyncio import json import os from mpp import Challenge, Credential, Receipt from mpp.client import Client from mpp.methods.tempo import ChargeIntent, TempoAccount, tempo URL = "https://grant-search.krimskrams.xyz/mpp/v1/search?q=education&src=learn" CHAIN_ID = 4217 CURRENCY = "0x20C000000000000000000000b9537d11c60E8b50" RECEIVER = "0x01682fbff2a4dfac6a7a341f8f8714aa6fff9675" MAX_AMOUNT = 20_000 class CappedTempoMethod: name = "tempo" def __init__(self, delegate): self.delegate = delegate async def create_credential(self, challenge: Challenge) -> Credential: request = challenge.request details = request.get("methodDetails", {}) if int(request.get("amount", 0)) > MAX_AMOUNT: raise RuntimeError("The challenge amount is too large.") if request.get("currency", "").lower() != CURRENCY.lower(): raise RuntimeError("The challenge currency is not USDC.e.") if request.get("recipient", "").lower() != RECEIVER.lower(): raise RuntimeError("The challenge receiver is not approved.") if int(details.get("chainId", 0)) != CHAIN_ID: raise RuntimeError("The challenge chain is not Tempo.") return await self.delegate.create_credential(challenge) async def main(): key = os.environ.get("TEMPO_PRIVATE_KEY", "") if not key: raise RuntimeError("Set TEMPO_PRIVATE_KEY.") account = TempoAccount.from_key(key if key.startswith("0x") else f"0x{key}") delegate = tempo( intents={"charge": ChargeIntent()}, account=account, chain_id=CHAIN_ID, rpc_url="https://rpc.tempo.xyz", currency=CURRENCY, ) async with Client(methods=[CappedTempoMethod(delegate)]) as client: response = await client.get(URL, timeout=60) response.raise_for_status() data = response.json() receipt = Receipt.from_payment_receipt(response.headers["payment-receipt"]) print(json.dumps({ "status": response.status_code, "result_count": data["result_count"], "first_title": data["results"][0]["title"], "transaction": receipt.reference, }, indent=2)) asyncio.run(main()) ``` Set `TEMPO_PRIVATE_KEY` to the private key for your funded Tempo wallet. Then run: ```bash .venv/bin/python pay_mpp.py ``` ### 4. Read the answer No MPP payment ran for this page. The test payer has 0.009727 USDC.e, which is less than the 0.02 USDC.e price. This is the recorded real result from 2026-08-20 22:06 UTC: ```text MPP paid call: HTTP 200 with one opportunity MPP receipt header: present Tempo USDC.e balance: 0.029748 -> 0.009727 ``` The JSON answer has `result_count` and `results`. Each result has the title, deadline, award values, eligibility, and official Grants.gov link. ## It worked. Now what - OSHA Search: `GET /v1/search` or `GET /mpp/v1/search`, $0.02. - Agent API Listings: `POST /v1/packages` or `POST /mpp/v1/packages`, $0.20. - Buyer-Readiness Linter: `POST /v1/lint` or `POST /mpp/v1/lint`, $0.10. - Friction Logs: `GET /v1/log/{slug}` or `GET /mpp/v1/log/{slug}`, $1.00. - Payment Reconciliation: `GET /v1/report` or `GET /mpp/v1/report`, $0.10. - Listing Drift Watchdog: `GET /v1/drift` or `GET /mpp/v1/drift`, $0.10. Read every product route and price: https://krimskrams.xyz/llms.txt