Skip to content

About

Atomic settlement layer for the agent economy. Sealed-bid RFQ + HTLC settlement, fused into one operation. For AI agents (MCP-native) and institutional OTC desks.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@hashlock-tech/mcp

Hashlock Markets — the settlement layer for the agent economy, as MCP tools. Non-custodial cross-chain OTC: sealed RFQ + price negotiation + HTLC atomic settlement — both legs settle or both refund; no bridge, no custodian, no counterparty risk. BTC ↔ EVM / TRON / Solana.

Live on mainnet: Bitcoin, Ethereum, Base, Robinhood Chain, TRON, Solana. Use a dedicated key with only the funds you intend to trade.

📖 Documentation: docs.hashlock.markets — concepts, quickstart, guides and the API reference. For agents: llms.txt (index), llms-full.txt (everything in one file), any page as Markdown by adding .md, and a docs-search MCP server at https://docs.hashlock.markets/mcp.

npm License: MIT

What is this?

The canonical Model Context Protocol server for Hashlock Markets. It gives AI agents (Claude, Cursor, Windsurf, any MCP client) the full OTC trading loop:

  1. Browse the asset registry and the public RFQ board
  2. Post a public RFQ or a private fixed-price order (shareable link)
  3. Respond to requests with a price; negotiate (counter / accept / decline) in the deal thread
  4. Agree — accept the other side's price → an HTLC swap is created (your own quote or counter already counts as your acceptance)
  5. Track settlement — who funded, timelocks, tx hashes — and manage receive/refund addresses

Settlement signing (funding and claiming the HTLCs) stays with your own wallet — the server never holds keys or funds. The swap secret is generated locally on your machine and only its sha256 hashlock is sent; retrieve it with get_deal_secret when it's time to claim.

