HSP. Developer Guide
Draft Protocol · Pre-1.0

Verify the settlement,
not the promise.

HSP is a verifiable settlement layer for stablecoin payments. Instead of trusting a payment processor’s “paid” flag, every payment carries cryptographic evidence that anyone can re-check. A payer signs a Mandate; an adapter settles it and emits a signed Receipt; a verifier checks the pair (plus any compliance Attestations) against one rule:

ACCEPT requiredCapabilities satisfiedCapabilities (proof ∪ attestations) Everything the payer’s mandate required is satisfied by the receipt’s proof and the attestations. That single subset check is the whole trust model. ACCEPT means ship.
signed by the payer
Mandate
pay this much of this token to this recipient on this chain, requiring capabilities Z
signed by an adapter
Receipt
I observed the settlement that satisfies this mandate
signed by an issuer
Attestation
I vouch the subject is KYC’d / not sanctioned — optional, per policy
VERIFIER
pure function · runs anywhere
same inputs ⇒ same decision
ACCEPT

You do not need to run any infrastructure. The SDK is open source at github.com/project-hsp/hsp; the organizer hosts this shared sandbox, so you point the SDK at it and pay. The sandbox values marked <…> below (Coordinator URL, API key, faucet) are shared once it is live.

01

What is HSP

Three moves, three objects, one rule

Payment rails move money; almost none give the relying party a cryptographic answer to “did the thing I authorized actually happen, under the conditions I demanded?” HSP is that answer layer. The flow is always the same three moves:

1Intent

The payer signs a mandate — an EIP-712 message: pay this much of this token to this recipient on this chain, meeting these requirements.

2Settlement

The payer’s own wallet broadcasts the on-chain transfer. HSP is zero-custody: no service ever holds your funds.

3Verification

A verifier independently checks the receipt against the mandate and a pinned trust policy. ACCEPT means ship — and never requires trusting the Coordinator.

Three wire objects carry the evidence — and that is the entire protocol surface:

ObjectSigned bySays
Mandatethe payer“I intend to pay X to Y, and I require capabilities Z”
Receiptan adapter“I observed the settlement that satisfies this mandate”
Attestationan issuer“I vouch the subject is KYC’d / not sanctioned / …”

The end-to-end flow

One pay() call drives five moves. Here is what actually happens on the wire — and which of them a relying party can re-check:

  1. Build & sign the mandate.

    The SDK assembles the EIP-712 MandateBody (payer, token, amount, recipient, chainId, deadline, requiredCapabilities, nonce) and your wallet signs it. paymentId = keccak256(mandate).

  2. Register it.

    POST /payments hands the signed mandate (plus any attestations) to the Coordinator, which runs an admission check and stores it as PROPOSED.

  3. Settle on-chain.

    Your own wallet broadcasts the ERC-20 transfer(recipient, amount). No service ever touches the funds — this is what zero-custody means concretely.

  4. Observe.

    POST /payments/:id/observe with the txHash. The Coordinator waits for confirmations, finds the matching Transfer log, and the adapter key signs a Receipt attesting exactly what it saw.

  5. Verify → ship.

    The verifier checks the receipt’s proof (and attestations) against the mandate and a pinned policy. requiredCapabilities ⊆ satisfiedCapabilitiesACCEPT ⇒ status SETTLED. A merchant re-runs this step alone, offline, to decide whether to ship — no trust in the Coordinator required.

Compliance is not a bolt-on: KYC and sanctions are ordinary capabilities in those sets, toggled per payment. The protocol is a pre-1.0 draft — these concepts are stable; wire details may still change.

02

What you can build

Four scenarios, end-to-end today

Every scenario below is implemented, tested against real chains, and inspectable in the Explorer — the decision trace shows exactly which capability was required and what satisfied it. The matrix is two settlement paths × two policies:

#ScenarioWhat it demonstratesRequires
1Public · evm-transfera plain ERC-20 stablecoin transfer, verified — the hello-world
2Public · x402paying via real Coinbase x402 v2 (HTTP-native, client-pull)
3Compliant · evm-transfera transfer that also carries KYC + sanctions attestationsattests:kyc + attests:sanctions
4Compliant · x402KYC / sanctions enforced over the x402 pathattests:kyc + attests:sanctions

Build agentic commerce, paid APIs (x402 paywalls), compliant settlement flows — or your own settlement adapter (§07).

