| title | Quickstart |
|---|---|
| description | From an API key to a settled swap over the /v1 REST API. |
This walks one swap end to end over REST. The server builds unsigned transactions; you sign every transaction yourself. The server never signs anything that moves your funds.
All amounts are base-unit integer strings (satoshis, wei, token base units).
export HL=https://api.hashlock.marketsPick one path.
Sign in at [hashlock.markets/developers](https://hashlock.markets/developers) and create a key. It is shown once. No browser needed. Get a nonce, sign a message with the wallet, post it.```typescript
import { privateKeyToAccount } from 'viem/accounts';
const HL = 'https://api.hashlock.markets';
const account = privateKeyToAccount(process.env.EVM_KEY as `0x${string}`);
const { nonce } = await (await fetch(`${HL}/v1/keys/nonce`)).json();
const message = [
'Hashlock Markets — create an API key.',
'',
`Address: ${account.address}`,
'Key name: my-bot',
'Scopes: read, taker, maker',
`Nonce: ${nonce}`,
].join('\n');
const signature = await account.signMessage({ message }); // personal_sign / EIP-191
const res = await fetch(`${HL}/v1/keys`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ rail: 'evm', address: account.address, message, signature }),
});
const { key, expiresAt } = await res.json(); // key = "hk_live_…", shown once
```
See [API keys](/guides/api-keys) for the message rules, other chains and key limits.
Check the key:
curl -s $HL/v1/me -H "Authorization: Bearer $HK"The response lists your scopes and, per chain, your login wallet and the wallets you have proven.
To create an order that gives an asset, your account needs a proven wallet on that asset's chain. A key starts with only the address its account signed in with.
const { nonce } = await (await fetch(`${HL}/v1/wallets/nonce`, { headers: { Authorization: `Bearer ${HK}` } })).json();
const message = `Hashlock Markets — link this wallet.\n\nAddress: ${account.address}\nNonce: ${nonce}`;
const signature = await account.signMessage({ message });
await fetch(`${HL}/v1/wallets/evm`, {
method: 'POST',
headers: { Authorization: `Bearer ${HK}`, 'content-type': 'application/json' },
body: JSON.stringify({ address: account.address, message, signature }),
});TRON, Solana and Bitcoin use POST /v1/wallets/tron|solana|bitcoin. See API keys.
List assets to get their ids:
curl -s $HL/v1/assets -H "Authorization: Bearer $HK"Taker — create an RFQ (needs the taker scope):
curl -s -X POST $HL/v1/rfqs \
-H "Authorization: Bearer $HK" -H "content-type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"direction": "sell_base",
"baseAssetId": "<asset uuid>",
"baseAmount": "50000000",
"quoteAssetId": "<asset uuid>",
"ttlSeconds": 900,
"visibility": "public"
}'sell_base means you give the base asset. buy_base means you receive it. A private order needs an askAmount and is reachable by link only; it can name a targetAddress.
Maker — quote an RFQ (needs the maker scope). This opens a negotiation thread:
curl -s -X POST $HL/v1/rfqs/$RFQ_ID/quotes \
-H "Authorization: Bearer $HK" -H "content-type: application/json" \
-d '{ "quoteAmount": "1500000000" }'Makers can also stream RFQs and quote over the maker feed.
Taker — each quote opens a thread. List them to get the THREAD_ID:
curl -s $HL/v1/rfqs/$RFQ_ID/threads -H "Authorization: Bearer $HK"Or register a quote.created webhook and be told.
Read the thread, counter if you want, then accept at the price you read.
curl -s $HL/v1/threads/$THREAD_ID -H "Authorization: Bearer $HK"
# counter
curl -s -X POST $HL/v1/threads/$THREAD_ID/propose -H "Authorization: Bearer $HK" \
-H "content-type: application/json" -d '{ "quoteAmount": "1490000000" }'
# accept the other side's pending price (must equal pendingAmount)
curl -s -X POST $HL/v1/threads/$THREAD_ID/accept-proposal -H "Authorization: Bearer $HK" \
-H "content-type: application/json" -d '{ "quoteAmount": "1490000000" }'Accepting the terms commits and cannot be undone. The initiator — the party who funds the long leg, see Roles — must also send hashlock = sha256(secret):
import { randomBytes, createHash } from 'node:crypto';
const secret = randomBytes(32); // keep this private and safe
const hashlock = createHash('sha256').update(secret).digest('hex'); // 64 hex chars, no 0x
await fetch(`${HL}/v1/threads/${threadId}/accept`, {
method: 'POST',
headers: { Authorization: `Bearer ${HK}`, 'content-type': 'application/json' },
body: JSON.stringify({ quoteAmount: currentQuoteAmount, hashlock }),
});When both sides have accepted, the response includes the new swap.
For each chain of the swap, set your address. On the chain you receive on it is your payout address; on the chain you fund it is your refund address. For Bitcoin, send the compressed public key (hex); the server derives the P2WSH escrow once both sides are set.
curl -s -X POST $HL/v1/swaps/$SWAP_ID/address -H "Authorization: Bearer $HK" \
-H "content-type: application/json" \
-d '{ "chain": "ethereum", "address": "0xYourAddress" }'Pass "leg": "a" or "b" only when both legs are on the same chain. Over the API, an address that receives money must be your login wallet or one proven from a signed-in web session — see API keys.
The maker funds leg a; the taker funds leg b. The initiator funds first: the counterparty's fund builder refuses until the initiator's leg is funded.
curl -s -X POST $HL/v1/swaps/$SWAP_ID/legs/$MY_FUND_LEG/fund -H "Authorization: Bearer $HK"The response is chain-specific signing material. Sign it locally and broadcast. For an EVM leg:
import { createPublicClient, createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains'; // use the chain your swap's leg is on
const build = await (await fetch(`${HL}/v1/swaps/${swapId}/legs/${myFundLeg}/fund`, {
method: 'POST', headers: { Authorization: `Bearer ${HK}` },
})).json();
// { sign: 'evm-tx', chainId, from, txs: [{ to, data, value /* hex wei */ }, ...] }
// Sign every tx from `build.from` — the factory refuses any other sender.
const wallet = createWalletClient({ account, chain: mainnet, transport: http() });
const reader = createPublicClient({ chain: mainnet, transport: http() });
for (const tx of build.txs) {
const request = await wallet.prepareTransactionRequest({
account, to: tx.to, data: tx.data, value: BigInt(tx.value),
});
const signed = await wallet.signTransaction(request);
const { txid } = await (await fetch(`${HL}/v1/tx/broadcast`, {
method: 'POST',
headers: { Authorization: `Bearer ${HK}`, 'content-type': 'application/json' },
body: JSON.stringify({ chain: 'evm', signed }),
})).json();
await reader.waitForTransactionReceipt({ hash: txid }); // an ERC-20 approve must land before createSwap
}Once both legs are funded, the initiator claims the leg they receive on (the maker receives on leg b, the taker on leg a). That reveals the secret on chain.
curl -s -X POST $HL/v1/swaps/$SWAP_ID/legs/$MY_RECEIVE_LEG/claim -H "Authorization: Bearer $HK" \
-H "content-type: application/json" -d '{ "secret": "<32-byte hex preimage>" }'Sign and broadcast as in step 6. The counterparty then claims the other leg with the now-public secret. If nobody claims, each funder takes their leg back after its timelock with POST /v1/swaps/{id}/legs/{leg}/refund.
Track state with GET /v1/swaps/{id} or webhooks.