Hippius Hermes Documentation

Hermes is a Bittensor cross-subnet Machine-to-Machine (M2M) communication protocol. It enables trustless, high-bandwidth communication between any two nodes across any Bittensor subnet using Hippius infrastructure.

Dual-Layer Architecture

Control Plane

Low-latency QUIC signaling via iroh with direct UDP hole-punching. No relays.

Data Plane

Large file transfer via the Hippius Sync-Engine API (Coming Soon!). Control messages carry metadata and keys, not the data itself.

Installation

Python

pip install hippius-hermes

Rust

[dependencies]
hippius-hermes-core = "0.2"

Node Registration

Before a Hermes Node can send or receive messages on the network, its offline Iroh Ed25519 SecretKey must be cryptographically associated with your main Substrate Wallet in the AccountProfile pallet on the Hippius chain.

We provide two automated CLI registration methods: API (Feeless) and Chain (Direct).

1. Generate an identity

First, generate your local node secret key:

cargo run --bin hippius-hermes-cli -- keygen --out-path hermes.key

2. Register your Node

Configure your hermes_config.json with your SS58 wallet address and the path to the key you just generated. Then run the interactive registration command:

# Default (API Method - No Substrate Tokens Required!)
cargo run --bin hippius-hermes-cli -- --config hermes_config.json register --ss58 5Grwv... --username my_node

# Direct Chain Method (Requires Substrate Tokens for Gas)
cargo run --bin hippius-hermes-cli -- --config hermes_config.json register --ss58 5Grwv... --method chain

Cryptographic Ownership Verification

Regardless of the method chosen, the CLI will prompt you locally for your Substrate Wallet Mnemonic.

Enter Substrate Wallet Mnemonic (hidden): *****

Security Note: If you use the default API method, your mnemonic is never transmitted. It is exclusively used locally by the Rust Keyring to generate a mathematical Signature over your Iroh NodeId. This signature is then POSTed to the Hippius API, which transparently pays the Extrinsic Gas fees on your behalf.

Configuration

Every Hermes node requires a configuration that identifies the node and controls its network behavior. The subnet_ids field is the key to ALPN-based subnet whitelisting.

Config Fields

Field Type Required Description
node_secret_key_path string Yes Path to the 32-byte Ed25519 secret key file for Iroh node identity
ss58_address string Yes Your node's SS58 address on the Bittensor network
api_token string Yes Hippius Sync-Engine API token for data plane access (Coming Soon)
storage_directory string Yes Local directory for the offline message queue database
rpc_url string No Hippius chain RPC endpoint. Default: wss://rpc.hippius.network:443
subnet_ids array of u16 No List of Bittensor subnet netuids to whitelist. Default: [] (cross-subnet only)
s3 object No Optional native S3 credentials block containing bucket, access_key, and secret_key to override the Arion HTTP API.
enable_firewall boolean No If true, Hermes will aggressively drop all incoming connections from SS58 addresses not injected via client.set_firewall_whitelist([...]). Default: false
pullweights_api_key string No Optional PullWeights API key for pushing/pulling ML models via the pullweights.com registry. Default: null
enable_queue boolean No Enable persistent sled message queue. Only needed for listeners. Default: false
encryption_key_path string No Path to 32-byte X25519 secret key file for E2E encrypted messaging. Default: null
skip_identity_verification boolean No Skip on-chain identity verification for incoming P2P data. Default: false

JSON Config File

{
    "node_secret_key_path": "/etc/hermes/iroh.key",
    "ss58_address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
    "api_token": "sk-your-hippius-token-here",
    "storage_directory": ".hermes_data",
    "rpc_url": "wss://rpc.hippius.network:443",
    "subnet_ids": [42, 69],
    "s3": {
        "bucket": "hippius-arion",
        "access_key": "YOUR_KEY",
        "secret_key": "YOUR_SECRET"
    },
    "enable_firewall": true,
    "pullweights_api_key": "sk-pw-your-key-here",
    "encryption_key_path": "./hermes_encryption.key"
}

The subnet_ids field is optional. If omitted, the node only accepts the universal cross-subnet protocol hippius-hermes/1. When specified, the node additionally accepts per-subnet protocols for each listed netuid.

ALPN Protocol Negotiation