Coming soon — private payments. A later release will add and open up privacy-preserving settlement: hide the recipient (stealth addresses) or the amount (a shielded pool), each with optional view-key disclosure to a regulator. They slot in as adapters — the capability model already reserves room for them, so nothing else on this page changes when they ship.

03

The tool stack

Pick your layer

You consume HSP through a small layered stack — each layer is independently usable, and each lower layer is what the one above is built on. All of it runs from TypeScript source; no build step.

skills/hsp-verifyAI skill — an agent verifies & reasons about HSP payments (moves no money; to pay, use @hsp/sdk)
@hsp/mcpMCP server (pure / key-less) — agents verify / explain / build HSP objects
@hsp/sdkone-call pay() + independent verify()most developers start here
@hsp/devkitbuild & conformance-test your own settlement adapter
@hsp/coreprotocol primitives — types, capabilities, verifier, signer, adapters
Coordinatorthe hosted hub you point your SDK at (+ Explorer + this portal)

The Coordinator is the only service you talk to over the network. It registers mandates, observes your on-chain settlement, runs the verifier, stores the (mandate, receipt, attestations) triple, and serves status, a web Explorer, and this portal. It is custody-free — it signs observations with an adapter key, never moves money.

04

Quickstart — your first payment in five minutes

hashkey-testnet

The SDK is distributed as a repository for the hackathon (not yet on npm). The organizer shares the repo link together with the sandbox details. Then:

  1. Install.
    git clone https://github.com/project-hsp/hsp && cd hsp
    # everything runs in Docker (no node on your host) — see the repo README,
    # or use your own Node 20+ toolchain:
    npm install
  2. Get testnet funds.

    Open the faucet page (/faucet/) and paste your address — you receive gas + test USDC, rate-limited per address & IP. Or by API:

    curl -X POST -H 'content-type: application/json' \
      -d '{"address":"0xYOUR_ADDRESS"}' <FAUCET_URL>/faucet
  3. Send a payment.
    import { HSPClient } from '@hsp/sdk';
    import { resolveChain } from '@hsp/core/chains/index';
    
    const chain = resolveChain('hashkey-testnet');            // pinned testnet USDC
    const hsp = new HSPClient({
      coordinatorUrl: process.env.HSP_COORDINATOR_URL,       // <COORDINATOR_URL>
      apiKey:         process.env.HSP_API_KEY,               // <API_KEY>
      signer:         { kind: 'privateKey', privateKey: process.env.HSP_PRIVATE_KEY },
      chain,
    });
    
    // USDC has 6 decimals → 1 USDC = 1_000_000 base units
    const handle = await hsp.pay({ to: '0xRecipient', amount: 1_000_000n });
    await handle.awaitSettled();                            // → terminal status (usually "SETTLED")

    That single pay() call builds the mandate, signs it, registers it, broadcasts the ERC-20 transfer from your own wallet, asks the Coordinator to observe it, and returns a handle.

  4. Watch the decision.

    Open /explorer and paste the paymentId — see the mandate, the receipt, and the subset chain that produced ACCEPT.

05

Core concepts you’ll meet

Capabilities · verifier · signer

Capabilities

Typed requirements, written verb:object:version. proves:… — the settlement structurally proves something. attests:… — an issuer vouches for something.

public = (trivially satisfied) · compliant = attests:kyc + attests:sanctions

The verifier

A pure function of (mandate, receipt, attestations, policy). ACCEPT iff requiredCapabilities ⊆ satisfiedCapabilities. You can run it yourself — you never have to trust the Coordinator’s word.

same triple + same pinned policy ⇒ same decision, anywhere

Signer

Three backends, one wire signature. The signing account is also the settling account (wallet-settling: Transfer.from must equal the mandate signer).

privateKey (scripts) · viemAccount · eip1193 (browser/wallet — key never leaves it)

paymentId

Equals the mandateHash. Use it to query status, deep-link the Explorer, and de-duplicate: one on-chain transfer settles at most one payment.

idempotent: re-registering the same mandate returns the same payment

Payment lifecycle

Status is derived from the admitted receipts: PROPOSEDATTEMPTEDSETTLEDDISPUTED — or terminal FAILED. A mandate past its deadline with nothing admitted reads EXPIRED.

a rejected submission is recorded but never changes the status

Capability grammar

A capability is verb:object:version; the verifier compares canonical sets byte-for-byte — no fuzzy matching, no partial credit.

CapabilityVerbSatisfied by
proves:settlement-verified:v1[via=…]provesa trust-minimized settlement proof (SPV / light-client / ZK)
attests:kyc:v1attestsa KYC attestation from a trusted issuer
attests:sanctions:v1attestsa sanctions-screening attestation

