Docs

Agents

spaceturtle

The connector, by hand

Peer with another node

Your node carrying another node's packets for a fee, set up from one URL.

Terminating your own routes earns from callers who know your address. Peering puts you on paths that start somewhere else: you carry someone's packet one hop further, and keep a fee for doing it.

The whole of it is one free read and three signed writes against your own node:

curl -s https://their-node.example/ilp | jq          # 1. read their terms, free
sign-write.sh -X POST -p /peers ...                  # 2. open your channel toward them
sign-write.sh -X POST -p /channels/<id>/fund ...     # 3. keep collateral behind your vouchers
sign-write.sh -X POST -p /routes/peers ...           # 4. route through it, at your price

A peering is created by an operator and by nothing else. It cannot be bought, learned or announced into existence. What you give it is one thing: the counterparty's self-description URL.

Being peerable

The minimal config from Run a node boots and serves, but nobody can peer with it: its self-description has no endpoints and "peerCarriages": []. Three more keys close that gap:

connector.toml
# Top level, beside client_edge_addr.
peer_expose = "http"             # "btp", "http", "both", or "neither" (default)

[node]
addresses     = ["g.your.node"]
http_endpoint = "https://your-node.example/ilp"

peer_expose says which carriage this node opens a peer listener for. [node] publishes where clients reach it. http_endpoint is required whenever anything is exposed, because a peer pays you by asking your client edge over HTTP whichever carriage its packets ride; btp_endpoint is required only when "btp" or "both" is exposed. For a local trial only, peer_allow_plaintext_endpoints = true accepts http:// and ws://; every deployed config stays on https:// and wss://.

Read the other node first

GET /ilp on the counterparty's URL is free, unauthenticated, and the whole of your homework. Do it by hand before you write anything:

curl -s https://their-node.example/ilp | jq
Their self-description
{
  "ilpAddresses": ["g.their.node"],
  "httpEndpoint": "https://their-node.example/ilp",
  "peerCarriages": ["http"],
  "edgeIdentity": { "keyId": "connector-signer", "publicKey": "0x04…" },
  "batchSettlements": [{
    "network": "eip155:84532",
    "asset": "0x0C996d7c934c79a6255254875607Fe69df25C0E1",
    "payTo": "0x…",
    "receiverAuthorizer": "0x…",
    "withdrawDelay": 86400,
    "name": "USDC",
    "version": "2"
  }],
  "voucherSigners": [{ "network": "eip155:84532", "signer": "0x…" }],
  "routes": [{ "prefix": "g.their.node.app", "price": "1000" }],
  "supportedVersions": [1],
  "defaultVersion": 1
}
FieldWhat it settles
peerCarriagesEmpty means not dialable: they have not set peer_expose, and your write will answer 502.
httpEndpoint / btpEndpointWhere you dial, and which carriage: wss:// is BTP, https:// is ILP-over-HTTP. BTP wins where both are published.
batchSettlements[]You need x402 terms on a network in common. No overlap is a 502.
voucherSigners[]The key their vouchers are signed with. Without one for the shared network the EVM write is a 502.
routes[].priceWhat their terminating route charges: the number your packet's amount has to cover. A decimal string, not a number.

Fields a node has nothing to say about are omitted entirely, never sent empty, so parse for absent keys. Note what is not there: no channel id, no peer list, no fee, no cap. A peering is two one-way x402 channels, one opened by each side. You open and fund yours toward their payTo, they open theirs toward yours, and each node recognises the other's channel by the voucherSigners key the other publishes. No identifier and no shared secret is ever exchanged.

The three writes

Peering is three signed writes against your own operator surface, in this order. sign-write.sh is the shipped signer.

1. Establish the peering. This is the write that reads their URL:

docs/operators/sign-write.sh -k operator-write.key -X POST -p /peers \
  -u https://your-node.example \
  -b '{"id":"their-node","url":"https://their-node.example/ilp","fee":100,"max_packet_amount":5000,"deposit":100000}'
The node's answer
{
  "id": "their-node",
  "fee": 100,
  "max_packet_amount": 5000,
  "source": "runtime",
  "channel": { "id": "0x…", "status": "created", "chain": "evm" }
}

Your node fetches that URL, picks the carriage from their endpoint's scheme, finds the settlement chain you share, and opens and funds your own outbound x402 channel toward them with deposit, or finds the one you already have open, spending nothing. "status" says which branch it took, "created" or "found". They do the same with your URL; until they do, their packets reach you as a client's.

  • id is your own local label; nothing puts it on the wire.
  • fee is what you keep for carrying one packet over this peering: flat, per packet, never a share of the amount.
  • max_packet_amount is the largest single packet you will carry for them, which is the most they can cost you at once.
  • Both are in base units of the channel this peering settles on. Two nodes that share more than one chain must say which: "chain": "evm" or "solana".

2. Keep your own collateral behind your own vouchers. fund takes an increment, so posting the same write twice deposits twice:

docs/operators/sign-write.sh -k operator-write.key -X POST -p /channels/0x…/fund \
  -u https://your-node.example -b '{"amount":3000}'

That is also the cure when a channel runs out of headroom and starts refusing packets T00. The router's view refreshes on its own schedule, so give it a minute or two before funding again.

3. Route through the peering. A peering and a route are different decisions, and either can exist without the other:

docs/operators/sign-write.sh -k operator-write.key -X POST -p /routes/peers \
  -u https://your-node.example \
  -b '{"prefix":"g.their.node","peer_id":"their-node","price":1100}'

price here is what your caller pays you for the whole path. What you retain is the fee on the peering; the rest goes onward. So the arithmetic that has to hold, and that nothing checks for you, is:

your price − your fee  ≥  the next hop's price
      1100 −       100  =  1000  ✓

One unit short and the far side rejects the packet outright, F03: "claim rejected: advances value by 950, less than this route's price of 1000". A refusal, not a discount.

GET /routes/peers reads the table back. DELETE /peers/:id is the kill switch: it takes the carriage away with the row, immediately and with no restart. A peering still referenced by a runtime route is refused until the route goes. A peering committed to the config file is the same two channels recorded as [[peers]], [[peer_channels]] and [[pay_channels]] rows; config always wins a collision.

Before you point this at a real chain

  • POST /peers spends gas, because it may open a channel and wait for confirmation. It is safe to retry: the same request against an established peering finds the same channel.
  • fee and max_packet_amount are yours to choose. No document can supply them; they are your policy about this counterparty.
  • A 502 is about them, a 400 is about you. A common one: POST /peers takes a self-description URL, so an origin with no /ilp on the end answers 502 naming that fix.
  • Their identity is trust-on-first-use over TLS, pinned by nothing. Whatever that URL serves is who the peering is with, and that choice determines the channel address. Vet the URL.
  • A hop can take your claim and decline to carry. Every packet you forward carries its own covering claim, so a reject comes back with that claim spent. This can only ever happen to the one packet in flight; nobody holds your deposit. Keep packets small, and let the amount grow with a path's record.
  • A new peering starts at a conservative cap, and nothing raises it automatically. Post the same id again with a larger max_packet_amount. An amount is a 64-bit integer, so one packet on an 18-decimals leg tops out around 18.4 tokens.

The connector repository has a local peering lab: two nodes, a disposable chain, and one packet across the peering, with nothing outside a local anvil.

Written from connector/docs/guide/peering.md.