Menu

Run a market maker

A core node that converts between XRP and dollars slowly runs out of one of them. A market maker brings it back: it swaps with the node when the node's quotes beat a pool on the XRP Ledger, and swaps the difference back on the pool. This page sets one up, with one node of your own.

On this page
  1. How it works
  2. One node, pinned
  3. Config and secrets
  4. Run it
  5. Bands

How it works

Each core node prices what it converts by what it holds: it quotes more for the asset it has least of. It signs a snapshot of its quotes, and pushes it to its peers. Your market maker's node peers with the core nodes it trades through, its targets, and reads their snapshots. The market maker also reads the price of an XRP Ledger pool between XRP and a dollar token. When a target's quote and the pool's price differ by more than a margin, it:

  1. asks its node to swap with that target, from the XRP hub to the dollar hub or back, at no worse than a rate that, swapped back on the pool, still clears the margin;
  2. swaps funds back on the pool, from one asset to the other on its node's XRP Ledger account, when the node holds more of an asset than its band allows.

The node sends the swap only while the target's snapshot promises at least that rate. So the target gets back the asset it ran short of, and the market maker keeps the margin. It is a client of its node, not a node: every amount it moves is moved by the node or by the XRP Ledger. Exchange rates explains quotes and snapshots.

Why market makers, not the nodes, rebalance, and why they swap with their peers

One node, pinned

You run one node, in the settlement hubs of both of the pool's assets:

  • The testnet's XRP hub, funded from its XRP ledger, xrpl-testnet-xrp.
  • The testnet's DSUSD hub, funded from its DSUSD ledger, xrpl-testnet-dsusd. The XRP ledger pays this hub's fees on the XRP Ledger too.

Both ledgers are on the node's one XRP Ledger account, and peer with no one. A swap with a target from one hub to the other makes that target convert, so one node trades through any number of targets, and its funds and bands are shared by all of them.

The node is a periphery node, with no hostname and no relay, and it pins every target as an uplink. So it peers with the targets and no one else, and each swap goes to the target it was meant for. Each target must be a core node in both hubs that offers swaps, as core nodes do unless their owner turns it off. On the testnet, boot1, boot2 and boot3 are such nodes. Your node pins each by its node ID, from its discovery document:

$curl -fsS https://boot1.dassie.xyz/.well-known/dassie/node | jq -r .id

Running the node

This runs the node as your user, with a data directory, IPC socket and ports of its own, apart from any other node on the machine:

$DASSIE_STATE_DIRECTORY=$HOME/mm/node \
  DASSIE_IPC_SOCKET_PATH=$XDG_RUNTIME_DIR/mm.sock \
  DASSIE_RELAY= \
  DASSIE_UPLINKS=<target node ID>,<another target's node ID> \
  dassie daemon --runtime standalone --http-port 4101 --https-port 5101 \
    --feed-listen 127.0.0.1:0
  • DASSIE_RELAY= keeps no relay. Nobody pays this node at a name, so it needs none, and no front door learns when it is online. Leave it out, and the node keeps the dassiepay.net front door, as a node without a hostname does by default.
  • DASSIE_UPLINKS pins the targets, by node ID, comma separated.
  • --feed-listen 127.0.0.1:0 keeps the node off the snapshot port, 7768, which a periphery node never answers on anyway.
  • Leave out --listen: the node's QUIC socket takes the address of its HTTPS listener, and must reach the targets. Nothing needs to reach the node's ports from outside, so keep them closed in your firewall.

For a server, the units Dassie's own market maker runs with are a good model: apps/market-maker/vm.

Setting it up

Keep the node's recovery phrase in a file of its own, and set the node up from it. Setup joins both hubs, which are built into Dassie, with the ledgers they are funded from. What the node is paid beyond each hub's float goes to your own account, which the swaps on the pool spend from:

$DASSIE_IPC_SOCKET_PATH=$XDG_RUNTIME_DIR/mm.sock \
  dassie setup --passphrase-file ~/mm/node.phrase \
    --hub hub-xrpl-testnet-xrp --hub hub-xrpl-testnet-dsusd

Running setup again changes nothing, so you can run it at every start. The node asks the testnet's faucet for XRP while its account does not exist yet. When it holds more XRP than its band, the market maker's first swap on the pool buys DSUSD with the surplus, so you need not buy DSUSD yourself.

