Package Exports
- @iicp/client
- @iicp/client/cli
Readme
iicp-client · TypeScript / JavaScript SDK
Use the open AI mesh from your TypeScript or JavaScript app. Install the client, send an intent, and get a routed response from an IICP node.
You do not need to run a node to try the client path. Consume first, provide later.
Works in Node.js ≥ 18, Deno, Bun, and modern browsers with the native Fetch API.
urn:iicp:intent:llm:chat:v1 → discover → select → submitInstall
npm install @iicp/client@latest
# yarn add @iicp/client@latest
# pnpm add @iicp/client@latestOne-line test
npm install -g @iicp/client@latest
iicp-node query "Hello, mesh."What good looks like:
iicp-node --help # shows query, serve, proxy, mcp-gateway, credits, ...
which iicp-node # points to your Node/npm environment
iicp-node --version # prints iicp-node 0.7.81 or newerThe query command contacts the public directory, discovers a matching live node, routes your prompt, and prints the response. No account, API key, or local node is required for this consumer path.
Privacy note: the selected remote node can read the prompt it executes. IICP-CX keeps key-ready transport/relay paths confidential, but it is not executor-blind inference. For sensitive data, use local/browser inference or a fail-closed routing profile.
Use from TypeScript
import { IicpClient } from "@iicp/client";
const reply = await new IicpClient().chat([
{ role: "user", content: "Hello, mesh." },
]);
console.log(reply.choices[0].message.content);Do I need to run a node?
No. Running a node is only needed when you want to provide compute or tools to the mesh. Start as a client; run a node later when you want to contribute.
Routing policy profiles
The client applies routing policy after prompt-free discovery and before the prompt is sent. Defaults stay adoption-friendly but keyless plaintext is still refused.
iicp-node query "Hello" --routing-profile standard # default encrypted mesh
iicp-node query "Secret" --routing-profile sensitive # fail closed: no remote executor
iicp-node query "Hello" --routing-profile eu-restricted # EU/EEA regions only
iicp-node query "Hello" --routing-profile strict-policy # requires no-retention manifestconst reply = await new IicpClient().chat(
[{ role: "user", content: "Hello" }],
{ routing_policy: { profile: "eu_restricted" } },
);Migrate from existing AI tools
Direct call:
// Before: call one vendor endpoint directly.
// After: ask IICP to discover and route by capability.
const reply = await new IicpClient().chat([
{ role: "user", content: "Summarize this document." },
]);Existing OpenAI-compatible tools:
npm install -g @iicp/client@latest
iicp-node proxy
export OPENAI_BASE_URL=http://127.0.0.1:9483/v1Then point LangChain, Cursor, liteLLM or another OpenAI-compatible tool at that base URL. Full guide: https://iicp.network/docs/proxy
Provider upgrade note
Upgrade note (0.7.80) — clients now support remote-routing policy profiles that can refuse unsafe remote dispatch before any prompt leaves the caller. Use
--routing-profile sensitivefor fail-closed no-remote behavior,eu-restrictedfor EU/EEA node filtering, orstrict-policywhen a no-retention node policy manifest is required.Existing provider reachability fixes from 0.7.79 remain intact.
Keeping provider nodes current
Provider nodes run an hourly official-registry check by default
(IICP_AUTO_UPDATE=1, IICP_AUTO_UPDATE_INTERVAL_S=3600; minimum 300s).
When npm publishes a newer stable release, serve runs
npm install -g @iicp/client@latest and re-execs the node so identity and cached
node tokens are preserved.
If a node is older than 0.7.67, perform one manual upgrade/restart first,
especially for Dockerized Python or TypeScript providers: early updater wiring
did not reliably cover every normal serve path. For Docker, use a Compose
restart: unless-stopped policy (or docker run --restart unless-stopped) so
0.7.80 can intentionally exit from a confirmed tunnel-dead state and let Docker
bring it back cleanly.
Upgrade note (0.5.3) — if you operate a node and use the native IICP TCP transport on port 9484, upgrade to
^0.5.3. Releases 0.5.0–0.5.2 emitted a non-standard CBOR dialect that does not interoperate with the Python or Rust SDK on the binary transport. The HTTP/v1/taskpath is unaffected. SeeCHANGELOG.mdfor details.
Architecture — consumer or provider?
This SDK covers both sides of the IICP protocol:
| Role | What you do | Class |
|---|---|---|
| Consumer | Send AI tasks to the mesh; discover and submit | IicpClient |
| Provider | Run a node, register with the directory, serve tasks | IicpNode |
Consumer and provider can run in the same process. For production provider nodes backed by Ollama/vLLM, see iicp.network/docs/node-setup.
Library quickstart
import { IicpClient } from "@iicp/client";
// directory_url defaults to https://iicp.network/api
const client = new IicpClient();
// chat() discovers, selects the best node, and submits in one call
const response = await client.chat(
[{ role: "user", content: "Hello from IICP!" }],
);
console.log(response.choices[0].message.content);For more control over node selection:
const nodes = await client.discover("urn:iicp:intent:llm:chat:v1");
if (!nodes.length) throw new Error("No nodes available");
const result = await client.submit({
intent: "urn:iicp:intent:llm:chat:v1",
payload: { messages: [{ role: "user", content: "Hello!" }] },
});Use as a local API proxy (OpenAI / Ollama / Anthropic compat)
Run a local gateway that speaks the OpenAI, Ollama, and Anthropic HTTP APIs and routes every request across the IICP mesh — point any tool you already use at it, no code changes.
npm i -g @iicp/client
iicp-node proxy # → http://127.0.0.1:9483
export OPENAI_BASE_URL=http://127.0.0.1:9483/v1 # OpenAI SDK / LangChain / Cursor / liteLLM
export OLLAMA_HOST=http://127.0.0.1:9483 # Open WebUI / Continue.dev / aider / JanLoopback-only consumer (never registers with the directory), built on Node's http (no
extra runtime dependency). Override the port with --port / IICP_PROXY_PORT; co-host
next to a node with iicp-node serve --with-proxy. Every response carries
Server: iicp-proxy. Full guide: https://iicp.network/docs/proxy
Configuration
import { IicpClient } from "@iicp/client";
const client = new IicpClient({
directory_url : "https://iicp.network/api", // IICP directory
timeout_ms : 30_000, // max 120 000 (SDK-04)
region : "eu-central", // prefer nodes in region
api_token : "your-token", // optional auth token
});| Option | Default | Description |
|---|---|---|
directory_url |
"https://iicp.network/api" |
IICP directory endpoint |
timeout_ms |
30000 |
Request timeout — max 120 000 ms |
region |
undefined |
Preferred node region |
api_token |
undefined |
Bearer token for authenticated nodes |
routing_epsilon |
0.05 |
ε-greedy exploration probability — with this probability a random node is selected instead of the top-ranked one, promoting discovery of new providers; 0 disables; override with IICP_ROUTING_EPSILON |
routing_policy |
{ profile: "standard" } |
Pre-dispatch remote-routing gate; use sensitive, eu_restricted, strict_policy, or an explicit debug override for special cases |
Discover options
const nodes = await client.discover("urn:iicp:intent:llm:chat:v1", {
region : "eu-central", // prefer nodes in this region
qos : "interactive", // quality-of-service hint
min_reputation: 0.7, // floor on directory reputation
limit : 5, // capped at 50
});Error handling
import { IicpClient, IicpError } from "@iicp/client";
const client = new IicpClient();
try {
const response = await client.chat([{ role: "user", content: "hi" }]);
} catch (e) {
if (e instanceof IicpError) {
console.error(`[${e.code}] ${e.message} (HTTP ${e.status_code})`);
}
}Error codes match the IICP error reference — e.g. task_timeout, capacity_exceeded, no_nodes_available.
Serving as a provider node
import { IicpNode } from "@iicp/client";
const node = new IicpNode({
nodeId : "my-node-001",
endpoint: "http://my.public.host:8020",
intent : "urn:iicp:intent:llm:chat:v1",
model : "llama3:8b",
});
const token = await node.register();
const stop = node.serve(async (task) => {
// Return the inner result value — serve() wraps it in {result: ...}
return { choices: [{ message: { role: "assistant", content: "Hello!" } }] };
}, { port: 8020, nodeToken: token });
process.on("SIGINT", () => { stop(); });Run a node from the CLI
Installing the package puts an iicp-node binary on your PATH. The CLI wires
up NAT detection, registration, heartbeats and a backend handler for you:
# Ollama on the default port — only --model is required
iicp-node serve --model qwen2.5:0.5b
# An OpenAI-compatible backend (LM Studio, vLLM, hosted gateway)
iicp-node serve \
--model phi3:mini \
--backend-url http://localhost:1234 \
--backend-api-key "$BACKEND_API_KEY"Every flag has an environment-variable equivalent (shown by iicp-node --help):
--model / IICP_BACKEND_MODEL, --backend-url / IICP_BACKEND_URL,
--backend-type / IICP_BACKEND_TYPE, --backend-api-key / IICP_BACKEND_API_KEY,
--directory-url / IICP_DIRECTORY_URL (default https://iicp.network/api),
--port / IICP_PORT (default 9484).
Backend types
--backend-type (or the getBackendHandler(type, opts) factory) selects how the
node talks to your model server. All backends present an identical llm:chat:v1
surface to IICP clients:
--backend-type |
Handler export | Speaks | Default base URL |
|---|---|---|---|
openai_compat (default) |
openaiCompatHandler |
OpenAI /v1/* dialect (Ollama, LM Studio, OpenAI) |
http://localhost:11434/v1 |
vllm |
vllmHandler |
OpenAI dialect, tuned for vLLM | http://localhost:8000/v1 |
llamacpp |
llamacppHandler |
OpenAI dialect, tuned for llama.cpp server | http://localhost:8080/v1 |
anthropic |
anthropicHandler |
Anthropic Messages API (POST /v1/messages) — first-class Claude |
https://api.anthropic.com/v1 |
Native Anthropic backend (v0.7.35+)
The anthropic backend speaks the Anthropic Messages API directly rather than
going through the OpenAI-compat shim. It translates an IICP llm:chat:v1 task into a
Messages request — hoisting system messages to the top-level system field, setting
the required max_tokens (default 4096), mapping image_url content parts to
Anthropic image blocks — and maps the response back to the OpenAI chat-completion
shape, so a Claude-backed node is indistinguishable from an Ollama/vLLM node to any
client. The API key is sent as the x-api-key header (not a Bearer token).
# Serve Claude to the mesh. --backend-type anthropic defaults --backend-url to
# https://api.anthropic.com, so you only supply the key and model.
iicp-node serve \
--backend-type anthropic \
--backend-api-key "$ANTHROPIC_API_KEY" \
--model claude-opus-4-8In code:
import { IicpNode, anthropicHandler } from "@iicp/client";
const node = new IicpNode({
nodeId : "claude-node-001",
endpoint: "http://my.public.host:8020",
intent : "urn:iicp:intent:llm:chat:v1",
model : "claude-opus-4-8",
});
const handler = anthropicHandler({
apiKey: process.env.ANTHROPIC_API_KEY,
model : "claude-opus-4-8",
// baseUrl defaults to https://api.anthropic.com/v1
});
const token = await node.register();
node.serve(handler, { port: 8020, nodeToken: token });Multimodal capabilities — vision and audio
When a node registers, the SDK derives the input_modalities it advertises from the
model name (buildCapabilities / modalitiesForModel). Every model serves text;
in addition:
- image (vision) — model name contains
vl,vision,llava, oromni - audio — model name contains
audio,voxtral, oromni
A node serving several models advertises one capability entry per
(intent, input_modalities) group, so consumers can pick the right model for a
multimodal task via discover.
Listen port — default 9484, auto-increment (v0.7.5+)
The official IICP port 9484 is the default listen port (IICP_PORT, --port).
The iicp-node CLI auto-increments to the next free port when 9484 is already in
use, so several nodes on one host don't need hand-picked ports — first binds 9484,
second 9485, third 9486, etc. Each node gets its own port (hence its own NAT
pinhole); multiple models on one node share that single port. Auto-increment is
skipped when you pass an explicit --public-endpoint. node.serve(handler, { port })
uses the port you give it as-is (no auto-increment at the library level).
NAT traversal — automatic (v0.7.3+)
Since v0.7.3, NAT detection runs automatically on every node startup — no flags needed. The SDK tries each path in order and picks the best one for your network:
| Tier | When | What happens |
|---|---|---|
| 0 | VPS/cloud (public IP on NIC) or IICP_PUBLIC_ENDPOINT set |
Registers directly |
| 1a | Home router with UPnP, no CGNAT | Port-forward via UPnP → register WAN IP |
| 1b | CGNAT + IPv6 + AddPinhole works | Registers IPv6 with firewall rule |
| 1c | CGNAT + IPv6 + AddPinhole fails (e.g. FRITZ!Box error 606) | Registers IPv6 + logs guidance |
| 3 | CGNAT + no usable IPv6 | Opens a Quick Tunnel if available → otherwise auto-elects relay |
| 4 | Nothing worked | Serves locally with operator guidance |
Environment-specific behaviour
Docker bridge (-p 8020:8020) — UPnP is skipped (it would reach Docker NAT, not your
home router). The official image includes cloudflared, so without a public endpoint it
tries a zero-account Quick Tunnel, then relay. The image also sets IICP_SUPERVISED=1,
so with Docker restart policy enabled a confirmed tunnel-dead state exits visibly and
lets Docker restart the node. For stable direct hosting, set IICP_PUBLIC_ENDPOINT in
docker-compose.yml:
restart: unless-stopped
environment:
IICP_PUBLIC_ENDPOINT: "http://your-host-ip:8020"
IICP_BACKEND_URL: "http://host.docker.internal:11434"Or run with --network host to let UPnP work as on bare metal.
Kubernetes — set IICP_PUBLIC_ENDPOINT to the LoadBalancer / NodePort IP.
CGNAT + no IPv6 → Quick Tunnel, then relay:
[iicp-node] NAT tier=3: opening Quick Tunnel...
[iicp-node] no tunnel available — auto-electing relay from directory...
[iicp-node] auto-elected relay: relay.example.com:9485With cloudflared available, the node registers its own temporary HTTPS tunnel URL.
If that is unavailable, the node connects outbound to the elected relay and re-registers
automatically.
To use a specific relay: IICP_RELAY_WORKER_ENDPOINT=relay.example.com:9485.
Running a relay-capable node (relay operator)
const node = new IicpNode({
endpoint : "http://relay.example.com:8020",
intent : "urn:iicp:intent:llm:chat:v1",
relayCapable : true, // accept RELAY_BIND on TCP port 9485
relayAcceptPort: 9485,
enableMesh : true, // gossip relayCapable=true to peers
});Security note: relay bind authentication is pending (#510) — only run a relay accept port on networks you trust until the signed-bind mechanism ships.
Opt-out / override
IICP_AUTO_DETECT_NAT=false # disable detection entirely
IICP_PUBLIC_ENDPOINT=http://x.x.x.x:8020 # trust this endpoint
IICP_TUNNEL=0 # opt out of Quick Tunnel fallback
IICP_TUNNEL_CREATE_MIN_INTERVAL_S=120 # host-wide Quick Tunnel create pacing
IICP_TUNNEL_DEAD_POLICY=auto # auto|retry|exit|log-only (auto = supervised exit, manual retry)
IICP_SUPERVISED=1 # set by generated services/Docker so supervisors can restart
IICP_AUTO_UPDATE=1 # hourly provider self-update; set 0 to disable
IICP_AUTO_UPDATE_INTERVAL_S=3600 # update cadence in seconds; minimum 300
IICP_RELAY_WORKER_ENDPOINT=host:9485 # specific relay instead of auto-electOperator identity
Your operator identity is an ed25519 keypair — its public key is your operator_id (the
directory stores it as operator_pubkey). One identity spans every node you run: it binds them to
you (nodes show Operated by <your name> ✓), earns a
founder ordinal, and rolls each node's credits into one operator
wallet. Your display_name is the public, mutable handle; your contact stays local.
iicp-node init # create your key-backed identity (~/.iicp/operator.json)
iicp-node serve --node mynode # signs an operator→node delegation; binds the node to you
iicp-node operator rename "NewName" # change your public display_name (signed)
iicp-node operator encrypt # password-encrypt the secret at rest ($IICP_OPERATOR_PASSPHRASE)
iicp-node operator decrypt # remove at-rest encryptionThe key is the identity — whoever holds ~/.iicp/operator.json controls it (its nodes, ordinal,
and wallet); there is no central recovery. Back it up (encrypted), never commit or share it; lose it
and the identity, with its founder ordinal, is gone.
Full guide: iicp.network/docs/operator-identity
SDK conformance
| Rule | Description | Status |
|---|---|---|
| SDK-01 | discover → select → submit pipeline with node retry | ✓ |
| SDK-02 | task_id auto-generated (UUID v4) |
✓ |
| SDK-03 | Intent URN pattern validation | ✓ |
| SDK-04 | timeout_ms capped at 120 000 ms |
✓ |
| SDK-05 | Retry on 429 / 503 with exponential back-off | ✓ |
| SDK-06 | W3C traceparent propagation |
✓ |
Conformance tier: iicp:sdk:v1 (spec S.14) · Request a badge
Development
npm install # install deps
npm run typecheck # tsc strict
npm test # run the unit suite
npm run build # emit to dist/Links
- Protocol spec — full IICP specification
- Node setup guide — run your own node
- Error reference — all error codes
- iicp-client-python — Python SDK
- iicp-client-rust — Rust SDK
Apache 2.0 · iicp.network