Hermes uses Application-Layer Protocol Negotiation (ALPN) to scope and filter QUIC connections at the TLS level. ALPN is a TLS extension that lets the client and server agree on an application protocol during the TLS handshake — before any application data is exchanged.

Why ALPN? Connections with unrecognized ALPNs are rejected during the TLS handshake itself, before any application code runs. This makes ALPN filtering extremely efficient and impossible to bypass without a valid TLS certificate for the expected protocol.

Protocol Format

Protocol ALPN String Scope Always Registered
Cross-subnet hippius-hermes/1 Universal — any node can communicate Yes
Per-subnet hippius-hermes/subnet/<netuid> Scoped to a specific Bittensor subnet Only if listed in subnet_ids
Data plane (P2P) hippius-hermes/data/1 Direct peer-to-peer file streaming Yes

How It Works

1
Registration

At startup, the node registers ALPNs with the Iroh QUIC endpoint based on subnet_ids. The cross-subnet ALPN hippius-hermes/1 is always registered automatically.

2
Connection

When a sender connects, they specify which ALPN to use. The receiver's TLS config automatically rejects connections that don't match any registered ALPN.

3
Validation

After the TLS handshake, the listener performs a defense-in-depth check by verifying the negotiated ALPN against its registered set before processing any message.

Cross-Subnet Protocol

The cross-subnet ALPN hippius-hermes/1 is the universal protocol that every Hermes node accepts regardless of configuration. It enables communication between nodes on different Bittensor subnets.

ALPN: hippius-hermes/1

Sender (Subnet 42) ──── hippius-hermes/1 ────> Receiver (Subnet 69)
                    Cross-subnet OK

Use this when your message isn't scoped to a specific subnet — for example, when a validator on subnet 42 needs to coordinate with a miner on subnet 69.

Even with an empty subnet_ids config, the node always accepts hippius-hermes/1 connections. This ensures every node is reachable for cross-subnet communication.

Per-Subnet Protocols

Per-subnet ALPNs let nodes scope their communication to a specific Bittensor subnet. When a node registers subnet_ids: [42], it additionally accepts connections using the ALPN hippius-hermes/subnet/42.

ALPN: hippius-hermes/subnet/42

Sender (Subnet 42) ── hippius-hermes/subnet/42 ──> Receiver (Subnet 42)
                      Same subnet OK

Sender (Subnet 69) ── hippius-hermes/subnet/42 ──> Receiver (Subnet 42)
                      Receiver accepts (42 is registered)

