Skip to main content

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.

ModeLibraryDescription
encrypted TCP (transport.tcp-enc.v1)Node.js netPreferred. Direct TCP with a mutually authenticated, encrypted channel (X25519 + AES-256-GCM)
plaintext TCPNode.js netLegacy fallback for peers that do not advertise encrypted TCP
webrtc (transport.webrtc.v1)node-datachannelWebRTC 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

  1. Give the seller machine a stable LAN address.
  2. Forward the external TCP port (normally 6882) on the router to the seller machine's signaling port.
  3. Allow inbound TCP traffic on that port in the host and cloud firewalls.
  4. Set seller.publicAddress to the router's public IP or a DNS name that resolves to it.
  5. 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

frame header (9 bytes)
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

HexNamePurpose
0x01HandshakeInitInitiator -> Responder
0x02HandshakeAckResponder -> Initiator
0x10PingKeepalive probe
0x11PongKeepalive response
0x20HttpRequestBuyer -> Seller: proxy request
0x21HttpResponseSeller -> Buyer: complete response
0x22HttpResponseChunkSeller -> Buyer: streaming chunk
0x23HttpResponseEndSeller -> Buyer: final chunk
0x24HttpResponseErrorSeller -> Buyer: error
0x25HttpRequestChunkBuyer -> Seller: chunked upload body
0x26HttpRequestEndBuyer -> Seller: chunked upload end
0x50SpendingAuthBuyer -> Seller: EIP-712 spending authorization
0x51AuthAckSeller -> Buyer: reserve() confirmed
0x53SellerReceiptSeller -> Buyer: running-total receipt
0x54BuyerAckBuyer -> Seller: receipt acknowledgement
0x55TopUpRequestSeller -> Buyer: request additional auth
0x56PaymentRequiredSeller -> Buyer: 402 payment terms
0xF0DisconnectGraceful disconnect
0xFFErrorProtocol-level error

Handshake (EIP-191 Challenge-Response)

handshake flow
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

ParameterValue
Ping interval15 seconds
Pong timeout5 seconds
Max missed pongs3 (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.

HexNameDirectionPurpose
0x50SpendingAuthBuyer → SellerEIP-712 signed spending authorization
0x51AuthAckSeller → BuyerSeller confirms on-chain reserve() succeeded
0x53SellerReceiptSeller → BuyerSeller reports usage/charge during or after serve
0x54BuyerAckBuyer → SellerBuyer acknowledges receipt
0x55TopUpRequestSeller → BuyerSeller requests additional spending authorization
0x56PaymentRequiredSeller → Buyer402 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.

auto mode flow
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.