Larkin
Contents

Documentation

Larkin reference.

Everything you need to wire Larkin into an x402 endpoint and verify the signed receipts later. The SDK source is on GitHub — every claim on this page is a link away from the code that proves it.

Overview

x402 answers did they pay? Larkin answers should we let them? When an AI agent pays an x402-protected API, Larkin runs between the payment and your handler — checking the wallet’s age, history, counterparties, funding source, and on-chain reputation against rules you define. It returns an allow, deny, or surcharge decision plus an Ed25519-signed receipt of that decision.

The SDK is one line of middleware. The receipt is verifiable forever using only our published public key — even if our API disappears tomorrow, every receipt we’ve ever issued stays valid. That’s the whole product.

Quick start

Sign up at larkin.sh/signup for a free API key (10K checks / month, no card). Then five lines wraps any Next.js route handler:

import { preflight } from "@larkinsh/x402/next";

export const POST = preflight(handler, {
  apiKey: process.env.LARKIN_API_KEY!,
  minScore: 40,
  mode: "block",
});

That’s the production pattern. Hono and Express adapters follow the same shape (covered below).

Installation

The TypeScript package ships as ESM only — use import, not require(). Node 18+.

npm i @larkinsh/x402
pnpm add @larkinsh/x402
yarn add @larkinsh/x402

Python 3.9+:

pip install larkin-x402

Framework adapters

Subpath exports keep your bundle minimal — @larkinsh/x402/next, @larkinsh/x402/hono, and @larkinsh/x402/express. Each adapter exposes the same preflight() wrapper.

Next.js (App Router)

// app/api/paid/route.ts
import { preflight } from "@larkinsh/x402/next";

async function handler(req: Request): Promise<Response> {
  return Response.json({ data: "your paid response" });
}

export const POST = preflight(handler, {
  apiKey: process.env.LARKIN_API_KEY!,
  minScore: 40,
  mode: "block",
});

Hono

import { Hono } from "hono";
import { preflight } from "@larkinsh/x402/hono";

const app = new Hono();

app.post(
  "/paid",
  preflight(
    async (c) => c.json({ data: "your paid response" }),
    {
      apiKey: process.env.LARKIN_API_KEY!,
      minScore: 40,
      mode: "block",
    },
  ),
);

Express

import express from "express";
import { preflight } from "@larkinsh/x402/express";

const app = express();

app.post(
  "/paid",
  preflight(
    (_req, res) => res.json({ data: "your paid response" }),
    {
      apiKey: process.env.LARKIN_API_KEY!,
      minScore: 40,
      mode: "block",
    },
  ),
);

Configuration options

Pass these as the second argument to preflight().

OptionTypeRequiredNotes
apiKeystringyesYour pf_live_* key from larkin.sh/dashboard.
minScorenumber (0–100)noThreshold below which the chosen mode triggers. Reasonable production default: 40. See Score interpretation below.
mode"block" | "warn" | "surcharge"noDefault "block". block returns 403 on deny. warn always runs the handler and adds X-Larkin-* headers. surcharge signals the x402 layer to multiply price for sub-threshold wallets.
surcharge{ below: number, multiplier: number }noRequired when mode is "surcharge". Multiplier applied to the x402 price for wallets below `below`.
chainIdnumbernoDefault 1 (Ethereum). Also supported: 8453 (Base), 137 (Polygon).
endpointstringnoOverride the default https://larkin.sh API origin. Used for self-hosting or staging.
fetchImpltypeof fetchnoInject a custom fetch implementation. Used by tests; rarely needed in production.
requireERC8004booleannoIf true, deny wallets without an ERC-8004 registration regardless of score. See Score interpretation for the v1 caveat.

API reference: /v1/check

The SDK calls this endpoint internally. You can also call it directly if you’re building outside one of the supported frameworks.

Request

POST https://larkin.sh/v1/check with header X-API-Key: pf_live_… and a JSON body:

{
  "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "chainId": 1,                  // 1 (Ethereum), 8453 (Base), or 137 (Polygon)
  "minScore": 40,                // optional, 0–100
  "requireERC8004": false        // optional, see Score interpretation
}

