Haveno API
The Haveno API lets programs interact with Haveno directly, using the same backend that powers Haveno Desktop. It provides access to wallets, offers, trades, and market data. With it you can build trading bots, gather market statistics, or integrate Haveno into another application.
This guide covers the setup and a few short programs to get started.
Just want to trade?
If you only want to buy and sell XMR, see Getting Started. This guide is for building on top of Haveno.
How it works
Programs interact with a Haveno daemon (havenod), the same non-custodial backend used by Haveno Desktop. The daemon holds the wallet and connects to the Haveno network over Tor.
The API is exposed as gRPC services, so any gRPC-compatible client can use it. This guide uses haveno-ts, a TypeScript library that wraps the API in typed calls. A small Envoy proxy bridges the client to the daemon.
Requirements
- Node.js
- A Haveno build
- The Envoy proxy, e.g.
brew install envoy
Start a daemon
From the haveno repository, start the daemon and its proxy in separate terminals. Replace <your-api-password> with a strong, randomly generated secret, for example from openssl rand -hex 32. On Windows, run haveno-daemon.bat instead of ./haveno-daemon.
# terminal 1: start the Haveno daemon
./haveno-daemon --baseCurrencyNetwork=XMR_MAINNET --useLocalhostForP2P=false --useDevPrivilegeKeys=false \
--nodePort=9999 --appName=Haveno --apiPassword=<your-api-password> --apiPort=1201 \
--useNativeXmrWallet=false --ignoreLocalXmrNode=false
The daemon joins the Haveno network and serves its API over gRPC. The proxy exposes that API to your program at http://localhost:8080, which is the address the examples below connect to.
Note
These commands join mainnet. To develop with test funds instead, run make haveno-daemon-stagenet and make envoy-stagenet, and connect with the stagenet target's password, apitest. Your program connects to http://localhost:8080 either way.
Secure your daemon
The API grants full control of the daemon and its wallet. Anyone who can reach the API port and provide the password can move your funds. The daemon listens only on 127.0.0.1 unless --apiBindAddress sets another interface.
Protect it as follows:
- Set a strong password with
--apiPassword. Never useapitest, the password built into the Makefile targets, with real funds. - Keep the API port private. If you set
--apiBindAddress, use a firewall so the port is reachable only from the machines your programs run on.
Install the library
In a project, install haveno-ts:
Or start from the Haveno sample app, a minimal working program using the library.
Connect to a daemon
Create a client and request the daemon's version to confirm the connection.
import { HavenoClient } from "haveno-ts";
// connect to a Haveno daemon
const haveno = new HavenoClient("http://localhost:8080", "<your-api-password>");
console.log("Connected to Haveno " + await haveno.getVersion());
await haveno.disconnect();
The second argument is the daemon's API password, set when the daemon starts. After five failed password attempts within a minute, the daemon refuses every API call, including ones with the correct password, for up to a minute.
Read the market
The following program reads live market data: the current price of XMR, and the open offers to buy or sell it.
import { HavenoClient, HavenoUtils, OfferDirection } from "haveno-ts";
const haveno = new HavenoClient("http://localhost:8080", "<your-api-password>");
// the current price of 1 XMR in USD
const price = await haveno.getPrice("USD");
console.log(`Market price: 1 XMR = $${price}`);
// the open offers to sell XMR for USD
const offers = await haveno.getOffers("USD", OfferDirection.SELL);
console.log(`\n${offers.length} offers to sell XMR:`);
for (const offer of offers) {
const amount = HavenoUtils.atomicUnitsToXmr(offer.getAmount());
console.log(` ${amount} XMR @ $${offer.getPrice()} (${offer.getPaymentMethodShortName()})`);
}
await haveno.disconnect();
A note on amounts
Haveno measures XMR in atomic units (1 XMR = 10¹² atomic units). Use HavenoUtils.atomicUnitsToXmr and HavenoUtils.xmrToAtomicUnits to convert between the two.
Gather statistics
The daemon exposes the network's public trade history, which can be used for research, dashboards, or price analysis. This example counts completed trades by currency.
import { HavenoClient } from "haveno-ts";
const haveno = new HavenoClient("http://localhost:8080", "<your-api-password>");
// count completed trades by currency
const stats = await haveno.getTradeStatistics();
const counts = new Map<string, number>();
for (const stat of stats) {
const code = stat.getCurrency();
counts.set(code, (counts.get(code) ?? 0) + 1);
}
console.log("Trades by currency:");
for (const [code, count] of counts) {
console.log(` ${code}: ${count}`);
}
await haveno.disconnect();
Listen for trades
Register a listener to receive updates as a trade progresses: a new trade, a payment sent, a payout confirmed.
import { HavenoClient, NotificationMessage } from "haveno-ts";
const haveno = new HavenoClient("http://localhost:8080", "<your-api-password>");
await haveno.addNotificationListener((notification: NotificationMessage) => {
if (notification.getType() === NotificationMessage.NotificationType.TRADE_UPDATE) {
const trade = notification.getTrade();
console.log(`Trade ${trade?.getShortId()} is now: ${trade?.getPhase()}`);
}
});
console.log("Listening for trade updates… press Ctrl+C to stop.");
A complete trade
This example runs a full trade between two traders, Alice and Bob, each connected to their own daemon. Alice posts an offer to sell XMR for BTC, Bob takes it, and the two complete payment.
import { HavenoClient, HavenoUtils, OfferDirection, TradeInfo } from "haveno-ts";
// two traders, each connected to their own daemon
const alice = new HavenoClient("http://localhost:8080", "<alice-api-password>");
const bob = new HavenoClient("http://localhost:8081", "<bob-api-password>");
// each creates a payment account to send and receive BTC
const aliceAccount = await alice.createCryptoPaymentAccount("Alice's BTC", "BTC", "<alice-btc-address>");
const bobAccount = await bob.createCryptoPaymentAccount("Bob's BTC", "BTC", "<bob-btc-address>");
// Alice posts an offer to sell 0.5 XMR for BTC
const offer = await alice.postOffer({
direction: OfferDirection.SELL,
amount: HavenoUtils.xmrToAtomicUnits(0.5),
assetCode: "BTC",
paymentAccountId: aliceAccount.getId(),
securityDepositPct: 0.15
});
console.log(`Alice posted offer ${offer.getId()}`);
// Bob takes the offer, which starts the trade
const trade = await bob.takeOffer(offer.getId(), bobAccount.getId());
console.log(`Bob took the offer, starting trade ${trade.getShortId()}`);
// the deposit transactions must confirm before payment can begin (about 20 minutes on mainnet)
await waitUntil(bob, trade.getTradeId(), (t) => t.getIsDepositsUnlocked());
// Bob (the buyer) sends BTC to Alice, then confirms payment sent
await bob.confirmPaymentSent(trade.getTradeId());
console.log("Bob sent payment");
// Alice (the seller) waits for the payment, then confirms it received
await waitUntil(alice, trade.getTradeId(), (t) => t.getIsPaymentSent());
await alice.confirmPaymentReceived(trade.getTradeId());
console.log("Alice received payment. Trade complete.");
await alice.disconnect();
await bob.disconnect();
// poll a trade until a condition is met
async function waitUntil(client: HavenoClient, tradeId: string, met: (trade: TradeInfo) => boolean) {
while (!met(await client.getTrade(tradeId))) {
await HavenoUtils.waitFor(10000); // check again in 10 seconds
}
}
Note
Both wallets must hold enough XMR to cover the trade amount, the security deposit, the trade fee, and mining fees
Remote access over Tor
To reach a daemon from another device without opening a port, add --apiHiddenService=true to the daemon command. The daemon publishes its API as an onion service on its own Tor connection and logs the address:
The address stays the same across restarts and is saved in xmr_mainnet/tor/hiddenservice/api/hostname in the data directory: ~/.local/share/Haveno on Linux, ~/Library/Application Support/Haveno on macOS, or %APPDATA%\Haveno on Windows.
The onion leads to the daemon's gRPC port, not the Envoy proxy, so connect with a gRPC client that routes through Tor. The daemon requires an --apiPassword of at least 8 characters and cannot be started with --useLocalhostForP2P=true or --hiddenServiceAddress.
If the account is protected by a password, the onion is published once the account is opened. To publish it before then, add --apiHiddenServiceBeforeLogin=true, which starts Tor without the account's saved bridge settings.
Warning
Anyone with the onion address and the API password controls the daemon and its wallet. Keep both private.
Further reading
- API reference: complete method documentation
- Sample app: a runnable starting point
- haveno-ts: the library source and tests
- Developer docs: extending the API itself
Questions?
For help, join the Matrix room.