draconic21
Integration · TypeScript / JavaScript

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.
Not legal, tax, or investment advice. Payments are USDC on Base and are irreversible once settled — test with a small balance first. draconic21 never holds your funds or your private key; every dollar goes straight from your wallet to the seller's on-chain address via the x402 facilitator.

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.

RoutePriceWhat it does
POST /v1/sanctions_screen$0.006OFAC SDN name/wallet screening
POST /v1/sanctions_delta$0.008OFAC SDN list changes since a date
POST /v1/edgar_filings$0.02Recent SEC filings by ticker/CIK
POST /v1/edgar_company_facts$0.02Key XBRL facts by ticker/CIK
POST /v1/edgar_fulltext_search$0.02Full-text search of SEC filings since 2001
POST /v1/signal_latest$0.03Daily agent-economy signal brief
POST /v1/signal_delta$0.05What changed in Signal since a date

Buy the Daily Agent Signal once per edition (hash-pinned)

The flagship buy, in full: check the free /health hash, stop if you already hold that edition, pay $0.03 for POST /v1/signal_latest with expect_sha256 (a brief revised after your check returns 409 with payment_taken:false before any 402), then hash the raw response bytes and confirm they match. The same hash is anchored to Bitcoin at /signal/timestamps.json. Dry-run without a key pays nothing.

// Buy draconic21's Daily Agent Signal once per edition, hash-pinned, and
// prove which edition you paid for. Plain TypeScript, official x402 v2
// client packages (same as call-paid-route.ts next to this file).
//
//   1. FREE  GET /health            -> today's edition date + sha256
//   2. STOP  if that sha256 equals the one you already hold (LAST_SIGNAL_SHA256)
//   3. PAY   POST /v1/signal_latest {"expect_sha256": <hash>}   ($0.03 USDC, Base)
//            If the brief was revised after step 1 the server answers 409
//            with payment_taken:false BEFORE any 402, so you never pay for
//            a brief you did not check.
//   4. PROVE sha256(raw response bytes) === the hash from step 1; the same
//            hash is anchored at /signal/timestamps.json?sha256=<hex>.
//
// Prerequisites: npm install @x402/fetch @x402/evm @x402/core viem
// Run it:
//   DRY RUN (no key, pays nothing, shows the hash-pinned 402):
//     npx tsx buy-signal.ts
//   PAY FOR REAL ($0.03 USDC on Base, non-refundable):
//     BUYER_PRIVATE_KEY=0x... LAST_SIGNAL_SHA256=<hash you hold, optional> npx tsx buy-signal.ts
// Never commit the key; keep it in an env var only.

import { createHash } from "node:crypto";
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/signal_latest";

async function main(): Promise<void> {
  // 1. Free check (Render free tier can cold-start; first call may take ~30 s).
  const health = await (await fetch(ORIGIN + "/health")).json();
  const edition = health?.signal;
  if (edition?.status !== "available" || !/^[0-9a-f]{64}$/.test(edition?.sha256 ?? "")) {
    console.log("No Signal edition available right now; nothing to buy.");
    return;
  }
  console.log(`Edition ${edition.date} sha256 ${edition.sha256}`);

  // 2. Already hold it? Stop without signing.
  if (process.env.LAST_SIGNAL_SHA256 === edition.sha256) {
    console.log("You already hold this edition. Nothing new to buy.");
    return;
  }

  const init = {
    method: "POST",
    headers: { "content-type": "application/json", accept: "application/json" },
    body: JSON.stringify({ expect_sha256: edition.sha256 }),
  };

  const privateKey = process.env.BUYER_PRIVATE_KEY;
  if (!privateKey) {
    const res = await fetch(ORIGIN + ROUTE, init);
    if (res.status === 409) {
      console.log("409: brief revised since the free check; re-run to pin the new hash.");
      return;
    }
    if (res.status !== 402) throw new Error(`expected HTTP 402, got ${res.status}`);
    const body = await res.json().catch(() => undefined);
    const required = new x402HTTPClient(new x402Client()).getPaymentRequiredResponse((n) => res.headers.get(n), body);
    const accept = required.accepts.find((a) => a.network === "eip155:8453") ?? required.accepts[0];
    console.log("HTTP 402 Payment Required (hash-pinned). Set BUYER_PRIVATE_KEY to actually pay.");
    console.log(JSON.stringify({ network: accept.network, amount: accept.amount, asset: accept.asset, payTo: accept.payTo }, null, 2));
    return;
  }

  // 3. Pay: wrapFetchWithPayment handles 402 -> sign -> retry.
  const client = new x402Client();
  registerExactEvmScheme(client, { signer: privateKeyToAccount(privateKey as `0x${string}`) });
  const res = await wrapFetchWithPayment(fetch, client)(ORIGIN + ROUTE, init);
  if (res.status === 409) {
    console.log("409: brief revised since the free check; no payment taken. Re-run.");
    return;
  }
  if (!res.ok) throw new Error(`paid call failed: ${res.status} ${await res.text()}`);

  // 4. Prove: hash the raw bytes, not a re-serialized copy.
  const bytes = Buffer.from(await res.arrayBuffer());
  const got = createHash("sha256").update(bytes).digest("hex");
  if (got !== edition.sha256) throw new Error(`sha256 mismatch: got ${got}, expected ${edition.sha256}`);
  console.log(`Paid and verified: sha256 ${got} (store it as LAST_SIGNAL_SHA256).`);
  console.log(`Bitcoin anchor: ${ORIGIN}/signal/timestamps.json?sha256=${got}`);
  console.log(bytes.toString("utf8"));
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
# Dry run (no wallet, no payment):
npx tsx buy-signal.ts

# Pay for real ($0.03 USDC on Base), skipping editions you already hold:
BUYER_PRIVATE_KEY=0x... LAST_SIGNAL_SHA256=<hash you hold> npx tsx buy-signal.ts