Leave the node's Swapping with peers setting on, as it is for a new node, so that it reads its targets' snapshots. Give it no swap targets in its settings, so that it swaps only when the market maker asks. The market maker refuses to start while swapping is off.

Config and secrets

The market maker reads one JSON file, and checks all of it before it connects to anything:

~/mm/config.json
{
  "socket": "/run/user/1000/mm.sock",
  "passphraseFile": "/home/me/mm/node.phrase",
  "xrpl": {
    "server": "wss://s.altnet.rippletest.net:51233",
    "token": { "currency": "DSUSD", "issuer": "rURgPoYNN7tGqBzGXFeCLmKYgpSjYgJP6M" }
  },
  "xrp": {
    "ledger": "hub-xrpl-testnet-xrp+xrp",
    "ownerBook": "xrpl-testnet-xrp+xrp",
    "tradeSize": 2000000000,
    "band": { "low": 20000000000, "high": 80000000000 }
  },
  "token": {
    "ledger": "hub-xrpl-testnet-dsusd+usd",
    "ownerBook": "xrpl-testnet-dsusd+usd",
    "tradeSize": 2000000000,
    "band": { "low": 5000000000, "high": 40000000000 }
  },
  "marginPpm": 10000,
  "slippagePpm": 10000
}
socket

The node's IPC socket.

passphraseFile

The file with the node's recovery phrase, below.

xrpl

The XRP Ledger server, wss://, on the testnet, and the token the pool pairs with XRP: its currency code and issuer. The market maker refuses to sign for a server that is not on the testnet.

xrp, token

Each of the pool's two assets:

  • ledger: the node's ledger of the asset's hub, which its swaps with targets pay on and are paid back on.
  • ownerBook: the node's ledger of the same asset on its XRP Ledger account, which funds the hub and which the swaps on the pool spend.
  • tradeSize: the most one swap with a target pays of the asset.
  • band: its band, below.

Amounts are in the node's ledger units, a billion to one XRP or one dollar: 2000000000 is 2.

marginPpm

How much better than the pool a swap must be, in parts per million: 10000 is one percent.

slippagePpm

How far the pool may move against a swap on it before that swap fails rather than pays a worse price, in parts per million.

caFile

Optional: a file of root certificates to trust the XRP Ledger server by, beside the system's, for a host that has none of its own.

The config names no targets: they are the node's uplinks, which DASSIE_UPLINKS pins. These values are those of Dassie's own market maker, and they are starting points. Its XRP band's top is below what the faucet gives a new account, so its first swap on the pool buys DSUSD, and the two bands together hold the whole grant. allowPrivate is for dev networks and tests only. A config in the format of 0.2.x, with targets or a socket for each asset, is refused. The full rules are in config.rs.

The phrase

The market maker signs its swaps on the pool with the node's account key, so it needs the node's phrase, not only its socket. It checks that the phrase opens the account both of the node's owner books are on, keeps only the signing key in memory, and never logs a phrase.

Run it

$dassie market-maker --config ~/mm/config.json

It decides every ten seconds, and logs one line per decision: a swap with a target, with what it paid and what came back, a swap on the pool, or why it holds. It stops at start if something it needs is missing, such as a ledger its config names that the node does not run. Once it runs, it rides out outages and keeps going.

It trades only through targets whose snapshot is current, so a target that has gone quiet is left out while the others trade. Targets that pay alike take turns, so its swaps spread over them.

Bands

Each asset has a band: the least and the most of it the node should hold, counting its hub and its own account together.

  • Below the band: no swap with a target takes an asset below the bottom of its band. A swap that would is made smaller.
  • Above the band: before any swap with a target, the market maker swaps the asset back to the middle of its band on the pool, from one owner book to the other, never delivering more than the other asset's band has room for. So the two never swap the same funds back and forth.
  • Both above: it holds, and trades nothing.

Set each band's middle above what the node keeps in its hub, so that what a swap on the pool spends is on its account. The bands are the node's, whichever target a swap goes through, so adding a target needs no more funds.

The market maker in full, in the spec: what it trusts, and how it counts a swap on its way.

A kopje of granite blocks against the sun, in veld grass; one dassie climbs its slab, one keeps lookout on top inside the sun, and one sleeps on the next rock; on a wide screen, a pair sits on a kopje to the left