Introducing ic-agent — an Internet Computer agent written in Motoko
Hi everyone,
We just published ic-agent to mops: a client for the Internet Computer’s HTTP interface,
written in Motoko. It does what agent-js or the Rust ic-agent do: sign requests, submit
calls, poll for results and read state. It verifies everything it receives:
certificates and query responses alike.
The library was funded by ICDevs.org (a 501c3 non-profit). If you find it useful, please
consider chipping in at https://icdevs.org/donation.html.
mops add ic-agent
Repo: https://github.com/icdevsorg/ic-agent-mo (release v0.2.0)
Why a Motoko agent?
Inside a canister you never need one — you call another canister with await. But Motoko
increasingly runs outside canisters: we’ve been supporting moxzi, a
self-hosted Motoko compiler with off-chain runtimes — a server (moxzid), a browser runtime,
and a game player — that run the same compiled actors you’d deploy on the IC. Those actors
need to talk to real canisters: read a ledger, post a high score, call a DAO. That’s an
agent’s job, and until now it meant leaving Motoko.
ic-agent runs anywhere an actor can make HTTPS outcalls (through the management canister’s
http_request, or a transport the host plugs in).
What it does
import Agent "mo:ic-agent";
import Identity "mo:ic-agent/Identity";
let agent = Agent.Agent(Agent.defaults("https://icp-api.io", #anonymous));
let ledger = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai");
switch (await* agent.queryCall(ledger, "icrc1_name", to_candid())) {
case (#ok reply) { let ?name : ?Text = from_candid(reply) else return; /* "Internet Computer" */ };
case (#err e) { /* typed error */ };
};
-
Calls:
queryCall,call,readState,status— andcallsplit into its two
halves,submit+poll.submitreturns the request id immediately andpollis one
read_stateround trip, so code that must not block (a game loop, a scheduler) submits on
one tick and polls on later ones.calluses the synchronous v4 call endpoint and only
falls back to polling when the replica answers 202. -
Identities: anonymous, ed25519, secp256k1, any
Signerthe host supplies, and
Internet Identity delegation chains. Principals are the standard self-authenticating
ones. -
Everything verified, by default. Certificates (from
call,poll,readStateand the
canister-info reads) are checked for their BLS signature, a delegation scoped to the
canister or subnet you asked about, a well-formed tree, and a fresh/time. Query
replies and query rejections are checked against the node signatures they carry,
using node keys from a verified/subnetcertificate.#skipexists, and prints a loud
warning on every response it skips. -
The rest of the HTTPS interface: v3 query, v3 read_state and v4 synchronous call, with a
visible fallback for older gateways; subnet-scoped read_state, query and call; management
canister calls routed to the right effective canister id; certifiedmodule_hash,
controllersandmetadata;sender_info; and helpers to verify certified data and
canister signatures, which is how Internet Identity signs. -
Typed façades: with the moxzi CLI,
moxzi agent-bindings ledger.did --name Ledger
generates a Motoko module with every type of the interface and a proxy object whose methods
encode arguments, go out as queries or calls as the.didsays, and decode the reply — you
never touch Candid:let ledger = Ledger.connectText(agent, "ryjl3-tyaaa-aaaaa-aaaba-cai"); switch (await* ledger.icrc1_balance_of({ owner; subaccount = null })) { case (#ok n) …; case (#err e) … };
Performance
Verification is the expensive part of an agent. ic-agent uses our
bls12-381 library for certificates and a small Ed25519 verifier
of its own (RFC 8032 signing and verification) for node signatures and ed25519 identities. Measured on moxzid:
| Step | Wasm instructions | How often |
|---|---|---|
| One certificate (one BLS signature) | 1.44 billion | per certificate; two under a delegation |
| A delegated subnet’s node keys | 2.96 billion | once per subnet per 5 minutes |
| One query response’s node signature | 155 million | per query |
The IC allows 40 billion instructions per update message and 5 billion per query, so every
step fits in a single message.
How it’s tested
- Known-answer tests, built by both
mocand moxzi with identical results: request ids
from the interface spec and agent-js, a signed envelope byte for byte, ed25519 and
secp256k1 keys, principals and signatures, hash trees, and BLS hash-to-curve vectors. - The spec against real mainnet data: captured query responses and certificates from the
ICP ledger (root subnet), ckBTC (a delegated subnet) and an Internet Identity rejection.
Their node signatures must verify. Tampered replies, wrong request ids, other subnets’
keys, stale timestamps, out-of-scope delegations and a wrong root key must all fail. 64
Ed25519 keys and signatures made by OpenSSL must be reproduced byte for byte, verify, and
fail when altered. - Live against PocketIC, with an NNS subnet plus an application subnet so every answer
comes under a real delegation. It covers verified queries and rejections, update calls,
submit plus poll, canister info, management calls, a canister signature and certified data
issued by a real canister.agent-jsreads the same canister as an independent oracle. - Live against mainnet: verified queries and certified reads on the root subnet and on a
delegated subnet.
Limits (0.2.0)
- No update calls on mainnet in CI. Update calls are proven against PocketIC; the mainnet
tests are read-only and anonymous. - No browser transport yet. Use HTTPS outcalls or a transport the host supplies.
- No native P-256 or WebAuthn signing. A host can supply those as a
Signer. - The subnet call endpoint is untested live. PocketIC’s gateway does not serve it, and it
only creates canisters. - secp256k1 signing builds its generator table once per identity, so keep the identity around.
If you used 0.1.0
Upgrade. 0.1.0 trusted query responses without checking their node signatures, and its
ed25519 identities sent an invalid signature on about 1 request in 24. That second problem
came from the ed25519 0.1.0 mops package: shifting zero right returned 255, which breaks
signing whenever the nonce has a zero top byte. We sent the one-character fix upstream
(imsk17/ed25519#1). If you depend on that package
directly, your signatures are affected too.
0.2.0 has two small breaking changes: Config has two new fields, so build it with
{ Agent.defaults(host, identity) with … }, and poll takes the request’s effective canister
id, which is the same canister as before except for calls to aaaaa-aa.
Issues and PRs welcome. If you’re running Motoko off-chain and want your actors to talk to the
IC, we’d love to hear what you build.