Registry API Integration Guide
If you're building a BitTorrent client, a HuggingFace model manager, or anything in between — welcome. You've already done the hard part: you handle the actual model files. This guide shows you how to connect your tool to the Hali registry in a handful of API calls, giving your users a searchable, trust-ranked index of everything they publish and discover.
What you get by integrating:
- Your users' models become discoverable to everyone on the network
- Download success rates feed into a public trust score automatically
- Magnet links and
.torrentfiles are stored and served by the registry - No accounts, no OAuth, no approval process — identity is a cryptographic keypair
Which path is right for you?
There are two integration paths. Pick the one that matches what your tool already does:
| Your tool already does this… | Use this path |
|---|---|
Creates or manages .torrent files | Path A — /publish — sign a manifest and publish directly |
| Downloads models from HuggingFace | Path B — /ingest — upload the torrent file; registry handles the rest |
Both paths share the same identity model and discovery API. Read Publisher Identity first, then jump to your path.
Publisher Identity
Your publisher identity is a single Ed25519 keypair. The public key is your permanent identity on the network — no registration, no approval, no email. Generate it once, store the private key securely, and you're ready.
Public keys are 32 bytes (64 hex characters). Signatures are 64 bytes (128 hex characters).
Generating a Keypair
Python
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
privkey_hex = private_key.private_bytes_raw().hex() # store securely
pubkey_hex = public_key.public_bytes_raw().hex() # this is your public identity
print(f"pubkey: {pubkey_hex}")
print(f"privkey: {privkey_hex}")
TypeScript / Node.js
import { webcrypto } from "crypto";
const keypair = await webcrypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
const pubkeyBytes = await webcrypto.subtle.exportKey("raw", keypair.publicKey);
const privkeyBytes = await webcrypto.subtle.exportKey("pkcs8", keypair.privateKey);
const pubkeyHex = Buffer.from(pubkeyBytes).toString("hex");
// Store keypair.privateKey for signing — never log or expose it
console.log("pubkey:", pubkeyHex);
Go
import "crypto/ed25519"
pubkey, privkey, _ := ed25519.GenerateKey(nil)
pubkeyHex := hex.EncodeToString(pubkey) // your public identity
// store privkey (64 bytes) securely
Store your private key in your app's config directory, encrypted with the user's system keychain or a passphrase. The registry never sees it — all signing happens locally.
Path A — BitTorrent client integration
Build a manifest describing the model, sign it locally, and publish it. The registry validates the signature and indexes it immediately.
Step 1 — Build the Manifest
{
"type": "llm",
"name": "Mistral-7B-Instruct-v0.3",
"format": "gguf",
"files": ["mistral-7b-instruct-v0.3.Q4_K_M.gguf"],
"infohash_v1": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"publisher_pubkey": "your64hexcharpublickey...",
"source": {
"origin": "huggingface",
"model_id": "mistralai/Mistral-7B-Instruct-v0.3"
},
"timestamp": 1748000000,
"signature": ""
}
Manifest fields:
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | ✓ | e.g. "llm", "embedding", "vision" |
name | string | ✓ | Human-readable model name |
format | string | ✓ | e.g. "gguf", "safetensors" |
files | string[] | ✓ | At least one file with a recognized extension |
infohash_v1 | string | * | 40 hex chars (SHA1 / BEP-3) |
infohash_v2 | string | * | 64 hex chars (SHA256 / BEP-52) |
publisher_pubkey | string | ✓ | Your 64-char public key |
source | object | — | Any key/value metadata you want indexed |
timestamp | int64 | ✓ | Unix seconds — must be within 7 days and not more than 5 minutes in the future |
signature | string | ✓ | 128 hex chars — computed in Step 2 |
*At least one of infohash_v1 or infohash_v2 is required. Provide both if your client creates hybrid torrents.
Recognized file extensions: .gguf, .safetensors, .bin, .pt, .pth
Step 2 — Sign the manifest
Sign the manifest with your Ed25519 private key. The registry verifies the signature on receipt — no account login needed.
import json, hashlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
def sign_manifest(manifest: dict, privkey_hex: str) -> str:
private_key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(privkey_hex))
payload = {k: v for k, v in sorted(manifest.items()) if k != "signature"}
canonical = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
digest = hashlib.sha256(canonical).digest()
return private_key.sign(digest).hex()
Step 3 — Publish
import time
import requests
def publish_manifest(manifest: dict, privkey_hex: str, registry_url: str = "https://hali.network"):
manifest["timestamp"] = int(time.time())
manifest["signature"] = sign_manifest(manifest, privkey_hex)
resp = requests.post(
f"{registry_url}/publish",
json={"manifest": manifest},
timeout=10,
)
if resp.status_code == 201:
print("Published successfully!")
return True
err = resp.json()
print(f"Error [{err.get('code')}]: {err.get('error')}")
return False
manifest = {
"type": "llm",
"name": "Mistral-7B-Instruct-v0.3",
"format": "gguf",
"files": ["mistral-7b-instruct-v0.3.Q4_K_M.gguf"],
"infohash_v1": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"publisher_pubkey": YOUR_PUBKEY_HEX,
"source": {"origin": "huggingface", "model_id": "mistralai/Mistral-7B-Instruct-v0.3"},
}
publish_manifest(manifest, YOUR_PRIVKEY_HEX)
The registry keeps your model indexed under both its v1 and v2 infohash if you provide both. Use v2 (SHA256) as the canonical identity when available — it's collision-resistant and future-proof.
Path B — HuggingFace tool integration
This path is for tools that download models from HuggingFace. Upload the .torrent file and a few metadata fields; the registry fetches the canonical model name, license, and visibility from HuggingFace directly.
What you need
- A
.torrentfile created from the downloaded model files - The HuggingFace
model_id(e.g.,"mistralai/Mistral-7B-Instruct-v0.3") - The
revision(commit SHA or branch) - A magnet link for the torrent
- Your Ed25519 keypair
POST to /ingest
Send a multipart form request:
import datetime, hashlib, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
def ingest_model(
torrent_path: str,
model_id: str,
revision: str,
infohash: str,
magnet: str,
source_url: str,
pubkey_hex: str,
privkey_hex: str,
registry_url: str = "https://hali.network",
):
timestamp = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%S.%f000Z")
with open(torrent_path, "rb") as f:
torrent_bytes = f.read()
local_hash = hashlib.sha256(torrent_bytes).hexdigest()
payload = "\n".join([
model_id, revision, infohash.lower().strip(),
magnet, source_url, local_hash, timestamp, pubkey_hex.lower(),
]).encode()
sig = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(privkey_hex)).sign(payload).hex()
resp = requests.post(
f"{registry_url}/ingest",
data={
"model_id": model_id, "revision": revision,
"infohash": infohash, "publisher_pubkey": pubkey_hex,
"publisher_sig": sig, "magnet": magnet,
"source_url": source_url, "local_hash": local_hash, "timestamp": timestamp,
},
files={"torrent": ("model.torrent", torrent_bytes, "application/x-bittorrent")},
timeout=30,
)
if resp.status_code == 201:
result = resp.json()
print(f"Ingested! infohash={result['infohash']}")
return result
print(f"Error [{resp.json().get('code')}]: {resp.json().get('error')}")
/ingest fields:
| Field | Type | Required | Notes |
|---|---|---|---|
torrent | file (binary) | ✓ | .torrent file, BEP-3 or BEP-52 |
model_id | string | ✓ | HuggingFace model ID |
revision | string | ✓ | Commit SHA or branch name |
infohash | string | ✓ | 40 hex chars (SHA1) — must match the torrent file |
publisher_pubkey | string | ✓ | Your 64-char public key |
publisher_sig | string | ✓ | 128-char hex signature |
magnet | string | ✓ | Must start with magnet:? |
source_url | string | ✓ | Origin URL of the model |
local_hash | string | ✓ | SHA256 hex of the torrent file bytes |
timestamp | string | ✓ | RFC3339 — within ±5 min of server time |
display_name | string | — | Custom display name for the publisher |
quantization | string | — | e.g., Q4_K_M, Q8_0 |
model_size_bytes | string | — | Total size as a decimal integer string |
Successful response:
{
"success": true,
"infohash": "a1b2c3d4e5f6...",
"visibility": "public",
"download_url": "/model/a1b2c3d4e5f6..."
}
To check whether a model is already indexed before uploading:
HEAD /ingest/{infohash}
Returns 200 if already indexed, 404 if not. Useful for deduplication in CI pipelines.
The infohash you submit must exactly match what's encoded in the torrent file. The registry verifies this independently and rejects mismatches with INFOHASH_MISMATCH.
Discovering Models
Whether you're building a BT client or an HF tool, this is how you let users browse and download what's in the registry.
Search
GET /search?q=mistral&limit=20&offset=0
Returns an array of ranked results (score DESC, then newest first). Only status=active models above the visibility threshold appear.
import requests
def search_models(query: str, limit: int = 20, offset: int = 0,
registry_url: str = "https://hali.network"):
resp = requests.get(f"{registry_url}/search", params={
"q": query, "limit": limit, "offset": offset
})
return resp.json() # list of SearchResult objects
results = search_models("mistral 7b gguf")
for r in results:
print(f"{r['name']} score={r['score']:.2f} magnet={r['magnet'][:60]}...")
SearchResult fields:
| Field | Type | Notes |
|---|---|---|
infohash | string | Use for GET /model/{infohash} and /download/{infohash} |
name | string | Canonical model name |
type | string | Model type |
format | string | File format |
score | float | Trust/visibility score (0–1) |
magnet | string | Magnet link — hand directly to your BT client |
torrent_url | string | Path to download the .torrent file |
model_id | string | HuggingFace model ID (if indexed via /ingest) |
license | string | SPDX license identifier |
model_size_bytes | int | Total size in bytes |
quantization | string | e.g., Q4_K_M |
created_at | string | ISO8601 timestamp |
Get Full Model Details
GET /model/{infohash}
Accepts both 40-char v1 and 64-char v2 infohashes. Returns everything in SearchResult plus publisher details and the full file list.
def get_model(infohash: str, registry_url: str = "https://hali.network"):
resp = requests.get(f"{registry_url}/model/{infohash}")
if resp.status_code == 404:
return None
return resp.json()
model = get_model("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2")
print(f"Files: {model['files']}")
print(f"Publisher reputation: {model['publisher']['reputation']:.2f}")
Download the Torrent File
GET /download/{infohash}
Returns the raw .torrent binary. You can hand this directly to any BitTorrent client:
def download_torrent(infohash: str, dest_path: str,
registry_url: str = "https://hali.network"):
resp = requests.get(f"{registry_url}/download/{infohash}", stream=True)
if resp.status_code == 200:
with open(dest_path, "wb") as f:
f.write(resp.content)
return True
return False
Discovery flow:
Reporting Downloads
This is one of the highest-value integrations for your users. When a download completes — or fails — send a quick report. It feeds the download_success_rate component of the scoring algorithm, which directly affects how prominently a model appears in search.
POST /download-report
Content-Type: application/json
{
"infohash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"completed": true,
"reporter_pubkey": "your64hexcharpublickey..."
}
def report_download(infohash: str, completed: bool,
reporter_pubkey: str = "",
registry_url: str = "https://hali.network"):
payload = {"infohash": infohash, "completed": completed}
if reporter_pubkey:
payload["reporter_pubkey"] = reporter_pubkey
resp = requests.post(f"{registry_url}/download-report", json=payload, timeout=5)
return resp.status_code == 201
Call this both on successful completion ("completed": true) and on failure ("completed": false). Failed downloads are just as useful — they warn future users about seedless torrents faster.
Publisher Profiles
Your publisher profile is optional but worth setting up. It gives you a human-readable name in search results and raises your reputation score, which carries over to all your published models.
POST /profile
Content-Type: application/json
import time
def publish_profile(
display_name: str,
description: str = "",
website: str = "",
pubkey_hex: str = "",
privkey_hex: str = "",
registry_url: str = "https://hali.network",
):
profile = {
"pubkey": pubkey_hex,
"display_name": display_name,
"timestamp": int(time.time()),
}
if description:
profile["description"] = description
if website:
profile["website"] = website
# Sign using canonical JSON → SHA256 → Ed25519 (same as manifest signing)
signature = sign_manifest(profile, privkey_hex)
resp = requests.post(
f"{registry_url}/profile",
json={"profile": profile, "signature": signature},
timeout=10,
)
return resp.status_code == 200
Profile fields:
| Field | Type | Required | Notes |
|---|---|---|---|
pubkey | string | ✓ | Your 64-char public key |
display_name | string | ✓ | Shown in search results and model pages |
description | string | — | Bio or tool description |
website | string | — | Project URL |
contact | string | — | Contact info — not exposed in public GET responses |
timestamp | int64 | ✓ | Must be newer than your last profile update |
To fetch a publisher profile (yours or anyone else's):
GET /publisher/{pubkey}
Recommended libraries
| Language | Ed25519 |
|---|---|
| Python | cryptography (Ed25519PrivateKey) |
| TypeScript/Node | crypto.subtle (Node 15+) or @noble/ed25519 |
| Go | crypto/ed25519 stdlib |
| Rust | ed25519-dalek |
| Java | java.security.KeyPairGenerator("Ed25519") |
API Quick Reference
| Method | Path | Purpose |
|---|---|---|
GET | /health | Service health check |
POST | /publish | Publish a signed manifest |
GET | /search?q=&limit=&offset= | Full-text search |
GET | /model/{infohash} | Full model details |
GET | /download/{infohash} | Download raw .torrent file |
POST | /download-report | Report download outcome |
POST | /profile | Publish/update publisher profile |
GET | /publisher/{pubkey} | Publisher profile + model list |
The {infohash} parameter accepts either a 40-char v1 (SHA1) or 64-char v2 (SHA256) infohash.
Error Codes
All error responses use this shape:
{
"error": "Human-readable message",
"code": "MACHINE_READABLE_CODE"
}
| Code | Cause | Fix |
|---|---|---|
INVALID_JSON | Request body is not valid JSON | Check Content-Type and body encoding |
MISSING_FIELD | A required field is absent | Check the field table for the endpoint |
VALIDATION_ERROR | Field value out of range or wrong format | Check constraints (timestamp window, field lengths) |
INVALID_PUBKEY | Public key is not 64 hex chars | Ensure hex encoding, not base64 |
INVALID_SIGNATURE | Signature is not 128 hex chars or fails Ed25519 verify | Verify signing steps match the endpoint's method |
INVALID_INFOHASH | Wrong length or non-hex characters | v1 = 40 hex, v2 = 64 hex |
INFOHASH_MISMATCH | Submitted infohash doesn't match the torrent file | Hash the final, written torrent file — not an in-memory buffer |
INVALID_TORRENT | Torrent file is not valid BEP-3 or BEP-52 | Validate with a torrent library before uploading |
TORRENT_TOO_LARGE | Torrent file exceeds 5 MB | Reduce file count or use smaller piece sizes |
INVALID_MAGNET | Magnet URI doesn't start with magnet:? | Regenerate from the torrent |
STALE_PROFILE | Profile timestamp is not newer than current | Use int(time.time()) — don't cache timestamps |
UPSTREAM_ERROR | Registry couldn't reach HuggingFace | Retry after a moment; check model_id spelling |
UPSTREAM_TIMEOUT | HuggingFace metadata fetch timed out | Retry |
SERVICE_UNAVAILABLE | Temporary registry error | Retry; check /health if it persists |
NOT_FOUND | Resource doesn't exist | Check the infohash or pubkey |
Rate Limits
All endpoints are rate-limited per source IP. If you're building a CI pipeline that publishes many models, stagger your requests. Rate-limited responses return HTTP 429 with {"error": "...", "code": "RATE_LIMITED"}.
Health Check
Before your first request in a session, you can check registry health:
GET /health
Responses:
{ "status": "ok" }
{ "status": "degraded" }
{ "status": "error" }
degraded means the registry is serving requests but some background processes are behind. Only error (HTTP 503) means you should wait before publishing.
Happy building. If something doesn't work as documented, open an issue — the spec and the implementation should always agree.