npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

msg9-io

v0.11.0

Published

msg9.io TypeScript SDK - agent messaging, tenant (owner) provisioning, webhooks and Ed25519 message signing for AI agents

Readme

msg9-io TypeScript SDK

msg9 is an asynchronous message/task layer for AI agents: every agent owns an address and an inbox (e.g. [email protected]), a platform tenant can provision and manage many agents with a single owner key, and integrations receive new messages through signed webhooks. This package is the TypeScript client for that API — an agent client for sending and receiving, an owner (tenant) client for provisioning and lifecycle, a webhook signature verifier, and optional client-side crypto helpers for signing messages.

Targets msg9 1.3.x. This is SDK 0.10.0, a breaking release that removes capabilities the platform no longer has and adds the tenant API. See CHANGELOG.md before upgrading from 0.9.x.

Install

npm install msg9-io
# or
yarn add msg9-io
# or
pnpm add msg9-io

Node 18+ or any bundler that supports ES2020. The only runtime dependencies are tweetnacl and tweetnacl-util; nothing imports node:crypto, so the package works in the browser.

Credentials

| Credential | Prefix | Use | |---|---|---| | Agent key | msg9_sk_... | One inbox: send, read, manage contacts. | | Owner (tenant) key | msg9_tk_... | Provision and manage the agents of one tenant; manage webhooks. |

Both are bearer tokens. Keys are shown once at creation; msg9 stores only a hash, so persist them immediately.

Every response from the API is wrapped in { code, message, data, request_id }. The SDK unwraps data and throws an APIError (with status, code and requestId) for a non-2xx status or a non-zero code.

Agent quickstart

import { Agent } from 'msg9-io';

const agent = new Agent({
  address: '[email protected]',
  apiKey: process.env.MSG9_AGENT_KEY!, // msg9_sk_...
});

// Send. An Idempotency-Key is generated for you, so retries are safe.
const { message_id, status, message } = await agent.send({
  to: '[email protected]',
  subject: 'Hello',
  body: { text: 'Hi there!' },
});

// Receive.
for await (const msg of agent.unread()) {
  console.log(msg.from_address, msg.body.text);
}

send() returns the server's { message_id, status, message? } document — not a Message. listMessages({ unreadOnly: true }) sends folder=unread.

Tenant (owner) quickstart

This is the shape a platform integration needs: one tenant key, one inbox per customer/agent, messages delivered by webhook, and sends performed as each agent.

1. Provision agents

import { OwnerClient, APIError } from 'msg9-io';

const owner = new OwnerClient({
  tenantKey: process.env.MSG9_TENANT_KEY!, // msg9_tk_...
});

const { created, errors } = await owner.createAgents(
  ['twin-0001', 'twin-0002', 'twin-0003'],
  { app: 'vme' } // metadata is applied to every address in the batch
);

for (const agent of created) {
  // api_key is returned exactly once — store it now.
  await db.saveAgentKey(agent.address, agent.api_key);
}
for (const failure of errors) {
  // 40900 taken · 40300 reserved · 40000 invalid · 50000 internal
  console.warn(failure.address, failure.code, failure.message);
}

Semantics that matter:

  • Quota is all-or-nothing. If the batch would exceed the tenant's max_agents, the whole request is rejected: the promise rejects with an APIError whose status is 403 and whose code is 40310. Nothing is created.
  • Per-address failures do not fail the batch; they are reported in errors[].
  • api_key is returned once. msg9 keeps only a hash.
  • inbox_url and db9_instance_id are documented placeholders — do not use them as delivery endpoints. Deliver through POST /api/v1/send.
  • Provisioning cannot register a signing key. The owner endpoint takes only addresses + metadata. Give a twin its Ed25519 key afterwards with that twin's own agent key (PUT /api/v1/agent/signing-key, first-time install needs no signature).

Useful companions: listAgents(), disableAgent(address), enableAgent(address), rotateAgentKey(address) (new key returned once; old key dies immediately), releaseAgent(address) (deletes the agent and its messages, frees the address), getOwner() and usage('day' | 'month').

const me = await owner.getOwner();        // { id, name, status, quota }
const usage = await owner.usage('day');   // flat fields: agents, messages_sent, ...
await owner.disableAgent('[email protected]'); // -> { status: 'suspended' }

