Transport
The transport layer handles peer-to-peer communication over direct, end-to-end encrypted connections. Between nodes the preferred transport is an encrypted TCP channel; WebRTC DataChannels (via node-datachannel) are used for peers that cannot open TCP sockets, such as browsers. All messages are transmitted as binary frames. Compatible with existing AI API formats, so existing tools work without modification.
Transport Modes
Transport selection is capability-driven; a node advertises what it supports in its signed discovery metadata.
| Mode | Library | Description |
|---|---|---|
encrypted TCP (transport.tcp-enc.v1) | Node.js net | Preferred. Direct TCP with a mutually authenticated, encrypted channel (X25519 + AES-256-GCM) |
| plaintext TCP | Node.js net | Legacy fallback for peers that do not advertise encrypted TCP |
webrtc (transport.webrtc.v1) | node-datachannel | WebRTC DataChannel (DTLS) via TCP signaling, for peers that cannot use TCP directly |
An initiator uses encrypted TCP whenever the peer supports it, WebRTC only for peers that advertise WebRTC but not encrypted TCP, and plaintext TCP only with legacy peers. Encrypted TCP is preferred partly because WebRTC DataChannel messages are capped at 256 KiB, below the protocol's frame sizes.
Seller Networking
A seller must announce an address that buyers can reach from the public internet. The default seller signaling/TCP port is 6882; the DHT port is 6881. seller.publicAddress should point to the public hostname or IP and the externally reachable signaling port, not a LAN address such as 192.168.x.x or 10.x.x.x.
antseed config seller set publicAddress "seller.example.com:6882"
antseed seller doctor
antseed seller start prints a prominent warning when publicAddress is empty or uses a loopback, private, link-local, unspecified, or multicast IP. antseed seller doctor classifies the configured address and attempts a bounded TCP connection from the seller machine. A successful local connection is useful but does not prove that inbound NAT traversal works; verify the same endpoint from a different network before considering the seller reachable.
Home or Office NAT
- Give the seller machine a stable LAN address.
- Forward the external TCP port (normally
6882) on the router to the seller machine's signaling port. - Allow inbound TCP traffic on that port in the host and cloud firewalls.
- Set
seller.publicAddressto the router's public IP or a DNS name that resolves to it. - Test the endpoint from a cellular connection, remote host, or another external network.
Some routers do not support NAT loopback (hairpinning), so a local probe to the public address can fail even when external clients can connect. Conversely, a local listener can succeed while the router still blocks inbound traffic. Carrier-grade NAT (CGNAT) usually prevents port forwarding entirely; request a public IP from the ISP or use a VPS pattern instead.
VPS and Reverse Tunnels
The simplest production deployment is a VPS with a public IP: allow inbound TCP on the announced port and run the seller there. If inference must remain on a private machine, expose its signaling port through a stable TCP reverse tunnel or gateway on a VPS, then announce the VPS hostname and public port. Configure the tunnel as a supervised service so it reconnects after restarts, and restrict firewall access to only the required ports.
Do not announce SSH-only tunnels, private VPN addresses, or temporary tunnel hostnames that buyers cannot reach. Re-run antseed seller doctor and an external connection test after changing DNS, firewall, router, or tunnel settings.
Frame Protocol
Offset Size Type Field
0 1 uint8 type (MessageType)
1 4 uint32 BE messageId
5 4 uint32 BE payloadLength
Max payload size: 64 MB. Frames exceeding this are rejected.
Message Types
| Hex | Name | Purpose |
|---|---|---|
| 0x01 | HandshakeInit | Initiator -> Responder |
| 0x02 | HandshakeAck | Responder -> Initiator |
| 0x10 | Ping | Keepalive probe |
| 0x11 | Pong | Keepalive response |
| 0x20 | HttpRequest | Buyer -> Seller: proxy request |
| 0x21 | HttpResponse | Seller -> Buyer: complete response |
| 0x22 | HttpResponseChunk | Seller -> Buyer: streaming chunk |
| 0x23 | HttpResponseEnd | Seller -> Buyer: final chunk |
| 0x24 | HttpResponseError | Seller -> Buyer: error |
| 0x25 | HttpRequestChunk | Buyer -> Seller: chunked upload body |
| 0x26 | HttpRequestEnd | Buyer -> Seller: chunked upload end |
| 0x50 | SpendingAuth | Buyer -> Seller: EIP-712 spending authorization |
| 0x51 | AuthAck | Seller -> Buyer: reserve() confirmed |
| 0x53 | SellerReceipt | Seller -> Buyer: running-total receipt |
| 0x54 | BuyerAck | Buyer -> Seller: receipt acknowledgement |
| 0x55 | TopUpRequest | Seller -> Buyer: request additional auth |
| 0x56 | PaymentRequired | Seller -> Buyer: 402 payment terms |
| 0xF0 | Disconnect | Graceful disconnect |
| 0xFF | Error | Protocol-level error |
Handshake (EIP-191 Challenge-Response)
Initiator Responder
│ │
├── HandshakeInit ─────────────>│
│ (evmAddress + nonce + sig) │
│ │
│<──── HandshakeAck ────────────┤
│ (evmAddress + nonce echo │
│ + sig) │
│ │
│ Both sides: Authenticated │
└───────────────────────────────┘
Each side sends a 32-byte random nonce signed with its secp256k1 private key via EIP-191 personal_sign. The responder echoes the initiator's nonce to prove it received the challenge. Verification uses ecrecover to confirm the signer matches the claimed EVM address. Handshake timeout: 10 seconds.
Keepalive
| Parameter | Value |
|---|---|
| Ping interval | 15 seconds |
| Pong timeout | 5 seconds |
| Max missed pongs | 3 (connection declared dead) |
Reconnection
Exponential backoff with jitter: base delay 1s, max delay 30s, max 5 attempts. Formula: min(baseDelay * 2^attempt + jitter, maxDelay). The buyer proxy handles transport reconnection independently from model-route failover. For model-only conversations, it softly prefers the seller that previously served the conversation and switches only when that route is unavailable, cooling down, policy-ineligible, or fails retryably. Explicit peer pins never switch automatically.
Payment Messages
Payment messages use the type range 0x50-0x5F. All payment payloads are JSON-encoded within the standard 9-byte binary frame.
| Hex | Name | Direction | Purpose |
|---|---|---|---|
| 0x50 | SpendingAuth | Buyer → Seller | EIP-712 signed spending authorization |
| 0x51 | AuthAck | Seller → Buyer | Seller confirms on-chain reserve() succeeded |
| 0x53 | SellerReceipt | Seller → Buyer | Seller reports usage/charge during or after serve |
| 0x54 | BuyerAck | Buyer → Seller | Buyer acknowledges receipt |
| 0x55 | TopUpRequest | Seller → Buyer | Seller requests additional spending authorization |
| 0x56 | PaymentRequired | Seller → Buyer | 402 trigger with payment terms |
PaymentMux
Payment messages are multiplexed over the same peer connection as proxy traffic. The 9-byte frame header (type, messageId, payloadLength) is shared across all message types — proxy (0x20-0x24), keepalive (0x10-0x11), and payment (0x50-0x5F). No separate connection or out-of-band channel is required.
The messageId field links payment messages to their originating proxy request. For example, a SpendingAuth (0x50) triggered by a specific HttpResponse carrying a 402 status uses the same messageId as that proxy exchange, allowing the seller to correlate the authorization with the pending request.
402 Payment Negotiation
When a buyer sends a request to a seller with no active spending authorization, the seller responds with HTTP 402 and a PaymentRequired (0x56) message. Two negotiation modes handle what happens next.
Auto Mode
The buyer node intercepts the 402 internally — it never reaches the application. The node checks the buyer's on-chain balance, signs a SpendingAuth (EIP-712), and sends it to the seller via PaymentMux. The seller verifies the signature, calls reserve() on-chain, and responds with AuthAck. The buyer then retries the original request.
Buyer Seller Chain
│ │ │
├── HttpRequest (0x20) ────────>│ │
│ │ │
│<── PaymentRequired (0x55) ────┤ │
│ (sellerEvmAddr, tokenRate, │ │
│ firstSignCap, suggested) │ │
│ │ │
│ [check balance, sign] │ │
│ │ │
├── SpendingAuth (0x50) ───────>│ │
│ (EIP-712 signature) │ │
│ ├── reserve() ─────────────────>│
│ │<── tx confirmed ──────────────┤
│ │ │
│<── AuthAck (0x51) ────────────┤ │
│ │ │
├── HttpRequest (0x20) ────────>│ [retry, serves normally] │
│<── HttpResponse (0x21) ───────┤ │
└───────────────────────────────┘ │
During serving, SellerReceipt (0x52) and BuyerAck (0x53) messages flow alongside proxy traffic for bilateral accounting. When usage exceeds 80% of the authorized maxAmount, the seller sends a TopUpRequest (0x54) to request additional authorization before the current one is exhausted.
Manual Mode
The 402 and PaymentRequired payload propagate to the application (e.g., the desktop app). The user sees an approval card with the seller's terms. On approval, the application signs the SpendingAuth and encodes it as base64 in the x-antseed-spending-auth HTTP header on the retry request. The buyer node extracts the header before proxying and sends the SpendingAuth via PaymentMux (0x50). From there, the on-chain flow is identical to auto mode.