Call draconic21's x402 API from plain TypeScript
Pay-per-call access to SEC EDGAR, OFAC sanctions, and Treasury data from any TypeScript or Node.js agent, using the official x402 v2 client packages — no account, no API key.
What this gets you
A plain fetch-based client that: (1) makes your request, (2) reads the HTTP 402 payment challenge draconic21 returns, (3) signs a USDC-on-Base payment with your wallet, and (4) retries the request with the payment attached — automatically, on every call. This uses @x402/fetch's wrapFetchWithPayment, which does exactly that (its own published docs describe this four-step flow).
Prerequisites
- A wallet on the Base network (chain id 8453) funded with a little USDC — a few dollars covers hundreds of calls at these prices.
- An x402 v2 client library (installed per snippet below) that can sign the "exact" EVM payment scheme (EIP-3009, gasless — no ETH for gas needed on your side).
- Your wallet's private key available to your own process/agent — never share it, never commit it, never paste it into a chat with an AI you don't fully trust with funds.
Install
npm install @x402/fetch @x402/evm @x402/core viem
These are the official x402 v2 scoped packages (not the older, unscoped x402-fetch/@coinbase/x402@0.x line some older tutorials show) — the same major version line draconic21's own server and its own buyer test script depend on.
The code
Run with tsx or compile with tsc. Dry-run (no key set) prints the payment challenge and pays nothing; set BUYER_PRIVATE_KEY to actually pay $0.006 for one sanctions_screen call.
// Plain TypeScript x402 client for draconic21's paid routes.
//
// Uses the OFFICIAL x402 v2 client packages (same ones this repo's own
// server and .run/buy_once.mjs buyer script depend on):
// @x402/fetch -- wrapFetchWithPayment(): wraps fetch, auto-handles the
// 402 -> sign -> retry dance
// @x402/evm -- registerExactEvmScheme(): registers the "exact" EVM
// payment scheme (EIP-3009, gasless) for Base (eip155:8453)
// @x402/core -- x402Client: the payment-scheme registry
// viem -- turns a raw private key into a signer
//
// Prerequisites:
// - A Base-network wallet funded with a little USDC (this route costs
// $0.006; fund with $1-2 to have headroom for a few calls).
// - npm install @x402/fetch @x402/evm @x402/core viem
// - Your wallet's private key in an env var (never commit it, never share
// it, never let an AI agent read the real value from disk on your
// behalf if you don't fully trust it).
//
// Run it:
// DRY RUN (no key, safe, shows the payment challenge, pays nothing):
// npx tsx call-paid-route.ts
// PAY FOR REAL ($0.006 USDC on Base, non-refundable):
// BUYER_PRIVATE_KEY=0x... npx tsx call-paid-route.ts
//
// Endpoint used below: POST /v1/sanctions_screen ($0.006) -- swap ORIGIN and
// the route/body for any other draconic21 route (see /openapi.json).
import { x402Client, x402HTTPClient } from "@x402/core/client";
import { wrapFetchWithPayment } from "@x402/fetch";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";
const ORIGIN = process.env.DRACONIC21_ORIGIN ?? "https://draconic21-x402-api.onrender.com";
const ROUTE = "/v1/sanctions_screen";
const BODY = { name: "Banco Nacional de Cuba" };
async function main(): Promise<void> {
const privateKey = process.env.BUYER_PRIVATE_KEY;
if (!privateKey) {
// No wallet configured: probe the route, print the exact payment
// requirements it returns, and stop. This never sends a payment.
const res = await fetch(ORIGIN + ROUTE, {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json" },
body: JSON.stringify(BODY),
});
if (res.status !== 402) {
throw new Error(`expected HTTP 402 Payment Required, got ${res.status}`);
}
// The x402 v2 payment challenge is carried in the PAYMENT-REQUIRED
// header (+ a JSON body), not just the body -- x402HTTPClient decodes
// both together into a structured PaymentRequired object.
const body = await res.json().catch(() => undefined);
const probe = new x402HTTPClient(new x402Client());
const required = probe.getPaymentRequiredResponse((name) => res.headers.get(name), body);
const accept = required.accepts.find((a) => a.network === "eip155:8453") ?? required.accepts[0];
console.log("HTTP 402 Payment Required. Set BUYER_PRIVATE_KEY to actually pay.");
console.log(JSON.stringify({ network: accept.network, price: accept.amount ?? accept.maxAmountRequired, asset: accept.asset, payTo: accept.payTo }, null, 2));
return;
}
// Wallet configured: build an x402 client that can sign the "exact" EVM
// scheme, wrap fetch with it, and let it handle 402 -> sign -> retry.
const account = privateKeyToAccount(privateKey as `0x${string}`);
const client = new x402Client();
registerExactEvmScheme(client, { signer: account });
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const res = await fetchWithPayment(ORIGIN + ROUTE, {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json" },
body: JSON.stringify(BODY),
});
if (!res.ok) {
throw new Error(`paid call failed: ${res.status} ${await res.text()}`);
}
console.log(JSON.stringify(await res.json(), null, 2));
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
# Dry run (no wallet, no payment):
npx tsx call-paid-route.ts
# Pay for real ($0.006 USDC on Base):
BUYER_PRIVATE_KEY=0x... npx tsx call-paid-route.ts
Routes you can call this way
Every example below targets one specific route to keep the snippet runnable, but the same client works against any draconic21 x402 route. Full list and schemas: /openapi.json.
| Route | Price | What it does |
|---|---|---|
POST /v1/sanctions_screen | $0.006 | OFAC SDN name/wallet screening |
POST /v1/sanctions_delta | $0.008 | OFAC SDN list changes since a date |
POST /v1/edgar_filings | $0.02 | Recent SEC filings by ticker/CIK |
POST /v1/edgar_company_facts | $0.02 | Key XBRL facts by ticker/CIK |
POST /v1/edgar_fulltext_search | $0.02 | Full-text search of SEC filings since 2001 |
POST /v1/signal_latest | $0.03 | Daily agent-economy signal brief |
POST /v1/signal_delta | $0.05 | What changed in Signal since a date |