Skip to main content
Every /v1 call (except minting a key) needs an API key:
X-Api-Key: hk_live_... works too. Keys issued by a testnet deployment start with hk_test_, mainnet keys with hk_live_.

Scopes

A key holding only read gets 403 (“this API key is read-only”) on any write except webhooks. Creating RFQs and listing tokens need taker specifically; quoting needs maker. Threads and swaps are readable only by their two parties. GET /v1/me shows the key’s scopes.

Key rules

  • Expiry: a key works for 90 days (expiresAt in the mint response). To renew, mint a new one.
  • Shown once: the plaintext key is returned once. The server stores only its hash and a display prefix.
  • Owner is told: every new key is announced to the account owner in Telegram, when Telegram is linked.
  • Wallet-only accounts hold 1 key. An account with only a wallet (an agent that minted by signature, or a wallet sign-in) holds one live key. A new signed mint replaces the previous key, which stops working. This is also how an agent recovers a lost or leaked key.
  • Person-backed accounts hold up to 10. An account signed up at /developers with email, Google or Telegram holds up to 10 live keys. Minting an eleventh is refused until one is revoked.
  • One rate budget per account: all keys of an account share it. More keys do not buy more rate. See Rate limits.

Mint a key in the browser

Sign in at hashlock.markets/developers and create a key. Revoke keys there too.

Mint a key with a wallet signature

No browser, no existing key. This is the path for agents.
  1. GET /v1/keys/nonce → { "nonce": "…" }. The nonce is single-use and lives 5 minutes.
  2. Sign a message with the wallet (rules below).
  3. POST /v1/keys with { rail, address, message, signature }, where rail is evm, tron, solana or bitcoin.
The key belongs to the account that wallet signs in to, created on first use. A wallet proven onto another account does not reach that account.

The message

The server checks: The name and scopes are read from the signed text only. What the wallet signed is exactly what is minted. Signing per rail is the same as for proving wallets: EVM personal_sign (EIP-191), TRON signMessageV2, Solana ed25519 signMessage (base58 signature), Bitcoin BIP-322.
replaced: true means a previous key of this account was revoked by this mint. Minting is limited per client IP: 20 mint requests per hour.

Proving wallets

An order that gives an asset needs a wallet the account can fund that leg from. A key starts with only the address its account signed in with. GET /v1/me lists, per chain, the login address and the proven ones. Over an API key, a wallet proved below lets the account give on EVM (set it as the refund address on the leg — it sends the fund). On TRON and Solana it does not: those legs are funded from the login wallet, or one authorised by it or linked in a signed-in session at hashlock.markets. On Bitcoin any wallet can pay the escrow, but its refund key must be one of those wallets’. An RFQ, quote or accept that gives on a chain the account cannot fund from is refused.
  1. GET /v1/wallets/nonce → a single-use link nonce.
  2. Sign a message that contains Hashlock Markets, the words link this wallet, the address, and Nonce: <nonce>:
    For Bitcoin the Address: line may be left out; the BIP-322 signature is verified against the address.
  3. POST /v1/wallets/{evm|tron|solana|bitcoin} with { address, message, signature }.
Responses: 200 proven (repeating a proof you hold is also 200); 400 bad signature, missing link this wallet, or a bad nonce; 409 the wallet belongs to another account. The link nonce is not a login nonce, and a key-mint nonce is not either. Each purpose has its own nonce pool, so a signature collected for one cannot be replayed for another. A link signature is not a session for that wallet’s account.

Authorising a wallet on another chain

A proof alone lets the account give from that wallet on EVM. To make a wallet count everywhere — you are paid at it, refunded at it, and a TRON leg funds from it — add an authorisation signed by the account’s login wallet:
  1. GET /v1/wallets/nonce → a link nonce, and GET /v1/wallets/nonce?purpose=authorize → an authorisation nonce.
  2. Sign the link message (above) with the new wallet, and this message with the login wallet:
  3. POST /v1/wallets/{family} with { address, message, signature, authorization: { address, message, signature } }, where authorization.address is the login wallet.
The answer carries authorised: true. An authorisation signed by any wallet other than the account’s login wallet is refused (400). Sign both within 5 minutes; each nonce is single-use. Only authorise wallets whose keys you alone hold: an authorised wallet can also sign in to the account. On TRON a leg is always funded from one wallet: the login TRON wallet, else the oldest one authorised by the login wallet or linked at hashlock.markets. DELETE /v1/wallets/{address} withdraws a proof. Login addresses are not proofs and cannot be removed this way. Putting a proof back needs a new signature from that wallet.

Payout addresses

A proof made with an API key alone widens what the account can trade. It does not count as a place to receive money unless it carries the login wallet’s authorisation. Over the API, POST /v1/swaps/{id}/address accepts an address that receives money — the payout address, and on Bitcoin and Solana the refund address too — only if it is:
  • a wallet the account signed in with, or
  • a wallet proven from a signed-in wallet session on the web, or
  • a wallet proven with the login wallet’s authorisation (above).
For Bitcoin, the pubkey of such an address. On EVM and TRON a refund returns to whoever funded, so the refund address only names the funder and is not restricted. This way a leaked key cannot point your money at the thief’s wallet. To be paid at a wallet that is not your login wallet, authorise it with the login wallet, or prove it from the web app while signed in.