Two ways to run

  • Local (stdio) — the npm package below. You run it on your machine with your own keys; it can settle autonomously (SIWE login + on-chain signing with HASHLOCK_*_KEY). Full trust in yourself.
  • Remote (hosted, Streamable HTTP) — a public URL (https://hashlock.markets/mcp) anyone can add from Claude / ChatGPT / any MCP client; one-click OAuth, no install. Multi-tenant, so it is strictly non-custodial: settlement returns unsigned transactions you sign with your own wallet, and the server never holds keys or your swap preimage. See Remote (hosted) below.

Install

HASHLOCK_API_URL=https://hashlock.markets/api npx -y @hashlock-tech/mcp

Minimal config (read-only — browse assets and the RFQ board, no key needed):

{
  "mcpServers": {
    "hashlock": {
      "command": "npx",
      "args": ["-y", "@hashlock-tech/mcp"],
      "env": { "HASHLOCK_API_URL": "https://hashlock.markets/api" }
    }
  }
}

To trade, add the agent's own key(s) — local stdio via npx (Claude Desktop / Cursor / Windsurf mcpServers config):

{
  "mcpServers": {
    "hashlock": {
      "command": "npx",
      "args": ["-y", "@hashlock-tech/mcp"],
      "env": {
        "HASHLOCK_API_URL": "https://hashlock.markets/api",
        "HASHLOCK_EVM_KEY": "0x<agent EVM key — or HASHLOCK_BANKR_API_KEY for a Bankr wallet>",
        "HASHLOCK_TRON_KEY": "<agent TRON key, 64-hex (optional)>",
        "HASHLOCK_BTC_KEY": "<agent BTC WIF (optional)>",
        "HASHLOCK_SOLANA_KEY": "<agent Solana key, base58 (optional)>"
      }
    }
  }
}

Auth — autonomous, per chain

The agent owns its key(s); the server does the login itself (nonce → sign → JWT, refreshed on expiry). The first configured key (EVM → TRON → BTC → Solana) mints the session; each key also signs settlement on its chain.

Env var Chain Login
HASHLOCK_EVM_KEY EVM SIWE personal_sign
HASHLOCK_BANKR_API_KEY EVM, instead of HASHLOCK_EVM_KEY SIWE personal_sign, signed by Bankr
HASHLOCK_TRON_KEY TRON signMessageV2
HASHLOCK_BTC_KEY Bitcoin BIP-322
HASHLOCK_SOLANA_KEY Solana ed25519 signMessage
HASHLOCK_TOKEN — a ready JWT (alternative to a key)

HASHLOCK_SOLANA_KEY is base58 — the 64-byte export a wallet gives you, or a bare 32-byte seed. It mints the session when it is the only key set, and otherwise sits behind the others in the order above. Either way it proves ownership of the wallet (a signed, nonce-bearing message to /me/link-solana) the first time the agent posts, quotes, accepts or calls whoami, because an order whose give leg is Solana is refused without it — and because it is what puts the agent in the feed for a private order aimed at that wallet. If the account is already linked to a different Solana wallet, that link is left alone; nothing else is blocked, and whoami.solanaLink says so. Solana is also the one chain whose transactions this package does not build: the escrow's ADDRESS is a hash of the agreed terms, so the server composes each one and the agent signs the bytes it is handed. Set the agent's Solana address — whoami returns it as localSigners.addresses.solana — as its settlement address with set_settlement_address before funding.

A Bankr wallet as the EVM wallet. A Bankr wallet has no key to export, so set HASHLOCK_BANKR_API_KEY (a Bankr API key with the Wallet API enabled, read-only off, "Enable arbitrary contract calls" on, and no allowed recipients on the key) instead of HASHLOCK_EVM_KEY. The server still builds every transaction itself from the deal it recorded and checks on your RPC that the call would go through; it then hands Bankr that exact call to sign and send (POST /wallet/submit) and follows the transaction on your RPC. Bankr does not sign a raw transaction for someone else to broadcast, so what goes on chain is Bankr's to send, and the wallet's security settings and spending limits apply. As with a local key, once the server has started to fund a swap it does not fund it again, whatever stopped it — so set the wallet up before agreeing a deal; a claim or a refund that Bankr refuses can be repeated. When Bankr does not say what it did with a transaction, it may be out, and the server says so. A claim and a refund go through Bankr too: leave the wallet unpaused and those settings as they are until every swap is settled or refunded.

With none set, read-only tools (list_assets, list_open_rfqs, get_rfq) still work. Use a dedicated key with only the funds you intend to trade.

Required: HASHLOCK_API_URL — the API base, e.g. https://hashlock.markets/api; there is no default. Other env: HASHLOCK_APP_URL (share links; default derived), HASHLOCK_EVM_RPC (default a public Ethereum mainnet RPC; another EVM chain takes HASHLOCK_EVM_RPC_<CHAIN>, e.g. HASHLOCK_EVM_RPC_BASE), HASHLOCK_TRON_HOST (default TronGrid mainnet), HASHLOCK_TRON_API_KEY (a TronGrid API key — TronGrid throttles requests without one), HASHLOCK_SOLANA_RPC (default Solana mainnet-beta; devnet for a testnet API) and HASHLOCK_BTC_ESPLORA (default mempool.space for the API's Bitcoin network) — where the agent reads the counterparty's escrow itself, HASHLOCK_SECRETS_PATH (default ~/.hashlock/mcp-secrets.json, mode 0600).

Remote (hosted)

The same server also runs as a remote MCP over Streamable HTTP so anyone can connect by URL — no install. This is the multi-tenant, non-custodial surface: browse, RFQ, negotiate, and get unsigned fund/claim/refund transactions you sign with your own wallet (there is no autonomous key-in-env signing and no server-side secret storage here — you supply your own hashlock and keep your own preimage).

Connect from a client: add the server URL. Nothing else — the client discovers that it needs authorization, sends you to Hashlock to sign in and approve, and receives its own key:

URL: https://hashlock.markets/mcp

Approving takes a signature from your wallet — a signed-in session alone grants nothing. The grant then appears under Developers as an ordinary API key: it expires after 90 days, you are told in Telegram (when linked) whenever one is created, and it can be revoked there at any time. Clients that do not speak OAuth can still send a key they created themselves as Authorization: Bearer hk_….

Agents, with no browser: an agent that holds a wallet key mints its own hk_ key — GET https://hashlock.markets/api/v1/keys/nonce, sign Hashlock Markets — create an API key. plus Address: and Nonce: lines (optionally Key name: and Scopes:), and POST /api/v1/keys with { rail, address, message, signature }. The key belongs to the account that wallet signs in to, created on first use. A wallet-only account holds one key — signing again replaces it, which is also how a lost key is recovered; sign up at /developers with email, Google or Telegram to hold up to 10. See /api/v1/docs.

How the OAuth flow works

Standard OAuth 2.1, so any compliant MCP client drives it unattended:

Step Endpoint
Unauthorized call names its metadata 401 + WWW-Authenticate: … resource_metadata=… (RFC 9728)
Client reads the resource + server metadata /.well-known/oauth-protected-resource, /.well-known/oauth-authorization-server (RFC 8414)
Client registers itself POST /oauth/register (RFC 7591)
You sign in and approve with a wallet signature, in the browser /oauth/authorize
Client redeems the code for a key POST /oauth/token — PKCE S256 required (RFC 7636)

Codes are single-use and expire in 60 seconds; redirect URIs are allowlisted, with loopback permitted per RFC 8252. The issued token IS the API key, so a grant is revocable from the same list as every other key.

Which chains you can give on. An order that GIVES an asset needs a wallet you proved on that chain, and a key starts with only the address its owner signed in with — whoami lists both. To add another, call wallet_proof_message for the exact text, sign it with that wallet (EVM personal_sign · TRON signMessageV2 · Solana signMessage · Bitcoin BIP-322) and pass it to prove_wallet. A proof widens what you can trade and nothing else: it never becomes the account's payout address, which only a wallet session can set, and remove_wallet_proof takes it back.

Run the hosted service yourself:

docker build -t hashlock-mcp-http .
docker run -p 8080:8080 -e HASHLOCK_V1_URL=https://api.hashlock.markets/v1 hashlock-mcp-http
# or, from source:
pnpm build && HASHLOCK_V1_URL=https://api.hashlock.markets/v1 PORT=8080 pnpm start:http

Env: HASHLOCK_V1_URL (developer-API base, default https://api.hashlock.markets/v1) · PORT (default 8080). Put it behind your reverse proxy at /mcp; GET /health is a liveness probe.

Tools (18)

Tool What it does
list_assets Asset registry (SYMBOL@chain refs, decimals)
list_open_rfqs Public RFQ board, filterable
get_rfq One RFQ / private order
create_rfq Post a public RFQ or private fixed-price order
cancel_rfq Cancel your own request
respond_to_rfq Respond with a price → opens a deal thread
negotiate message / propose / accept_proposal / accept / reject — accepting names the price you read, and is refused if it moved; your own quote or counter counts as your acceptance, so you accept only the other side's price; accept_proposal takes their counter and agrees the deal in one step
my_rfqs, my_deals Your requests and deal threads
deal_status Thread + negotiation history + HTLC swap state
set_settlement_address Your receive/refund address per chain
get_deal_secret The locally-stored swap preimage (gated on both legs funded)
reveal_claim Report an out-of-band claim (secret + tx) so the other leg settles
whoami The account you're authenticated as
set_profile The account's username, and whether counterparties see it and the account's track record (anonymous until it goes public)
fund_leg Autonomous: fund your side of a swap on-chain with the agent's own key (EVM/TRON/BTC/Solana)
claim_leg Autonomous: claim your receive leg with the preimage (reveals the secret on-chain)
refund_leg Autonomous: take your funded leg back once its timelock has passed and nobody claimed it

Amounts are human decimal strings ("0.5"); prices are the total quote-asset amount, not per-unit. Errors return a structured envelope { error: { code, is_retryable, recovery_hint } } agents can branch on.

Fully autonomous loop

With a key set for each chain a swap touches, an agent can run end to end with no human: create_rfq/respond_to_rfq → negotiate (accept) → set_settlement_address (both chains) → fund_leg → claim_leg. Funding/claiming is signed locally with the agent's keys; the swap secret is generated + stored locally and only its hashlock leaves the machine. Use a dedicated key with only the funds you intend to trade.

The agent never signs, funds or reveals a secret on the API's word alone:

  • The deal as agreed. A request the agent creates (taker) or quotes (maker) through this MCP is recorded locally with its side, assets and base amount, and per deal thread every price the agent agreed to (mcp-secrets.agreements.json beside the secrets file); each swap secret is bound to its request. fund_leg, claim_leg and get_deal_secret settle only a swap that matches — a deal agreed through another client is settled with your own wallet — with the fee capped at 30 bps of the taker's leg and no leg locked longer than 27 hours; an escrow already funded for the swap is never paid again.
  • Pinned per network. The HTLC factories, TRON pools, Solana programs, the Bitcoin fee treasury and the assets it settles in (the curated stablecoins and natives) are pinned in src/pinned.ts per network, and the network is the one the agent's own RPC answers for (EVM chain id, TRON and Solana genesis, the Bitcoin key's own network). A contract or asset Hashlock adds reaches agents through a new release.
  • Escrows on chain. Before the counterparty funds, and before the initiator's secret leaves the store in any way (claim_leg, get_deal_secret, reveal_claim), the escrow the agent is to be paid from is read through its own RPC: same hashlock, the agreed asset and amount, payable to the agent's own key or to an address it set itself with set_settlement_address (recorded locally once the API took it — never an address the API reports or lists), still open, Bitcoin coin as deep as the network requires — and, before funding, a timelock at least 2 hours past its own leg's. One escrow can back only one deal.
  • What it signs. The initiator funds only under the hashlock of the secret it holds for the deal; Bitcoin escrows are derived locally (the agent's key on the refund branch). Every Solana transaction the server builds is decoded and signed only if it is one HTLC call of the asked kind binding the agent, the checked escrow, the agreed mint and — on a fund — the agent's leg exactly.
  • Not bound locally: the counterparty's payout address on your own leg (only the API knows it).
  • Deals agreed before this release, or settlement addresses set outside this MCP, have no local record: settle those in the web app.

How atomic settlement works

Both parties lock funds in HTLCs bound to the same sha256(secret) hashlock — BTC as a P2WSH script, EVM/TRON as contracts. The initiator funds the long-timelock leg first (asymmetric timelocks, so nobody gets a free option). Claiming one leg reveals the secret on-chain, which unlocks the other leg. Either both legs settle, or both refund after their timelocks. The recipient of each leg is fixed at funding time — revealing the secret cannot redirect funds.

Development

pnpm install
pnpm run build    # tsup → dist/
pnpm run lint     # tsc --noEmit
pnpm test         # vitest

Releasing means bumping two version numbers, and they are not the same one: package.json (and server.json's packages[0].version, which must match it) is the npm package, while server.json's own top-level version numbers the entry in the MCP Registry. Leave the second behind and npm publishes happily while the registry answers cannot publish duplicate version. Then tag: git tag v0.8.1 && git push origin v0.8.1 runs both workflows.

Node ≥ 20. MIT.

About

Atomic settlement layer for the agent economy. Sealed-bid RFQ + HTLC settlement, fused into one operation. For AI agents (MCP-native) and institutional OTC desks.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages