Handsel. Vendor runbook
Reference implementation · Stripe test mode · No external vendor live yet
Quickstart · for API vendors

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 npm
$ npx handsel keygen shipping — not yet on npm
$ npx handsel register shipping — not yet on npm

2Verify — 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 npm

3Claim — 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 npm

Stripe 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 npm

5Three 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
  },
})
For agent buildersstock @modelcontextprotocol/sdk

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.)