Sender (Subnet 69) ── hippius-hermes/subnet/69 ──> Receiver (Subnet 42)
                      TLS REJECTED (69 not in receiver's subnet_ids)

This means a receiver with subnet_ids: [42] will accept connections on hippius-hermes/subnet/42 (and the universal hippius-hermes/1), but will reject connections on hippius-hermes/subnet/69 at the TLS level.

Multi-Subnet Whitelisting

Nodes can participate in multiple subnets simultaneously by listing multiple netuids in the subnet_ids array. Each netuid adds a corresponding ALPN protocol to the node's accepted set.

How It Works

Given this configuration:

{
    "subnet_ids": [1, 42, 69]
}

The node registers 5 ALPNs:

# ALPN Source
1 hippius-hermes/1 Always registered (cross-subnet control)
2 hippius-hermes/data/1 Always registered (direct P2P data)
3 hippius-hermes/subnet/1 From subnet_ids
4 hippius-hermes/subnet/42 From subnet_ids
5 hippius-hermes/subnet/69 From subnet_ids

Connection Matrix

With multi-subnet whitelisting, the node accepts incoming connections on any of its registered ALPNs:

Node A: subnet_ids = [42, 69]
    Accepts: hippius-hermes/1, hippius-hermes/data/1, hippius-hermes/subnet/42, hippius-hermes/subnet/69

Node B: subnet_ids = [42, 100]
    Accepts: hippius-hermes/1, hippius-hermes/data/1, hippius-hermes/subnet/42, hippius-hermes/subnet/100

Communication paths:
    A -> B via hippius-hermes/1           OK  (both accept cross-subnet)
    A -> B via hippius-hermes/subnet/42   OK  (B has 42 registered)
    A -> B via hippius-hermes/subnet/69   FAIL (B does NOT have 69 registered)
    A -> B via hippius-hermes/subnet/100  OK  (B has 100 registered)
    B -> A via hippius-hermes/subnet/100  FAIL (A does NOT have 100 registered)
    B -> A via hippius-hermes/subnet/42   OK  (A has 42 registered)
ALPN is receiver-side filtering. The sender picks which ALPN to use when connecting. The receiver's TLS config determines whether to accept or reject. Both nodes must agree on at least one common ALPN for communication to succeed. The universal hippius-hermes/1 guarantees a fallback path always exists.

Typical Deployment Patterns

Subnet-Only Miner

"subnet_ids": [42]

Accepts traffic from subnet 42 and cross-subnet. Rejects per-subnet traffic from any other subnet.

Multi-Subnet Validator

"subnet_ids": [1, 42, 69, 100]

Participates in 4 subnets simultaneously. Accepts per-subnet traffic from all of them.

Cross-Subnet Relay

"subnet_ids": []

Only accepts the universal hippius-hermes/1 protocol. Acts as a cross-subnet bridge.

Full-Network Node

"subnet_ids": [1, 2, 3, ..., 255]

Registers ALPNs for every subnet. Accepts all per-subnet and cross-subnet traffic.

Dynamic API Firewall

By default, Hippius Hermes accepts incoming connections from any node that correctly formats the ALPN handshake. To protect validators and miners against volumetric connection DDoS from unauthorized identities, you can enable the O(1) Memory Firewall.

Zero-Resource Rejection When enable_firewall: true is configured, the inner Rust QUIC engine intercepts TLS handshakes. It resolves the incoming NodeId to its SS58 address. If the SS58 is not in the whitelist, the connection is instantly killed before any streams are allocated or async memory is reserved, completely eliminating memory-exhaustion vectors.

Python Whitelist Injection

Validators generally sync the Bittensor Metagraph every 12 seconds. Whenever the metagraph updates, you dynamically inject the list of valid miner SS58 hotkeys into the Rust engine's active memory:

# Inside your Validator sync loop:
valid_hotkeys = metagraph.hotkeys  # List of string SS58 addresses

# Instantaneously updates the Rust QUIC Firewall rules
client.set_firewall_whitelist(valid_hotkeys)

Global ACL (Access Control List)

In addition to the subnet firewall, Hermes provides a Nebula-inspired global ACL that applies to all ALPNs (data, cross-subnet, subnet). The ACL has two layers:

1
Blocklist

Nodes on the blocklist are always rejected, regardless of allowlist status. Blocklist wins.

2
Allowlist

If the allowlist is non-empty, only listed nodes pass. If the allowlist is empty, all non-blocked nodes are allowed (open policy).

Python Example

# Block specific bad actors
resolved = await client.set_acl_blocklist(["5BadActor1...", "5BadActor2..."])
print(f"Blocked {resolved} nodes")

# Restrict to known-good nodes only
resolved = await client.set_acl_allowlist(metagraph.hotkeys)
print(f"Allowlisted {resolved} nodes")
Two-layer access control. Incoming connections pass through the Global ACL first (all ALPNs), then the Subnet Firewall (subnet ALPNs only). Both set_acl_allowlist() and set_acl_blocklist() return the number of successfully resolved SS58 addresses.

Direct P2P File Transfers

Hermes supports direct peer-to-peer file streaming over QUIC. Files are pushed directly from sender to receiver using the hippius-hermes/data/1 ALPN protocol.

Push-based model. The sender initiates the transfer and pushes file bytes directly to the receiver. The receiver's listener fires a data callback when the file arrives — no manual pull needed.

How It Works

1
Connect

Sender resolves the receiver's NodeId from their SS58 address and connects on hippius-hermes/data/1.

2
Stream

Sender writes a length-prefixed JSON header followed by raw file bytes (64KB chunks) over a QUIC bi-stream.

3
Receive

Receiver verifies sender identity, streams bytes to disk, and fires the data callback with file metadata.

Python Example

# Sender: push file directly to peer
filename = await client.send_file_unencrypted(dest_ss58, "./gradients.bin")

# Receiver: register data callback
def on_file(sender_ss58, filename, local_path, file_size):
    print(f"Received {filename} ({file_size} bytes) from {sender_ss58}")
    print(f"Saved to: {local_path}")

client.start_listener(on_message, on_data=on_file)

Rust Example

// Sender: push file directly to peer
let filename = client
    .send_file_unencrypted(dest_ss58, "./gradients.bin", None)
    .await?;

// Receiver: register data callback
client.spawn_listener(
    |msg| println!("Control: {}", msg.action),
    Some(|sender, filename, path, size| {
        println!("Received {} ({} bytes) at {}", filename, size, path);
    }),
);

End-to-End Encryption (E2EE)

Hermes nodes support secure payload delivery using the TweetNaCl crypto_box standard (X25519, XSalsa20, Poly1305).

How it Works During CLI registration, Hermes generates a static X25519 keypair. The private key is saved locally (hermes_encryption.key), and the public key is broadcasted to the decentralized Hippius blockchain inside the AccountProfile pallet. Senders can query this on-chain state to encrypt a payload that ONLY the target Hermes node can decrypt!

How It Works

1
Registration

During hippius-hermes-cli register, a static X25519 keypair is generated. The private key is saved locally as hermes_encryption.key. The public key is written to the AccountProfile pallet on the Hippius blockchain.

2
Key Lookup

The sender queries the target's SS58 address on-chain to retrieve their static X25519 public key from the AccountProfile storage.

3
Ephemeral DH Exchange

The sender generates a throwaway ephemeral X25519 keypair for Perfect Forward Secrecy. A shared secret is derived from the ephemeral private key and the target's static public key.

4
Encrypt & Transmit

The payload is encrypted using XSalsa20-Poly1305 with the shared secret. The ciphertext and the ephemeral public key are transmitted to the receiver.

5
Decrypt

The receiver reconstructs the shared secret using their static private key and the sender's ephemeral public key, then decrypts the ciphertext.

Native Hermes E2EE

The Hermes SDK handles E2EE natively — no manual crypto required. The client automatically resolves the recipient's on-chain encryption key and applies NaCl SealedBox encryption.

Python

from hermes import Config, HermesClient
import asyncio

async def main():
    config = Config.from_file("hermes_config.json")
    client = await HermesClient.create(config)

    # Send an E2E encrypted message (NaCl SealedBox)
    await client.send_message_encrypted(
        "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
        "encrypted_message",
        b"top secret payload data"
    )

asyncio.run(main())

CLI

hippius-hermes-cli --config hermes_config.json send-encrypted \
    --dest-ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
    --action "encrypted_message" \
    --message "top secret payload data"

Python E2EE Sender (Standalone)

Note: Standard Rust/C++ Hermes nodes natively automate this entire on-chain key lookup and handshake process over QUIC under the hood! However, if you are writing an external application (like a Python web backend) that needs to send an encrypted payload to a Hermes node, you can use the substrate-interface and PyNaCl libraries to securely encrypt a payload directly from the blockchain state.

from nacl.public import PrivateKey, PublicKey, Box
from nacl.encoding import HexEncoder
from substrateinterface import SubstrateInterface

# 1. Query target's encryption key from the Hippius blockchain
substrate = SubstrateInterface(url="wss://rpc.hippius.network:443")
bob_ss58 = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"

result = substrate.query("AccountProfile", "AccountProfiles", [bob_ss58])
bob_pub_hex = result.value['encryption_key'].replace("0x", "")

# 2. Generate ephemeral X25519 keypair for Perfect Forward Secrecy
ephemeral_private = PrivateKey.generate()
ephemeral_public = ephemeral_private.public_key

# 3. Encrypt with TweetNaCl crypto_box (X25519 + XSalsa20-Poly1305)
bob_pub = PublicKey(bob_pub_hex.encode("utf-8"), encoder=HexEncoder)
sender_box = Box(ephemeral_private, bob_pub)

message = b"Secure model weights for subnet 42"
encrypted = sender_box.encrypt(message)  # Nonce auto-prepended

# 4. Transmit encrypted payload + ephemeral public key to Bob
# Bob needs: encrypted (bytes), ephemeral_public (bytes)
print(f"Encrypted {len(encrypted)} bytes — only Bob can decrypt")

Python E2EE Receiver

The receiving Hermes node decrypts using its static private key and the sender's ephemeral public key:

from nacl.public import PrivateKey, PublicKey, Box
from nacl.encoding import RawEncoder

# 1. Bob loads his static private key from disk (generated during registration)
with open("hermes_encryption.key", "rb") as f:
    bob_private = PrivateKey(f.read(), encoder=RawEncoder)

# 2. Reconstruct the Box using Bob's private key + Alice's ephemeral public key
alice_ephemeral_pub = PublicKey(received_ephemeral_bytes, encoder=RawEncoder)
bob_box = Box(bob_private, alice_ephemeral_pub)

# 3. Decrypt — XSalsa20-Poly1305 authenticates and decrypts in one step
plaintext = bob_box.decrypt(received_ciphertext)
print(f"Decrypted: {plaintext.decode('utf-8')}")

Full Roundtrip Demo

A complete self-contained example that generates keys and demonstrates the full encrypt → transmit → decrypt lifecycle:

from nacl.public import PrivateKey, Box
from nacl.encoding import HexEncoder

# === Setup: Bob registers his static key on-chain ===
bob_static_private = PrivateKey.generate()
bob_static_public = bob_static_private.public_key
print(f"Bob registered: {bob_static_public.encode(encoder=HexEncoder).decode()}")

# === Alice encrypts a message for Bob ===
alice_ephemeral = PrivateKey.generate()
alice_box = Box(alice_ephemeral, bob_static_public)
encrypted = alice_box.encrypt(b"Secure P2P delivery verified.")
print(f"Alice encrypted: {len(encrypted)} bytes")

# === Bob decrypts using his static key + Alice's ephemeral public ===
bob_box = Box(bob_static_private, alice_ephemeral.public_key)
plaintext = bob_box.decrypt(encrypted)
print(f"Bob decrypted: '{plaintext.decode()}'")
# Output: Bob decrypted: 'Secure P2P delivery verified.'
Forward Secrecy Each message uses a fresh ephemeral keypair. Compromising a sender's ephemeral private key only exposes that single message — not past or future communications. Bob's static key remains safe on disk.

Direct P2P vs Sync-Engine

Hermes offers two data transfer modes. Choose based on your use case:

Feature Direct P2P Sync-Engine
Transport QUIC bi-stream (hippius-hermes/data/1) HTTP upload/download via Arion
Intermediary None — pure P2P Hippius Sync-Engine server
Receiver must be online Yes — at time of send No — stored until retrieved
Best for Real-time streaming: gradients, tensors, live data Large persistent payloads: model weights, datasets
API (Python) send_file_unencrypted() send_file_via_s3()
Receiving Automatic via on_data callback Explicit download_file_http()
Use both together. For many workloads, you'll use Direct P2P for real-time gradient exchange during training, and the Sync-Engine for distributing finalized model checkpoints. Both modes work on the same Hermes client.

S3 Pre-Signed Downloads

For transmitting incredibly large multi-gigabyte models (e.g. LLM checkpoints), the Hippius Sync-Engine HTTP API can become a minor bottleneck because it streams data through the Arion servers for billing verification.

Hermes solves this by allowing Sender nodes to pre-sign native Amazon S3 GET queries perfectly offline.

1
Offline Generation

The Sender node (configured with S3 API keys) generates an S3 Pre-Signed URL for the file valid for exactly 24 hours, entirely locally without making any network calls to AWS.

2
Encrypted P2P Transmission

The Sender injects this raw Pre-Signed URL into the hermes.message(dest, payload) control packet. It transmits to the receiver instantly over the encrypted Iroh Control Plane.

3
Direct Bucket Streaming

The Receiver intercepts the control message payload, extracts the URL, and invokes the Rust download_file_http async method to pull the 10GB payload straight from the cloud bucket to disk at maximum line speed (10+ Gbps).

Receivers do NOT need S3 Configs. The beauty of this architecture is that your Miners/Receivers do not need any AWS IAM Credentials or S3 configuration. The sender handles all authorization via the cryptographic signature glued precisely onto the ephemeral GET URL.

PullWeights Model Registry

Hippius Hermes natively integrates with the PullWeights enterprise model registry, allowing seamless pushing and pulling of machine learning models via the CLI.

Configuring API Keys To push models, you must inject your PullWeights API key into your hermes_config.json under the pullweights_api_key field.

Pushing a Model

You can push a local model direct to the PullWeights registry via the CLI:

hippius-hermes-cli push-model \
  --org my-org \
  --model my-llm \
  --file-path ./model.safetensors

Pulling a Model

You can instantly pull specific tags down to your local directory (defaulting to the current working directory). Unauthenticated pulling is supported for public models, but private models utilize your pullweights_api_key.

hippius-hermes-cli pull-model \
  --org my-org \
  --model my-llm \
  --tag v1.0.0 \
  --download-dir /mnt/models

Python API

Config

from hermes import Config

# From keyword arguments
config = Config(
    node_secret_key_path="/etc/hermes/iroh.key",
    ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
    api_token="sk-your-token",
    storage_directory=".hermes_data",
    rpc_url=None,              # Optional, defaults to wss://rpc.hippius.network:443
    subnet_ids=[42, 69],       # Optional, defaults to []
    s3_bucket="my-models",     # Optional S3 Bypass
    s3_access_key="AKIA...",
    s3_secret_key="AWS...",
    enable_firewall=True,      # Optional O(1) TLS connection dropping
    pullweights_api_key="sk-...", # Optional PullWeights token
    enable_queue=True,         # Optional persistent sled message queue
    encryption_key_path="./hermes_encryption.key"  # Optional E2E encryption key
)

# From JSON file
config = Config.from_file("hermes_config.json")

HermesClient

from hermes import Config, HermesClient

# Create client (async)
client = await HermesClient.create(config)

# Upload to S3 and send Pre-Signed URL to peer (No Sync-Engine)
await client.send_file_via_s3(dest_ss58, "./model.bin")

# Download from a Pre-Signed URL
await client.download_file_http(presigned_url, "./model.bin")

# Update the memory firewall (drops unauthorized TLS at O(1))
resolved = await client.set_firewall_whitelist(["5Grwv...", "5FHne..."])

# Global ACL: blocklist / allowlist (applies to all ALPNs)
await client.set_acl_blocklist(["5BadActor..."])
await client.set_acl_allowlist(["5Trusted1...", "5Trusted2..."])

# Start listener (callback receives action, sender_ss58, payload_bytes)
def on_message(action, sender_ss58, payload):
    print(f"From {sender_ss58}: {action}")

client.start_listener(on_message)

# Direct P2P file transfer
filename = await client.send_file_unencrypted(dest_ss58, file_path)

# Start listener with data callback for direct P2P
def on_file(sender_ss58, filename, local_path, file_size):
    print(f"Got {filename} from {sender_ss58}")

client.start_listener(on_message, on_data=on_file)

# Send E2E encrypted message (NaCl SealedBox)
await client.send_message_encrypted(dest_ss58, "encrypted_message", b"secret payload")

# Start background retry worker (requires enable_queue=True in config)
client.start_retry_worker()

Rust API

Config

use hippius_hermes_core::Config;
use std::path::PathBuf;

let config = Config {
    node_secret_key_path: PathBuf::from("/etc/hermes/iroh.key"),
    ss58_address: "5GrwvaEF5zXb26...".to_string(),
    api_token: "sk-your-token".to_string(),
    storage_directory: PathBuf::from(".hermes_data"),
    rpc_url: "wss://rpc.hippius.network:443".to_string(),
    subnet_ids: vec![42, 69],
    enable_firewall: true,
    s3: Some(hippius_hermes_core::config::S3Config {
        bucket: "my-models".into(),
        access_key: "AKIA...".into(),
        secret_key: "AWS...".into(),
    }),
    pullweights_api_key: Some("sk-...".to_string()),
    skip_identity_verification: false,
    enable_queue: false,
    encryption_key_path: Some(PathBuf::from("./hermes_encryption.key")),
};

Client

use hippius_hermes_core::Client;

let client = Client::new(config).await?;

// Upload to S3 and send Pre-Signed URL to peer
client.send_file_via_s3("5Dest...", "./model.bin", None).await?;

// Download from a Pre-Signed URL
client.download_file_http("https://...", "./model.bin").await?;

// Update the memory firewall (drops unauthorized TLS at O(1))
let resolved = client.set_firewall_whitelist(vec!["5Grwv...".into(), "5FHne...".into()]).await;

// Global ACL
client.set_acl_blocklist(vec!["5Bad...".into()]).await;
client.set_acl_allowlist(vec!["5Good...".into()]).await;

// Start listener (control callback + optional data callback)
client.spawn_listener(
    |msg| println!("From {}: {}", msg.sender_ss58, msg.action),
    None::<fn(String, String, String, u64)>,
);

// Direct P2P file transfer
let filename = client
    .send_file_unencrypted(dest_ss58, file_path, None)
    .await?;

// Start listener with data callback
client.spawn_listener(
    |msg| println!("Control: {}", msg.action),
    Some(|sender: String, filename: String, path: String, size: u64| {
        println!("File: {} from {}", filename, sender);
    }),
);

// Send E2E encrypted message (NaCl SealedBox)
client.send_message_encrypted("5Dest...", "encrypted_message", b"secret payload".to_vec(), None).await?;

// Start background retry worker (requires enable_queue in config)
client.spawn_retry_worker();

ALPN Utilities

use hippius_hermes_core::network::node::{
    CROSS_SUBNET_ALPN, subnet_alpn, build_alpn_list
};

// Cross-subnet constant
assert_eq!(CROSS_SUBNET_ALPN, b"hippius-hermes/1");

// Build per-subnet ALPN
let alpn = subnet_alpn(42);
assert_eq!(alpn, b"hippius-hermes/subnet/42");

// Build full ALPN list from config
let alpns = build_alpn_list(&[42, 69]);
// Returns: ["hippius-hermes/1", "hippius-hermes/data/1", "hippius-hermes/subnet/42", "hippius-hermes/subnet/69"]

CLI Reference

Hermes provides a native Rust CLI for generating deterministic keys, running a listener daemon, and testing direct P2P transfers from the terminal.

1. Key Generation

Generate a secure, random Iroh secret key (NodeId) for your Hermes installation:

cargo run --bin hippius-hermes-cli -- keygen \
    --out-path examples/iroh.key

2. Backup / Reveal NodeId

Read an existing Iroh secret key from disk and print its corresponding public NodeId. Send this NodeId to peers so they can resolve your address offline.

cargo run --bin hippius-hermes-cli -- backup-key \
    --key-path examples/iroh.key

3. Start Listener

Start the Hermes daemon to listen for cross-subnet control messages and direct P2P file transfers:

cargo run --bin hippius-hermes-cli -- --config examples/hermes_config.json listen

4. Send File Directly

Push a file directly to a peer over QUIC (bypassing the Sync-Engine). For offline testing without the node tracker chain, explicitly provide the receiver's NodeId.

cargo run --bin hippius-hermes-cli -- --config examples/sender_config.json send-direct \
    --dest-ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
    --peer-node-id <RECEIVER_NODE_ID> \
    --file-path ./model.bin

5. Send Encrypted Message

Send an E2E encrypted message using NaCl SealedBox. The recipient's encryption key is resolved from the on-chain AccountProfile pallet.

cargo run --bin hippius-hermes-cli -- --config examples/sender_config.json send-encrypted \
    --dest-ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
    --message "top secret payload"

6. Send via S3

Upload a file to S3, generate a pre-signed URL, and send it to the peer over QUIC.

cargo run --bin hippius-hermes-cli -- --config examples/sender_config.json send-via-s3 \
    --dest-ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
    --file-path ./large_model.bin

7. Upload to S3

Upload a file directly to your configured S3 bucket without sending it to a peer.

cargo run --bin hippius-hermes-cli -- --config examples/sender_config.json upload-s3 \
    --file-path ./model.bin

8. Download from URL

Download a file from a pre-signed URL (or any HTTP URL) to a local path.

cargo run --bin hippius-hermes-cli -- download-url \
    --url "https://s3.example.com/model.bin?X-Amz-..." \
    --dest-path ./model.bin

9. Register Node

Register your node's Iroh NodeId and encryption public key on the Hippius blockchain.

cargo run --bin hippius-hermes-cli -- --config examples/hermes_config.json register \
    --ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY

10. Push Model

Push a local model directory to the PullWeights registry.

cargo run --bin hippius-hermes-cli -- --config examples/hermes_config.json push-model \
    --org my-org \
    --model my-llm \
    --file-path ./model.safetensors

11. Pull Model

Pull a model from the PullWeights registry to a local directory.

cargo run --bin hippius-hermes-cli -- pull-model \
    --org my-org \
    --model my-llm \
    --tag v1.0.0 \
    --download-dir /mnt/models

Example: Single Subnet Node

A miner that only participates in subnet 42:

import asyncio
from hermes import Config, HermesClient

async def main():
    config = Config(
        node_secret_key_path="/etc/hermes/iroh.key",
        ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
        api_token="sk-your-token",
        storage_directory=".hermes_data",
        subnet_ids=[42]  # Only accept subnet-42 and cross-subnet traffic
    )

    client = await HermesClient.create(config)
    # Node now accepts: hippius-hermes/1 + hippius-hermes/subnet/42

    def on_message(action, sender_ss58, payload):
        print(f"[Subnet 42] {sender_ss58}: {action}")

    client.start_listener(on_message)
    client.start_retry_worker()

    # Keep alive
    while True:
        await asyncio.sleep(3600)

if __name__ == "__main__":
    asyncio.run(main())

Example: Multi-Subnet Validator

A validator participating in subnets 1, 42, and 69 simultaneously. Incoming connections on any of these subnet-specific protocols are accepted.

import asyncio
from hermes import Config, HermesClient

async def main():
    config = Config(
        node_secret_key_path="/etc/hermes/iroh.key",
        ss58_address="5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
        api_token="sk-validator-token",
        storage_directory="/var/hermes",
        subnet_ids=[1, 42, 69]  # Accept traffic from all three subnets
    )

    client = await HermesClient.create(config)
    # Node accepts: hippius-hermes/1, hippius-hermes/subnet/1, hippius-hermes/subnet/42, hippius-hermes/subnet/69

    def handle_message(action, sender_ss58, payload):
        print(f"[Multi-Subnet Validator] {action} from {sender_ss58}")
        # Route based on action or parse payload metadata
        if action == "process_data_unencrypted_store":
            import json
            meta = json.loads(payload)
            print(f"  HCFS Hash: {meta['hash']}")

    client.start_listener(handle_message)
    client.start_retry_worker()

    while True:
        await asyncio.sleep(3600)

if __name__ == "__main__":
    asyncio.run(main())

Example: Cross-Subnet Communication

Two nodes on different subnets communicating via the universal hippius-hermes/1 protocol. This works regardless of subnet_ids configuration.

import asyncio
from hermes import Config, HermesClient

async def main():
    # This node is on subnet 42 but needs to talk to subnet 69
    config = Config(
        node_secret_key_path="/etc/hermes/iroh.key",
        ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
        api_token="sk-your-token",
        storage_directory=".hermes_data",
        subnet_ids=[42]
    )

    client = await HermesClient.create(config)

    # send_file_unencrypted uses hippius-hermes/data/1 (direct P2P QUIC).
    # The receiver accepts hippius-hermes/1 regardless of their subnet_ids config.
    dest_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
    filename = await client.send_file_unencrypted(
        dest_ss58,
        "./gradients.safetensors"
    )
    print(f"Sent cross-subnet! File: {filename}")

if __name__ == "__main__":
    asyncio.run(main())

Example: Rust Multi-Subnet Node

use hippius_hermes_core::{Client, Config};
use std::path::PathBuf;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let config = Config::from_file("hermes_config.json")?;
    // Or construct manually:
    // let config = Config {
    //     node_secret_key_path: PathBuf::from("/etc/hermes/iroh.key"),
    //     ss58_address: "5GrwvaEF...".to_string(),
    //     ...
    // };

    let client = Client::new(config).await?;
    // Node accepts: hippius-hermes/1, hippius-hermes/subnet/42, hippius-hermes/subnet/69

    client.spawn_listener(
        |msg| println!("[Hermes] {} from {}", msg.action, msg.sender_ss58),
        None::<fn(String, String, String, u64)>,
    );

    client.spawn_retry_worker();

    // Keep alive
    loop {
        tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
    }
}