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:
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.
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:
The payer signs a mandate — an EIP-712 message: pay this much of this token to this recipient on this chain, meeting these requirements.
The payer’s own wallet broadcasts the on-chain transfer. HSP is zero-custody: no service ever holds your funds.
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:
| Object | Signed by | Says |
|---|---|---|
| Mandate | the payer | “I intend to pay X to Y, and I require capabilities Z” |
| Receipt | an adapter | “I observed the settlement that satisfies this mandate” |
| Attestation | an issuer | “I vouch the subject is KYC’d / not sanctioned / …” |
One pay() call drives five moves. Here is what actually happens on the wire — and which of
them a relying party can re-check:
The SDK assembles the EIP-712 MandateBody (payer, token, amount, recipient, chainId, deadline,
requiredCapabilities, nonce) and your wallet signs it. paymentId = keccak256(mandate).
POST /payments hands the signed mandate (plus any attestations) to the Coordinator, which
runs an admission check and stores it as PROPOSED.
Your own wallet broadcasts the ERC-20 transfer(recipient, amount). No service ever
touches the funds — this is what zero-custody means concretely.
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.
The verifier checks the receipt’s proof (and attestations) against the mandate and a pinned policy.
requiredCapabilities ⊆ satisfiedCapabilities ⇒ ACCEPT
⇒ 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.
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:
| # | Scenario | What it demonstrates | Requires |
|---|---|---|---|
| 1 | Public · evm-transfer | a plain ERC-20 stablecoin transfer, verified — the hello-world | ∅ |
| 2 | Public · x402 | paying via real Coinbase x402 v2 (HTTP-native, client-pull) | ∅ |
| 3 | Compliant · evm-transfer | a transfer that also carries KYC + sanctions attestations | attests:kyc + attests:sanctions |
| 4 | Compliant · x402 | KYC / sanctions enforced over the x402 path | attests: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.
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.
pay() + independent verify() — most developers start hereThe 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.
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:
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
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
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.
Open /explorer and paste the paymentId — see the
mandate, the receipt, and the subset chain that produced ACCEPT.
Typed requirements, written verb:object:version. proves:… — the settlement
structurally proves something. attests:… — an issuer vouches for something.
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.
Three backends, one wire signature. The signing account is also the settling account
(wallet-settling: Transfer.from must equal the mandate signer).
Equals the mandateHash. Use it to query status, deep-link the Explorer, and de-duplicate:
one on-chain transfer settles at most one payment.
Status is derived from the admitted receipts: PROPOSED → ATTEMPTED → SETTLED → DISPUTED — or terminal FAILED. A mandate past its deadline with nothing admitted reads EXPIRED.
A capability is verb:object:version; the verifier compares canonical sets byte-for-byte —
no fuzzy matching, no partial credit.
| Capability | Verb | Satisfied by |
|---|---|---|
| proves:settlement-verified:v1[via=…] | proves | a trust-minimized settlement proof (SPV / light-client / ZK) |
| attests:kyc:v1 | attests | a KYC attestation from a trusted issuer |
| attests:sanctions:v1 | attests | a 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).
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 }
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 });
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 });
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();
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' }));
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);
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.
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.
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"] }
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.)
| Tool | Does |
|---|---|
| hsp_verify | run the verifier over (mandate, receipt[, attestations]) — ACCEPT iff required ⊆ satisfied; pins the adapter, doesn’t trust a Coordinator |
| hsp_explain | the same decision narrated — ship?, outcomeClass → action, error-code meaning, required vs provided caps, trust boundary |
| hsp_inspect | decode a mandate / receipt / attestation into plain labelled fields (read-only) |
| hsp_capability | resolve verb:object:version → id + meaning, or list the baseline vocabulary |
| hsp_capability_diff | compare required vs satisfied capability sets → what’s missing |
| hsp_build_requirements | emit a §7.7 MandateRequirements (public | compliance) |
| hsp_check_requirements | pre-flight: does a mandate satisfy a given MandateRequirements? |
| hsp_build_mandate | construct an UNSIGNED MandateBody + its mandateHash (signing is external — a wallet MCP or @hsp/sdk) |
| hsp_prepare_payment | prepare 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_payment | relay 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 |
| Method | Path | Purpose |
|---|---|---|
| POST | /payments | register a signed mandate (+ attestations) |
| POST | /payments/:id/observe | ask the Coordinator to observe your settlement tx |
| POST | /payments/:id/receipts | submit an adapter-signed receipt (for custom adapters) |
| GET | /payments/:id | payment status + the stored triple |
| GET | /payments/:id/explain | label-resolved decision trace (what the Explorer shows) |
| GET | /payments | browse all payments — the detail list (Bearer key) |
| GET | /requirements?chain= | the deployment’s requirement advertisement |
| GET | /chains | chain registry + the adapter address to pin |
| GET | /stats | public aggregate dashboard (JSON) |
| GET | /docs, /explorer | this 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.
# 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" }
# 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" } }
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 }
| Class | Meaning | What to do |
|---|---|---|
| ACCEPT | settled and verified under your policy | ship |
| RETRYABLE | transient — not observable yet, stale state | retry |
| POLICY | a policy / requirement isn’t met | fix the mandate / attestations |
| PERMANENT | structurally invalid — contradicts the mandate | give up / debug |
| Frequent code | It means |
|---|---|
| HSP-MAND-EXPIRED | settled after the mandate deadline (an on-time settlement stays verifiable later) |
| HSP-RCPT-SIG | receipt not signed by a trusted adapter — check your pinned address |
| HSP-RCPT-PROOF | amount / recipient / token mismatch — exact-amount only; no fee-on-transfer or multi-log txs on the public path |
| HSP-RCPT-OBS-REUSED | this on-chain transfer already settled another mandate (one settlement, one payment) |
| HSP-MAND-REQ-INSUFFICIENT | mandate doesn’t require everything the deployment’s policy floor demands |
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.
Record the adapter address once and hardcode it. Never re-fetch it from the party you are trying not to trust.
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.)
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.
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 set | Value | What it is |
|---|---|---|
| HSP_COORDINATOR_URL | <COORDINATOR_URL> | the hub — register, observe, verify, Explorer |
| HSP_ISSUER_URL | <COORDINATOR_URL>/issuer | mock compliance issuer — testing only (kyc + sanctions) |
| HSP_FACILITATOR_URL | <COORDINATOR_URL>/facilitator | x402 v2 facilitator (for scenarios 2 & 4) |
| HSP_API_KEY | get one → /register | your team’s write key (Bearer) — self-service |
| HSP_CHAIN | hashkey-testnet | chain 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).
| Name | Chain ID | Stablecoin | Note |
|---|---|---|---|
| hashkey-testnet | 133 | USDC · 6 dec | faucet-friendly — start here · RPC testnet.hsk.xyz |
| anvil-dev | 31337 | per-run MockERC20 | local development |
| hashkey | 177 | USDC.e · 6 dec | mainnet — real money |
| ethereum | 1 | USDC · 6 dec | mainnet — real money |
Registry defaults shown; when this page is served by a live Coordinator the table reflects its actual configuration.