A public payment requires the empty set (trivially satisfied). A compliant payment requires attests:kyc + attests:sanctions — the verifier ACCEPTs only if both are present and signed by an issuer the deployment’s policy trusts.

The sandbox’s adapters are operator-attested observations, so today they satisfy no proves:* capability — proves:settlement-verified is reserved in the vocabulary for a future trust-minimized adapter. The capabilities you can actually require right now are the empty set (public) and attests:kyc / attests:sanctions (compliant).

Signer backends

One wire signature, three sources for the key — and the signing account is always the settling account (Transfer.from must equal the mandate signer).

// 1) raw key — scripts / demos / small testnet amounts
signer: { kind: 'privateKey', privateKey: '0x…' }
// 2) any viem account — mnemonic, HD, hardware wallet
signer: { kind: 'viemAccount', account }
// 3) a real wallet (browser) — the key never leaves it
signer: { kind: 'eip1193', provider: window.ethereum, address }
06

Recipes

Compliant · x402 · verify

Compliant payment (KYC + sanctions)

The SDK fetches matching attestations from the issuer and registers them alongside your mandate; the verifier credits them.

const hsp = new HSPClient({ /* …as above… */ issuerUrl: process.env.HSP_ISSUER_URL });

await hsp.pay({
  to: '0xRecipient', amount: 1_000_000n,
  profile: { compliance: ['kyc', 'sanctions'] },  // signs the caps in
});

Pay via x402 (real Coinbase x402 v2)

You sign an HSP mandate and an EIP-3009 authorization; a stock facilitator settles on-chain (you pay no gas), then the Coordinator confirms the transfer and signs the verifiable HSP receipt.

await hsp.payX402({
  merchant:       '0xMerchant',
  facilitatorUrl: process.env.HSP_FACILITATOR_URL,
  amount:         1_000_000n,
  // profile: { compliance: ['kyc','sanctions'] }  // optional
});

to charge for an HTTP resource (a paywall), use x402Gate / fetchWithX402

Verify a payment you received — don’t trust the Coordinator

Pin the adapter’s observation address once (out-of-band, from /chains) and verify locally — the proof is yours to check.

import { HSPVerifier } from '@hsp/sdk';

const verifier = new HSPVerifier({ chain, adapterAddress: PINNED });
const d = await verifier.verify(mandate, receipt, attestations);
if (d.ok && d.outcomeClass === 'ACCEPT') ship();

re-fetching the pin from the party you’re trying not to trust defeats the point

Charge for an HTTP resource (x402 paywall)

Gate a route: an unpaid request gets 402 with the price; the client retries with an X-PAYMENT header, the facilitator settles, and your handler runs.

import { x402Gate } from '@hsp/sdk';

app.get('/premium', x402Gate({
  facilitatorUrl, merchant, amount: 10_000n,   // 0.01 USDC
}), (req, res) => res.json({ data: 'paid content' }));

client side: fetchWithX402(url) pays & retries for you

Track a payment to completion

awaitSettled() polls until a terminal state — then branch on the outcome.

const h = await hsp.pay({ to, amount });
const s = await h.awaitSettled({ timeoutMs: 120_000 });
if (s.status === 'SETTLED') ship();
else console.error(s.status, s.lastDecision?.errorCode);

prefer to poll yourself? GET /payments/:id

07

Build your own settlement adapter

@hsp/devkit

Want to settle a different way — a new rail, a new proof? @hsp/devkit gives you a template plus a conformance runner that checks your adapter against the protocol’s generic obligations using the real verifier. The verifier never changes; only your proof schema does.

# 1. start from a template that already passes conformance
cp packages/devkit/template/my-adapter.ts        my-team/adapter.ts
cp packages/devkit/template/run-conformance.ts   my-team/run-conformance.ts
# 2. make it settle YOUR way, then self-test:
npx tsx my-team/run-conformance.ts
# → forged signatures, replays, deadline, observation reuse… all exercised

Then submit (adapterId, instanceKey, signing address, reorgPolicy) to the organizer to be registered in the sandbox Coordinator’s trust set — payers can settle through you. No protocol changes, no permission from the spec.

08

AI agents

Reason over HSP, key-less

The MCP server is pure and key-less — it holds no private key and signs nothing. It gives an agent tools to verify, explain, and build HSP objects, and now to pay — but key-lessly: it prepares the unsigned payment, a wallet MCP (or the user’s wallet) signs it, and it submits the signed result. @hsp/sdk (HSPClient.pay / payX402) is still an option for paying from code.