Response (200 OK)

{
  "ok": true,
  "data": {
    "checkId": "chk_7F2A9E3B",
    "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "score": 72,
    "scoreVersion": "v1-hybrid",
    "breakdown": {
      "walletAge": 18,
      "txHistory": 15,
      "counterparties": 14,
      "fundingSource": 12,
      "erc8004": 13
    },
    "decision": "allow",          // "allow" | "deny" | "surcharge"
    "reasons": [],                // populated on deny
    "surchargeMultiplier": 1,
    "receipt": {
      "payload": { /* canonical-JSON payload */ },
      "sig": "base64url-Ed25519-signature",
      "kid": "larkin-v1"
    }
  },
  "meta": {
    "latencyMs": 127,
    "cached": true,               // cache hits cost 0 credits
    "creditsRemaining": 9997,     // null on cache hits or accounting hiccups
    "partial": false,             // true if any provider call timed out
    "moltrustBlended": true       // false if MolTrust was unavailable
  }
}

Status codes

See the next section for full body shapes and SDK mappings.

  • 200 — decision returned, check data.decision.
  • 400 invalid_request / invalid_json.
  • 401 invalid_api_key.
  • 402 free_tier_exhausted, body includes upgradeUrl.
  • 502 scoring_failed (upstream provider fully unavailable).

Score interpretation

Five dimensions, each 0–20, summing to a 0–100 trust score. The score returned in data.score blends our own scoring (70%) with MolTrust’s open agent reputation feed (30%). The per-dimension breakdown is always our own. When MolTrust’s feed is unavailable, meta.partial: true indicates the score is from our own scoring only — the request still succeeds.

DimensionRangeWhat it measures
walletAge0–20Earliest outgoing tx, linear scale to 730 days. New wallets score low; year-old wallets max out.
txHistory0–20Activity volume + recency. Logarithmic in tx count; zero if dormant 180+ days.
counterparties0–20Distinct outgoing recipients in the last 1000 transfers. Bonus for known-good addresses (Coinbase, Binance, major DEXs, etc.).
fundingSource0–20First incoming transfer. Known exchange = 20, unknown EOA = 10, unknown contract = 5, mixer = 0.
erc80040–20Reserved for the upcoming agent reputation registry. Currently returns 0 — when the registry deploys, scores will reflect on-chain reputation without code changes on your end.

Choosing minScore

40 is a reasonable starting point for most production endpoints — it rejects dormant wallets, brand-new wallets, and obvious scam funding patterns while letting normal active wallets through. 60+ for higher-stakes endpoints (large transfers, sensitive data) — that bar effectively requires age, activity, and a clean funding source. Below 40 you’re mostly excluding only mixer-funded wallets.

Watch data.breakdown in your logs for a few days before tuning — real traffic is the only honest calibration. Setting requireERC8004: true today denies every request because the registry isn’t live yet; reach for it once the registry deploys.

Error handling

Two layers — the HTTP response from /v1/check, and the SDK’s normalized outcome kinds. Most users only see the SDK layer; direct API users see HTTP.

HTTP error bodies

Statuserror.codeMeaning
400invalid_requestBody failed schema validation. Issues array included.
400invalid_jsonBody wasn't valid JSON.
401invalid_api_keyMissing, malformed, or revoked X-API-Key. No detail leaked about which.
402free_tier_exhaustedFree-tier monthly quota hit. Body includes upgradeUrl pointing at /dashboard/billing. Upgrade to unblock.
502scoring_failedAll upstream providers timed out. Transient — retry with backoff.

SDK outcome kinds

The SDK’s evaluate() returns one of these. Adapters translate them into HTTP responses; advanced consumers can dispatch on the kind directly.

