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/x402Python 3.9+:
pip install larkin-x402Framework 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().
| Option | Type | Required | Notes |
|---|---|---|---|
apiKey | string | yes | Your pf_live_* key from larkin.sh/dashboard. |
minScore | number (0–100) | no | Threshold below which the chosen mode triggers. Reasonable production default: 40. See Score interpretation below. |
mode | "block" | "warn" | "surcharge" | no | Default "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 } | no | Required when mode is "surcharge". Multiplier applied to the x402 price for wallets below `below`. |
chainId | number | no | Default 1 (Ethereum). Also supported: 8453 (Base), 137 (Polygon). |
endpoint | string | no | Override the default https://larkin.sh API origin. Used for self-hosting or staging. |
fetchImpl | typeof fetch | no | Inject a custom fetch implementation. Used by tests; rarely needed in production. |
requireERC8004 | boolean | no | If 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, checkdata.decision.400—invalid_request/invalid_json.401—invalid_api_key.402—free_tier_exhausted, body includesupgradeUrl.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.
| Dimension | Range | What it measures |
|---|---|---|
walletAge | 0–20 | Earliest outgoing tx, linear scale to 730 days. New wallets score low; year-old wallets max out. |
txHistory | 0–20 | Activity volume + recency. Logarithmic in tx count; zero if dormant 180+ days. |
counterparties | 0–20 | Distinct outgoing recipients in the last 1000 transfers. Bonus for known-good addresses (Coinbase, Binance, major DEXs, etc.). |
fundingSource | 0–20 | First incoming transfer. Known exchange = 20, unknown EOA = 10, unknown contract = 5, mixer = 0. |
erc8004 | 0–20 | Reserved 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
| Status | error.code | Meaning |
|---|---|---|
| 400 | invalid_request | Body failed schema validation. Issues array included. |
| 400 | invalid_json | Body wasn't valid JSON. |
| 401 | invalid_api_key | Missing, malformed, or revoked X-API-Key. No detail leaked about which. |
| 402 | free_tier_exhausted | Free-tier monthly quota hit. Body includes upgradeUrl pointing at /dashboard/billing. Upgrade to unblock. |
| 502 | scoring_failed | All 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.
| kind | Behavior |
|---|---|
allow | Adapter 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. |
deny | Score below minScore in block mode. Adapter returns 403 payment_denied. Only fires in block mode. |
missing_proof | PAYMENT-SIGNATURE header absent or undecodable. Adapter returns 400. Usually a misconfigured caller, not a Larkin issue. |
service_unavailable | Larkin API unreachable (timeout, network error, 5xx). Block mode returns 503; warn mode runs the handler with X-Larkin-Error: service_unavailable. |
free_tier_exhausted | Your 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:00ZProgrammatic
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.
| Tier | Price | Included checks |
|---|---|---|
| Free | $0 | 10,000 / month |
| Pro | $29 / month | 500,000 / month |
| Scale | $199 / month | 5,000,000 / month |
Above 5M / month, email sales@larkin.sh.
Cap behavior
| Tier | Stated limit | Hard cap (2x) | Behavior beyond stated limit |
|---|---|---|---|
| Free | 10K / month | 10K / month | 402 free_tier_exhausted |
| Pro | 500K / month | 1M / month | Continues serving, no overage billing in v1 |
| Scale | 5M / month | 10M / month | Continues 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.