From nothing to a settled test receipt, no sales call in the middle.
You onboard the way your users will: the vendor account is a lazily-materialized side effect of integrating. Mount ~20 lines of config, keep your tool handler, and let a self-grading conformance check tell you you're done.
Status, up front: the handsel CLI and @handsel/* packages
are not yet on npm (every command below is marked accordingly). The stack is a working reference
implementation with Stripe in test mode; no external vendor is live yet. What you can run
today is the real zero-form e2e from a clone — see the sourcenot yet public.
1Register — claim a tenant
Scaffold the edge, mint a signing key, and register your resource origin with the core.
Registration pins your origin and your authoritative public key — the trust root receipt integrity
rests on. No forms, no dashboard.
npm create @handsel/vendor shipping — not yet on npmnpx handsel keygen shipping — not yet on npmnpx handsel register shipping — not yet on npm2Verify — the self-grading exam
"A phase is done iff its prove command is green" is the project's own law; it becomes your onboarding
contract. verify runs a vendor-facing conformance slice against your integration
and says, definitively, "you're done" — or exactly what's broken.
npx handsel verify shipping — not yet on npm3Claim — connect Stripe
"Connect with Stripe." Connect OAuth hands over verified business identity, entity, and payouts — the entire company-details form, deleted, because Stripe already collected it. Handsel is not merchant of record on the primary rail and holds $0.
npx handsel claim shipping — not yet on npmStripe stays in test mode until you say otherwise; no live payments move.
4Connect — go live on the ladder
Point your MCP surface at the mounted routes and you're on the ladder: guest calls get a price-0
co-signed receipt, and the same grant climbs to claimed, mandated, and
assured without re-onboarding.
npx handsel connect shipping — not yet on npm5Three integration depths, one gradient
SDK
Full @handsel/sdk — ~20 lines of config over your
existing endpoint. You keep your tool handler; the protocol does the rest.
Proxy
Zero code: CNAME a subdomain or add three lines of middleware and Handsel fronts your existing API. Day-one shallow entry.
Your agent
The onboarding surface is a prompt, not docs.
/handsel-integrate reads your OpenAPI, wires the SDK, and opens the PR.
Vendors climb from shallow to deep exactly like users climb guest → mandated.
6The vendor edge — hello world
The whole integration is config plus your own tools. Byte-identical to what
npm create @handsel/vendor --product word-count scaffolds — the one synced
hello-world across the SDK docs and this site (docs:sync).
/**
* Word Count — a Handsel MCP vendor over @handsel/mcp. One REAL deterministic tool,
* word_count (a pure function of its input — no dataset, no inference, no network on
* the answer path), sits behind the Handsel ladder: @handsel/sdk enforces guest/claimed
* quota, the 401/403 challenges, and receipts. Configure via env (see .env.example).
*/
import { serveMcpVendor } from '@handsel/mcp'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
const env = z
.object({
HANDSEL_CORE: z.string().url(),
VENDOR_RESOURCE: z.string().url(),
VENDOR_SIGNING_SEED: z
.string()
.regex(/^[0-9a-fA-F]{64}$/, 'expected a 64-hex Ed25519 seed from `handsel keygen`')
.optional(),
VENDOR_SECRET: z.string().optional(),
CORE_SECRET: z.string().optional(),
PORT: z.coerce.number().int().positive().optional(),
})
.parse(process.env)
await serveMcpVendor({
product: 'word-count',
name: 'Word Count',
resource: env.VENDOR_RESOURCE,
handselCore: env.HANDSEL_CORE,
...(env.VENDOR_SIGNING_SEED ? { signingSeed: env.VENDOR_SIGNING_SEED } : {}),
...(env.VENDOR_SECRET ? { vendorSecret: env.VENDOR_SECRET } : {}),
...(env.CORE_SECRET ? { coreSecret: env.CORE_SECRET } : {}),
// Pricing models — the receipt bills price_usd_micros * units (patterns: the
// @handsel/sdk README, "Pricing models"). Beyond per-call flat:
// metered — unit: 'token' + onCall: (body) => ({ units: yourCount(body) })
// success-gated — bill_on_miss: false + onCall: (body) => ({ miss: wasEmpty(body) })
// freemium — price_usd_micros: 0 (calls co-sign price-0 proof-of-use receipts)
pricing: { unit: 'call', price_usd_micros: 500 },
tiers: { guest: { calls_per_day: 5 }, claimed: { calls_per_day: 500 } },
terms: 'Word Count demo vendor. Calls are metered per the vendor-signed schedule.',
port: env.PORT ?? 8787,
// Return a fresh MCP server per request; @handsel/mcp owns the transport. Register
// your real tools here — word_count is pure + deterministic (Covenant 3).
buildServer: () => {
const server = new McpServer({ name: 'word-count', version: '0.1.0' })
server.registerTool(
'word_count',
{
title: 'Count words',
description:
'Count the whitespace-separated words in a string. Deterministic; no dataset, no inference.',
inputSchema: { text: z.string().describe('The text to count words in.') },
},
({ text }) => {
const trimmed = text.trim()
const count = trimmed === '' ? 0 : trimmed.split(/\s+/).length
return { content: [{ type: 'text' as const, text: String(count) }] }
},
)
return server
},
})
Zero Handsel-specific code. Your client's own OAuth machinery climbs.
No token means a 401 with the RFC 9728 pointer, and the MCP SDK's own OAuth flow runs discovery → registration → authorize → token. The client retries with the bearer and calls the real tool. The signup never happened.
const client = new Client({ name: 'my-agent', version: '0.0.0' })
const url = new URL('https://api.example.com/mcp')
const transport = () => new StreamableHTTPClientTransport(url, { authProvider })
// First connect: 401 → the SDK's OWN auth flow runs. No forms.
await client.connect(transport()).catch(() => {})
// Second connect: retry with the bearer, then call the REAL tool.
await client.connect(transport())
const result = await client.callTool({
name: 'define',
arguments: { word: 'serendipity' },
})
// → the real definition returns; the account is now a side effect of use.
Condensed from apps/mcp-e2e — a real, passing end-to-end test against the live
stack over real Postgres.
Building an agent that discovers and onboards vendors autonomously? Point it at
/llms.txt — the machine-readable what-Handsel-is, the discovery chain,
and the hello-world, written to be pasted into an agent.
Playground — paste your API's base URL and watch an unmodified agent climb it behind an ephemeral Handsel proxy. Preview the playground → (the hosted run is still landing; until then the page degrades honestly and shows the local-run recipe.)