Connect an ILP client over BTP
Interledger software such as ilp-plugin-btp and
ilp-protocol-stream can use a Dassie node as its parent.
This page gets your client an API key, connects it, and shows what
the node lets it do.
On this page
Make an API key
The owner makes keys in the node's wallet.
-
Open Settings, and find the API keys card.
-
Under Create BTP token, give the key a label, so that you know it again, up to 64 bytes.
-
Pick its Ledger. The client pays and is paid in that ledger's asset, and its packets never change ledger. On the testnet, pick the ledger of a settlement hub, such as
hub-xrpl-testnet-xrp+xrp: nodes pay each other over hubs, so only a hub's ledger reaches other nodes. -
Set a Spending cap: an amount per hour, per day or per week. Leave it empty for no limit.
-
Press Create BTP token, and copy the URL. The wallet shows the token once: the node keeps only its hash.
The URL looks like this:
btp+wss://:<token>@node.example.com/btp
On a node whose HTTPS port is not 443, the port follows the host. A
node without a hostname, such as the desktop app, names
localhost and its port, for a client on the same
computer. BTP is served only on the node's own HTTPS port: a front
door never passes it on.
A node keeps up to 64 keys. The owner can change a key's cap, or revoke it, on the same card at any time. API keys shows the owner's side.
Connect
Install the client libraries:
$npm install ilp-plugin-btp ilp-protocol-ildcp ilp-protocol-stream ws
Pass the URL in an environment variable, so that it stays out of your code, and connect:
import BtpPlugin from "ilp-plugin-btp";
import ILDCP from "ilp-protocol-ildcp";
const plugin = new BtpPlugin({ server: process.env.BTP_URL });
await plugin.connect();
const { clientAddress, assetCode, assetScale } = await ILDCP.fetch((data) =>
plugin.sendData(data)
);
console.log(clientAddress, assetCode, assetScale);
$BTP_URL='btp+wss://:<token>@node.example.com/btp' node client.mjs
test.das.hauavng4okp6d55tghlsiqvnn6j5v6mpmktgnfq.k3x9qa XRP 9
The plugin sends the token in BTP's auth message. The
node expects it within ten seconds of the WebSocket opening.
A node on its own certificate
A node without a hostname, such as the desktop app, serves a
certificate it made itself, for localhost. A server
node serves one too, for its domain name, until Let's Encrypt has given it one
(Certificates). No certificate
authority vouches for it, and the plugin checks certificates, so the
snippet above fails the TLS handshake. Fetch the node's certificate
once, with the host and port of the BTP URL, over a network you
trust; the port is 443 when the URL names none. For the desktop app
the host is localhost:
$openssl s_client -connect <host>:<port> -servername <host> </dev/null | openssl x509 > node.pem
Then hand the plugin a WebSocket that trusts exactly that
certificate. It comes from the ws package, which the
plugin uses too:
import { readFileSync } from "node:fs";
import BtpPlugin from "ilp-plugin-btp";
import WebSocket from "ws";
const ca = readFileSync("node.pem", "utf8");
class TrustingWebSocket extends WebSocket {
constructor(url) {
super(url, { ca });
}
}
const plugin = new BtpPlugin(
{ server: process.env.BTP_URL },
{ WebSocket: TrustingWebSocket },
);
await plugin.connect();
The node keeps that certificate across restarts. It makes a new one only when it nears its end or when the node's names change; fetch it again then.
The address ILDCP gives you
ILDCP answers with an address below the node's own,
test.das.<node ID> on the testnet, with a segment
of six random letters and digits for this connection. Each
connection gets a new one, so do not hand it out as a lasting
address. Packets sent to it reach your client while it is connected;
when it goes away, the packets on their way to it fail.
A node without a hostname, such as the desktop app, is a periphery
node: it has no route of its own, and other nodes reach it beneath
one of its uplinks. So they reach your client at
<uplink's address>.<node ID>.<segment>,
not at the address ILDCP gives. Sending works the same either way.
The asset is the key's ledger's: its code, and a scale of 9. XRP and the testnet's dollar both count in billionths.
Why it works this way: why each endpoint gets its own ledger's asset.
Pay with STREAM
With the plugin connected, any STREAM sender can pay. Here it pays an
incoming payment's credentials, the ilpAddress and
sharedSecret from its methods
(how to get them):
import { createConnection } from "ilp-protocol-stream";
const connection = await createConnection({
plugin,
destinationAccount: method.ilpAddress,
sharedSecret: Buffer.from(method.sharedSecret, "base64url"),
});
const stream = connection.createStream();
await stream.sendTotal(500_000_000);
await connection.end();
await plugin.disconnect();
createConnection asks ILDCP for your address itself.
Disconnect the plugin when you are done: otherwise it reconnects on
its own. STREAM on Dassie says more
about the STREAM side.
What your client may do
-
Money moves only in ILP packets. A BTP
Transferis refused with an error that says so. -
It spends from the owner's funds on the key's
ledger. Each key has an account of its own under them, which only
records what went through it. A packet the owner's funds cannot
cover gets
T04. -
Its packets never change ledger. The node sends
them only to next hops it shares the key's ledger with. A
destination with no such route gets
F02. -
No packet may carry more than the node's largest
packet, 0.1 of a unit, such as 0.1 XRP. A larger one gets
F08, with the most that would pass, and STREAM splits what it sends to fit.
The spending cap
A cap allows at most an amount per window, and refills continuously, not all at once when the window ends. Over any stretch of time, a key spends at most the amount, plus the amount again for each window that passes.
- Only money that leaves the owner's funds counts. Paying the node's own wallet, or another key's client on the same node, does not.
- Every connection made with one key shares its cap, and packets in flight count as if they arrived.
-
A packet the cap has no room for now gets
T04, which STREAM waits out and tries again. A packet larger than the whole cap getsF08.
Error codes says what to do about each code.
When it does not connect
| What you see | Why | What to do |
|---|---|---|
The WebSocket closes with authentication failed
|
The token is wrong, or the owner revoked it. | Ask the owner for a new key. |
HTTP 429, Too many failed attempts |
Your address failed to log in too often. Only failures count, so a client may reconnect as often as it needs. | Wait a while, and use the right token. |
The WebSocket closes with
the token's ledger does not run
|
The owner turned off the scheme of the key's ledger. The key works again once it runs. | Ask the owner. |
HTTP 503, Too many BTP clients |
The node holds as many BTP connections as it takes. | Try again later. |
HTTP 503, The ledger is not running
|
The node is not set up, or is starting. | Try again in a moment. |
The WebSocket closes with no authentication
|
The client sent no token within ten seconds. |
Check that the URL has the token before the
@.
|
When the owner revokes a key, its connections close, and from that moment they take no new packets.