Skip to main content

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 .torrent files 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 filesPath A — /publish — sign a manifest and publish directly
Downloads models from HuggingFacePath 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
tip

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:

FieldTypeRequiredNotes
typestringe.g. "llm", "embedding", "vision"
namestringHuman-readable model name
formatstringe.g. "gguf", "safetensors"
filesstring[]At least one file with a recognized extension
infohash_v1string*40 hex chars (SHA1 / BEP-3)
infohash_v2string*64 hex chars (SHA256 / BEP-52)
publisher_pubkeystringYour 64-char public key
sourceobjectAny key/value metadata you want indexed
timestampint64Unix seconds — must be within 7 days and not more than 5 minutes in the future
signaturestring128 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)
info

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

  1. A .torrent file created from the downloaded model files
  2. The HuggingFace model_id (e.g., "mistralai/Mistral-7B-Instruct-v0.3")
  3. The revision (commit SHA or branch)
  4. A magnet link for the torrent
  5. 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:

FieldTypeRequiredNotes
torrentfile (binary).torrent file, BEP-3 or BEP-52
model_idstringHuggingFace model ID
revisionstringCommit SHA or branch name
infohashstring40 hex chars (SHA1) — must match the torrent file
publisher_pubkeystringYour 64-char public key
publisher_sigstring128-char hex signature
magnetstringMust start with magnet:?
source_urlstringOrigin URL of the model
local_hashstringSHA256 hex of the torrent file bytes
timestampstringRFC3339 — within ±5 min of server time
display_namestringCustom display name for the publisher
quantizationstringe.g., Q4_K_M, Q8_0
model_size_bytesstringTotal 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.

warning

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.

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:

FieldTypeNotes
infohashstringUse for GET /model/{infohash} and /download/{infohash}
namestringCanonical model name
typestringModel type
formatstringFile format
scorefloatTrust/visibility score (0–1)
magnetstringMagnet link — hand directly to your BT client
torrent_urlstringPath to download the .torrent file
model_idstringHuggingFace model ID (if indexed via /ingest)
licensestringSPDX license identifier
model_size_bytesintTotal size in bytes
quantizationstringe.g., Q4_K_M
created_atstringISO8601 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
tip

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:

FieldTypeRequiredNotes
pubkeystringYour 64-char public key
display_namestringShown in search results and model pages
descriptionstringBio or tool description
websitestringProject URL
contactstringContact info — not exposed in public GET responses
timestampint64Must be newer than your last profile update

To fetch a publisher profile (yours or anyone else's):

GET /publisher/{pubkey}

LanguageEd25519
Pythoncryptography (Ed25519PrivateKey)
TypeScript/Nodecrypto.subtle (Node 15+) or @noble/ed25519
Gocrypto/ed25519 stdlib
Rusted25519-dalek
Javajava.security.KeyPairGenerator("Ed25519")

API Quick Reference

MethodPathPurpose
GET/healthService health check
POST/publishPublish 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-reportReport download outcome
POST/profilePublish/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"
}
CodeCauseFix
INVALID_JSONRequest body is not valid JSONCheck Content-Type and body encoding
MISSING_FIELDA required field is absentCheck the field table for the endpoint
VALIDATION_ERRORField value out of range or wrong formatCheck constraints (timestamp window, field lengths)
INVALID_PUBKEYPublic key is not 64 hex charsEnsure hex encoding, not base64
INVALID_SIGNATURESignature is not 128 hex chars or fails Ed25519 verifyVerify signing steps match the endpoint's method
INVALID_INFOHASHWrong length or non-hex charactersv1 = 40 hex, v2 = 64 hex
INFOHASH_MISMATCHSubmitted infohash doesn't match the torrent fileHash the final, written torrent file — not an in-memory buffer
INVALID_TORRENTTorrent file is not valid BEP-3 or BEP-52Validate with a torrent library before uploading
TORRENT_TOO_LARGETorrent file exceeds 5 MBReduce file count or use smaller piece sizes
INVALID_MAGNETMagnet URI doesn't start with magnet:?Regenerate from the torrent
STALE_PROFILEProfile timestamp is not newer than currentUse int(time.time()) — don't cache timestamps
UPSTREAM_ERRORRegistry couldn't reach HuggingFaceRetry after a moment; check model_id spelling
UPSTREAM_TIMEOUTHuggingFace metadata fetch timed outRetry
SERVICE_UNAVAILABLETemporary registry errorRetry; check /health if it persists
NOT_FOUNDResource doesn't existCheck 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.