kindBehavior
allowAdapter runs your handler. In block mode this means score ≥ minScore. In warn and surcharge modes the handler always runs — Larkin’s underlying decision is exposed via the X-Larkin-Decision response header (allow / deny / surcharge) for your logging.
denyScore below minScore in block mode. Adapter returns 403 payment_denied. Only fires in block mode.
missing_proofPAYMENT-SIGNATURE header absent or undecodable. Adapter returns 400. Usually a misconfigured caller, not a Larkin issue.
service_unavailableLarkin API unreachable (timeout, network error, 5xx). Block mode returns 503; warn mode runs the handler with X-Larkin-Error: service_unavailable.
free_tier_exhaustedYour Larkin account hit its monthly Free-tier quota. SDK logs a console.warn with the upgrade URL. Block mode returns 503 (the trust gate is effectively unavailable until you upgrade); warn mode runs the handler with X-Larkin-Error: free_tier_exhausted. Added in @larkinsh/x402 1.0.3.

Production patterns

Block mode is the right default for most paid endpoints — fail closed, a deny is a deny. Warn mode is for shadow-launching: ship Larkin into production, watch theX-Larkin-Decision header in your logs for a week, then flip to block once you’re confident in your minScore. Surcharge mode is for APIs that’d rather charge low-trust agents 10× than refuse them outright — common for content APIs.

Monitor the X-Larkin-Error response header. A spike of service_unavailable is our problem; a spike of free_tier_exhausted is yours (upgrade, or budget for an upgrade).

Note: 503s in your access logs come from the adapter, not from Larkin’s API. If you see Larkin returning 503, it’s because your account is over-cap (free_tier_exhausted) or the API was unreachable (service_unavailable) — both surface via the X-Larkin-Error response header.

Receipt verification

Every /v1/check response includes an Ed25519-signed receipt. Anyone with our public key can verify it forever — even if our API disappears tomorrow, every receipt we’ve ever issued stays valid. That’s the whole trust model.

CLI

npx @larkinsh/verify path/to/receipt.json
# ✓ Valid. wallet=0xd8dA… score=72 decision=allow issued=2026-04-20T18:00:00Z

Programmatic

import { verify, verifyWithFetch } from "@larkinsh/verify";

// Pure: caller supplies the public key (use the JWKS endpoint below).
const result = verify(receipt, publicKeyBase64Url);
// → { valid: true, payload: {...} }
//   { valid: false, reason: "..." }

// Convenience: fetches and caches the JWKS, then verifies.
const result2 = await verifyWithFetch(receipt);

Public keys

Served as JWKS-like JSON at larkin.sh/.well-known/larkin-keys.json. Cache aggressively — these rotate rarely (v1 has one key, kid: "larkin-v1"). Self-hosters can override the keys URL via the LARKIN_KEYS_URL env var on the CLI, or the keysUrl option on verifyWithFetch().

MCP server

@larkinsh/mcp is a Model Context Protocol server that exposes Larkin’s scoring as a single tool, check_wallet. Use it from Claude Desktop, Cursor, or any MCP-compatible client.

Claude Desktop

Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "larkin": {
      "command": "npx",
      "args": ["-y", "@larkinsh/mcp"],
      "env": {
        "LARKIN_API_KEY": "pf_live_..."
      }
    }
  }
}

Restart Claude Desktop. The check_wallet tool will be available in any conversation. Cursor and other MCP clients use the same config shape.

Pricing & limits

Three tiers. Cached reads are free across all tiers — the same wallet checked twice in five minutes counts once.

TierPriceIncluded checks
Free$010,000 / month
Pro$29 / month500,000 / month
Scale$199 / month5,000,000 / month

Above 5M / month, email sales@larkin.sh.

Cap behavior

TierStated limitHard cap (2x)Behavior beyond stated limit
Free10K / month10K / month402 free_tier_exhausted
Pro500K / month1M / monthContinues serving, no overage billing in v1
Scale5M / month10M / monthContinues serving, no overage billing in v1

Pro and Scale tiers include 2x overage headroom — your service won’t break if you exceed your stated monthly limit. Hitting the hard cap (2x) returns 402 tier_hard_cap_exceeded; contact sales@larkin.sh for higher volume.

Metered billing for sustained overage above the stated limit is on the roadmap for May 31, 2026. Until then, headroom usage is unbilled — but please email us if you’re sustaining 2x+ your tier so we can right-size your plan.

Manage your plan and billing at /dashboard/billing.


SDK source: github.com/larkin-dev/larkin. Issues: github.com/larkin-dev/larkin/issues.