@hsp/mcp

A pure, key-less MCP server: ten tools that construct, verify, explain, and now pay — key-lessly. It signs nothing and holds no signing key. Paying is prepare → a wallet MCP signs → submit: hsp_prepare_payment returns the unsigned mandate + settlement in standard wallet-RPC shapes, the agent routes them to a wallet MCP (Phantom, Coinbase, MetaMask, …) to sign, and hsp_submit_payment relays the signed result to the Coordinator. HSP_CHAIN is the only var needed to reason; to pay, add a Coordinator URL + write key (not a signing key).

// .mcp.json — hsp (key-less) + a wallet MCP as the signer
"hsp": { "command": "npx", "args": ["tsx", "packages/mcp/src/index.ts"],
  "env": { "HSP_CHAIN": "hashkey-testnet",
    // URL + WRITE key for prepare/submit — NOT a signing key:
    "HSP_COORDINATOR_URL": "<COORDINATOR_URL>", "HSP_API_KEY": "<your-team-key>",
    // optional — only widen what hsp_verify can check:
    "HSP_PINNED_ADAPTER_ADDRESS": "0x…" } },
// the EXTERNAL SIGNER — agent routes prepare's toSign[] here, then submits:
"phantom": { "command": "npx", "args": ["-y", "@phantom/mcp-server@latest"] }

skills/hsp-verify

An AI skill that teaches an agent to verify & reason about HSP payments — verify / explain a received payment, inspect/decode wire objects, resolve capabilities, and check requirements. It moves no money and holds no key. Drop it into your agent’s skills directory. (To pay, the hsp MCP prepares the unsigned payment and a wallet MCP signs it — key-less; @hsp/sdk is still an option for paying from code.)

does: verify / explain / inspect / capabilities / requirements — no money moved

ToolDoes
hsp_verifyrun the verifier over (mandate, receipt[, attestations]) — ACCEPT iff required ⊆ satisfied; pins the adapter, doesn’t trust a Coordinator
hsp_explainthe same decision narrated — ship?, outcomeClass → action, error-code meaning, required vs provided caps, trust boundary
hsp_inspectdecode a mandate / receipt / attestation into plain labelled fields (read-only)
hsp_capabilityresolve verb:object:version → id + meaning, or list the baseline vocabulary
hsp_capability_diffcompare required vs satisfied capability sets → what’s missing
hsp_build_requirementsemit a §7.7 MandateRequirements (public | compliance)
hsp_check_requirementspre-flight: does a mandate satisfy a given MandateRequirements?
hsp_build_mandateconstruct an UNSIGNED MandateBody + its mandateHash (signing is external — a wallet MCP or @hsp/sdk)
hsp_prepare_paymentprepare a payment → register the mandate and return the UNSIGNED toSign[] in standard wallet-RPC shapes (eth_signTypedData_v4 mandate + eth_sendTransaction / EIP-3009 settlement). Signs nothing — route to a wallet MCP / wallet
hsp_submit_paymentrelay the externally-signed mandate + settlement to the Coordinator — re-verifies the signature (a tampered body is rejected), observes, returns SETTLED. Holds no key, only a write key
09

Coordinator endpoints

Write endpoints need a Bearer key
MethodPathPurpose
POST/paymentsregister a signed mandate (+ attestations)
POST/payments/:id/observeask the Coordinator to observe your settlement tx
POST/payments/:id/receiptssubmit an adapter-signed receipt (for custom adapters)
GET/payments/:idpayment status + the stored triple
GET/payments/:id/explainlabel-resolved decision trace (what the Explorer shows)
GET/paymentsbrowse all payments — the detail list (Bearer key)
GET/requirements?chain=the deployment’s requirement advertisement
GET/chainschain registry + the adapter address to pin
GET/statspublic aggregate dashboard (JSON)
GET/docs, /explorerthis portal + the decision-trace UI

Write endpoints need Authorization: Bearer <API_KEY>. paymentId = mandateHash (idempotent); a 202 from observe means the tx is still confirming — the SDK retries for you.

register a signed mandate → get a paymentId
# POST /payments      Authorization: Bearer <API_KEY>
{ "chain": "hashkey-testnet",
  "mandate": { "body": { /* 8 EIP-712 fields */ }, "signature": "0x…" },
  "attestations": [ /* optional — for compliant payments */ ] }