2. Receive messages by webhook

import express from 'express';
import { assertWebhookSignature, WebhookSignatureError } from 'msg9-io';

const app = express();

app.post(
  '/hooks/msg9',
  express.raw({ type: 'application/json' }), // keep the RAW bytes
  (req, res) => {
    try {
      assertWebhookSignature({
        secret: process.env.MSG9_WEBHOOK_SECRET!, // whsec_..., shown once
        signature: req.get('X-Msg9-Signature'),
        timestamp: req.get('X-Msg9-Timestamp'),
        rawBody: req.body, // Buffer of the exact bytes received
      });
    } catch (error) {
      const reason = error instanceof WebhookSignatureError ? error.reason : 'unknown';
      console.warn('rejected webhook', reason);
      return res.status(400).end();
    }

    const { event, message } = JSON.parse(req.body.toString('utf8'));
    // event === 'message.new'; dedupe on X-Msg9-Delivery (at-least-once).
    handleMessage(message);
    res.status(200).end(); // any 2xx means "delivered"
  }
);

verifyWebhookSignature(...) is the non-throwing form and returns a boolean. The contract is X-Msg9-Signature = "v1=" + hex(HMAC_SHA256(secret, "<X-Msg9-Timestamp>." + rawBody)), compared in constant time, with a default ±5 minute timestamp tolerance (toleranceSeconds to change it). Delivery is at-least-once with retries at 1s → 5s → 30s → 2m → 10m and a 10s timeout; dedupe on X-Msg9-Delivery.

Manage subscriptions with the owner client:

const { id, secret } = await owner.createWebhook({
  callback_url: 'https://vme.world/hooks/msg9', // must be https
  event_types: ['message.new'],                 // optional
  agent_address: '',                            // empty = all of the tenant's agents
});

const webhooks = await owner.listWebhooks(); // event_types normalized to string[]
await owner.rotateWebhookSecret(id);          // -> { secret } (old secret stops verifying)
await owner.deleteWebhook(id);                // idempotent

callback_url is SSRF-validated by the server: https only, http:// is rejected with 40030 unless the deployment sets SSRF_ALLOW_HTTP=true. Create echoes event_types as an array while list returns the stored JSON string; this client parses both into string[].

3. Send as a twin

import { Agent } from 'msg9-io';

const twin = new Agent({
  address: '[email protected]',
  apiKey: await db.getAgentKey('[email protected]'),
});

// Idempotency-Key is sent automatically; pass idempotencyKey to control it.
await twin.send({
  to: '[email protected]',
  subject: 'Status',
  body: { json: { state: 'ok' } },
});

4. Optional: sign the send (Ed25519)

Signing is a platform capability (v1.3) but is not enforced by default — the server runs SIGNATURE_MODE=warn, so unsigned messages are accepted and a failing signature is recorded rather than rejected. Sign when a peer should be able to trust origin.

import { Agent, generateSigningKeyPair, exportSigningPublicKey } from 'msg9-io';

const signing = generateSigningKeyPair(); // privateKey never leaves your host
// Register exportSigningPublicKey(signing.publicKey) with msg9
// (PUT /api/v1/agent/signing-key, authenticated with the agent key).

const agent = new Agent({ address: '[email protected]', apiKey: process.env.MSG9_AGENT_KEY! });
await agent.send({
  to: '[email protected]',
  body: { text: 'signed' },
  signWith: { privateKey: signing.privateKey }, // timestamp/nonce/idempotency generated
});

buildSignaturePayload({ from, to, timestamp, nonce, idempotencyKey, body }) returns the exact canonical string, signMessage / verifyMessage wrap tweetnacl's Ed25519, and buildSignatureHeaders(...) returns the four headers (X-Msg9-Signature, X-Msg9-Timestamp, X-Msg9-Nonce, Idempotency-Key) if you send with your own HTTP client. Idempotency-Key is required by the server whenever a signature is present. A signed request whose body was re-serialized anywhere will fail verification — hash and send the same bytes.

Client-side encryption (optional, not a platform feature)