# → 201  { "paymentId": "0x…", "status": "PROPOSED" }
report your settlement tx, then read the verified status
# POST /payments/0x…/observe   Authorization: Bearer <API_KEY>
{ "txHash": "0x…" }
# → 200 observed & verified   |   202 still confirming (retry)

# GET /payments/0x…    (public — no key needed)
{ "status": "SETTLED",
  "mandate": { /* … */ }, "receipts": [ { /* adapter-signed */ } ],
  "lastDecision": { "ok": true, "outcomeClass": "ACCEPT" } }
10

Outcomes & error handling

Branch on outcomeClass

Every verifier decision carries an outcomeClass — the HTTP-status-code of settlement. Branch on the class; log the errorCode.

const d = await verifier.verify(mandate, receipt, attestations);
switch (d.outcomeClass) {
  case 'ACCEPT':    ship(); break;
  case 'RETRYABLE': await retryLater(); break;
  case 'POLICY':    fix(d.errorCode); break;      // mandate / attestations
  case 'PERMANENT': giveUp(d.errorCode);              // structurally invalid
}
ClassMeaningWhat to do
ACCEPTsettled and verified under your policyship
RETRYABLEtransient — not observable yet, stale stateretry
POLICYa policy / requirement isn’t metfix the mandate / attestations
PERMANENTstructurally invalid — contradicts the mandategive up / debug
Frequent codeIt means
HSP-MAND-EXPIREDsettled after the mandate deadline (an on-time settlement stays verifiable later)
HSP-RCPT-SIGreceipt not signed by a trusted adapter — check your pinned address
HSP-RCPT-PROOFamount / recipient / token mismatch — exact-amount only; no fee-on-transfer or multi-log txs on the public path
HSP-RCPT-OBS-REUSEDthis on-chain transfer already settled another mandate (one settlement, one payment)
HSP-MAND-REQ-INSUFFICIENTmandate doesn’t require everything the deployment’s policy floor demands
11

Trust model & safety

One paragraph, three rules

HSP is zero-custody: your wallet settles; the Coordinator only signs observations with an adapter key — it cannot move funds. A relying party (a merchant, an auditor, a platform) pins the adapter’s address once (from GET /chains, out-of-band) and runs the verifier itself. If the Coordinator lied, your independent verification fails — that is the whole design.

Pin, don’t fetch-and-trust

Record the adapter address once and hardcode it. Never re-fetch it from the party you are trying not to trust.

Demo keys are demo keys

Example/script signing keys are for small testnet amounts. Anything that matters belongs in a real wallet — use the eip1193 path; the key stays in the wallet. (The MCP holds no key — paying is done by @hsp/sdk.)

Limit the blast radius

The faucet rate-limits per address & IP; team API keys are per-team — don’t share. Keep any payer key you hand a script or agent funded only for small testnet amounts.

12

This sandbox

What the organizer gives you

The hackathon sandbox runs the full stack on HashKey Chain testnet so you run nothing yourself. One HTTPS host fronts every service; point your SDK at these base URLs:

You setValueWhat it is
HSP_COORDINATOR_URL<COORDINATOR_URL>the hub — register, observe, verify, Explorer
HSP_ISSUER_URL<COORDINATOR_URL>/issuermock compliance issuer — testing only (kyc + sanctions)
HSP_FACILITATOR_URL<COORDINATOR_URL>/facilitatorx402 v2 facilitator (for scenarios 2 & 4)
HSP_API_KEYget one → /registeryour team’s write key (Bearer) — self-service
HSP_CHAINhashkey-testnetchain name + the pinned stablecoin

Faucet (testnet gas + USDC): /faucet/ — rate-limited per address & IP. The adapter address to pin for independent verification is published at /chains.

No external accounts to register. Grab your own API key at /register (self-service), and generate your own testnet wallet (a private key) funded from the faucet. No Circle, Coinbase, KYC-provider, RPC, or npm sign-up — the mock issuer and the x402 facilitator are part of this sandbox. The Explorer is public: looking up a payment by its id needs no key (an API key is only for browsing the full payment list).

Chains

NameChain IDStablecoinNote
hashkey-testnet133USDC · 6 decfaucet-friendly — start here · RPC testnet.hsk.xyz
anvil-dev31337per-run MockERC20local development
hashkey177USDC.e · 6 decmainnet — real money
ethereum1USDC · 6 decmainnet — real money

Registry defaults shown; when this page is served by a live Coordinator the table reflects its actual configuration.