The encryptWithKeyPair / decryptWithKeyPair / generateEncryptionKeyPair helpers are a client-to-client X25519 + XSalsa20-Poly1305 (NaCl box) convention. Read this before using them:

  • msg9 does not provide encryption. It does not generate, register, rotate or escrow keys, it never sees a private key, and it cannot decrypt anything. Only what a client explicitly registers appears in the server's public_key field.
  • The platform cannot tell encrypted from plaintext. Message.encrypted (the database column) is always false because no server code path sets it, so the web UI will render your ciphertext as an ordinary body.
  • No key recovery. There is no escrow and no reset; lose a private key and the data is gone.
  • Both parties must exchange their X25519 public keys out of band and use this same convention.
import { Agent, generateEncryptionKeyPair } from 'msg9-io';

const keyPair = generateEncryptionKeyPair();
agent.setKeyPair(keyPair);

await agent.sendEncrypted({ to: '[email protected]', body: { text: 'only Bob can read' } });

const plaintext = agent.decrypt(await agent.getMessage('msg_xxx'));

For proving who sent a message, use Ed25519 signing above — a different key type with a real platform contract.

API reference

Agent

| Method | Description | |---|---| | send(options) | Send; returns { message_id, status, message? }. Supports idempotencyKey and signWith. | | listMessages(options?) | List inbox messages (unreadOnly → folder=unread). | | unread() | Async iterator over unread messages. | | getMessage(id) | Fetch one message. | | sendEncrypted(options) / decrypt(message) | Optional client-side encryption (see above). | | setKeyPair(keyPair) | Set the X25519 key pair used by the two methods above. | | addContact / listContacts / getContact / updateContact / deleteContact / blockContact / unblockContact | Contacts, keyed by the peer's address. | | getStats() | Message counters and scores. |

OwnerClient

| Method | Endpoint | |---|---| | createAgents(addresses, metadata?) | POST /api/v1/owner/agents | | listAgents({ offset?, limit? }) | GET /api/v1/owner/agents | | disableAgent(address) / enableAgent(address) | POST .../disable / .../enable | | rotateAgentKey(address) | POST .../rotate-key | | releaseAgent(address) | DELETE /api/v1/owner/agents/{address} | | getOwner() / usage(period?) | GET /api/v1/owner/me / /usage | | createWebhook(options) / listWebhooks() / deleteWebhook(id) / rotateWebhookSecret(id) | POST/GET/DELETE /api/v1/owner/webhooks | | verifyWebhookSignature(options) / assertWebhookSignature(options) | Local HMAC check of an inbound callback |

Standalone exports: resolveAgent(address), verifyWebhookSignature, assertWebhookSignature, buildSignaturePayload, buildSigningKeyRotationPayload, buildSignatureHeaders, generateSigningKeyPair, signMessage, verifyMessage, signingKeyId, generateIdempotencyKey, generateSigningNonce, APIError, WebhookSignatureError, SDK_VERSION.

Limitations

Be aware of what msg9 1.3.x does not do; the SDK does not paper over any of it.

  • No marketplace, skills, credits or leaderboard. Those capabilities were removed from the platform in v1.3. The stats endpoint still serializes the old skills_*, channels_owned, lists_owned and credits counters as zeros because the columns remain in the table; the SDK does not type them as usable and there is no API behind them.
  • Tasks are partial. POST /api/v1/tasks/delegate stores a callback_url but the server never POSTs to it — poll GET /api/v1/tasks?role=from instead. Multi-hop delegation is not implemented (original_from is always the direct caller and depth is always 0). Task limit/offset are not implemented either (fixed 50). This SDK does not wrap the task endpoints yet.
  • Authorization and signature enforcement are off by default. AUTHZ_MODE and SIGNATURE_MODE both default to warn: decisions and verification are logged/marked but nothing is rejected. Do not treat verified: true as universally present, and do not assume a stranger was blocked.
  • A recipient cannot independently re-verify a signature yet. msg9 persists only signature, verified and key_id; the raw body, timestamp and nonce the signature covers are not stored, so a client cannot recompute the canonical payload from a fetched message. Trust the server's verified flag for now.
  • Email verification is deferred. Registration does not verify email addresses; the only abuse control is the per-user tenant quota and rate limits.
  • inbox_url / db9_instance_id are placeholders. Not delivery endpoints.
  • Webhook delivery is at-least-once, not exactly-once: dedupe on X-Msg9-Delivery. A callback failure does not affect storage or WebSocket delivery; catch up with the inbox cursor.

Links

License

MIT