@writhq/sdk
v0.2.1
Published
Writ agent SDK — Ed25519 keypair management, mandate-scoped assertion signing, and the X-Passport header for verified agent actions. Give your agents authority they can prove.
Maintainers
Readme
@writhq/sdk
Writ — KYA (Know Your Agent). Give your agents authority they can prove.
The agent-side SDK. It holds an agent's Ed25519 keypair, signs mandate-scoped
assertions, and presents them as the X-Passport header so a platform can
resolve the full chain of agency on every call:
action ← agent (this keypair)
← mandate (scope, caps, expiry, revocable)
← principal (KYC'd human or entity)
← liabilityThe platform side is @writhq/verify.
Drive the whole flow in one command with
npx @writhq/demo.
Install
npm install @writhq/sdkRequires Node 20+. ESM only — this package has no CommonJS build, so
require('@writhq/sdk') fails with ERR_PACKAGE_PATH_NOT_EXPORTED. That error
reads like a broken package and is not: use import, or await import() from
CJS.
Quickstart
An agent generates a keypair locally, the principal registers its public key (dashboard or their own backend), then the agent signs and presents assertions.
import { PassportAgent } from '@writhq/sdk';
// Loads (or creates on first run) the agent's local keypair under
// $PASSPORT_AGENT_HOME (default ./.passport-agent). The passport URL defaults
// to https://api.writhq.com; set PASSPORT_URL (or pass passportUrl) to point at
// a local stack instead.
const agent = await PassportAgent.load({
name: 'treasury-bot',
runtime: 'claude-code',
});
// Hand this public JWK to your principal to register the agent. They return an
// agent id (agt_...); record it so the agent can sign.
console.log(JSON.stringify(agent.publicJwk));
agent.setAgentId('agt_...');
// Point the agent at a mandate + platform (persisted locally).
agent.setContext({ mandate: 'mnd_...', platform: 'plt_northbank' });
// Present a signed, verified action. amount is in MINOR units (cents).
const { status, body } = await agent.present(
'https://northbank-production.up.railway.app/api/refill',
{ action: 'account.refill', amount: 50_000 }, // $500.00
);
console.log(status, body); // 200 + { decision:'allow', receipt, balance_minor, ... }Signing without presenting
sign() returns the compact JWS you attach yourself as the X-Passport header:
const jws = await agent.sign({ action: 'account.refill', amount: 50_000 });
await fetch('https://platform.example.com/api/refill', {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-passport': jws },
body: JSON.stringify({ amount_minor: 50_000 }),
});Signature authority (document.sign)
The same keypair answers a second question: may this agent put its principal's
name on this document? buildSignAssertion writes the assertion; everything
after it — the X-Passport header, the verify round trip, the receipt — is
identical to a payment.
import { buildSignAssertion, hashDocument } from '@writhq/sdk';
const assertion = await buildSignAssertion(
{
agent: 'agt_9f…',
mandate: 'mnd_tr7…',
platform: 'plt_northbank',
document_hash: await hashDocument(pdfBytes), // lowercase hex sha256
document_class: 'nda', // nda | msa | sow | order_form | dpa | other
counterparty: 'Northbank Sandbox',
liability_minor: 2_500_000, // $25,000 of exposure
},
privateJwk,
);Only the hash leaves your runtime — the passport never receives the document.
The mandate decides: paper of a class it doesn't list denies document_class, a
document over the per-document liability cap denies per_tx_cap, too much
cumulative exposure denies period_cap.
Holding a PassportAgent? It knows its own id, mandate and platform already,
so it can do the whole thing in one call:
const { status, body } = await agent.presentDocument(
'https://esign.example.com/envelopes/env_8821/sign',
{
document_hash: await hashDocument(pdfBytes),
document_class: 'nda',
counterparty: 'Northwind Logistics GmbH',
liability_minor: 2_500_000,
},
);agent.signDocument(args) returns just the compact JWS if you want to attach it
yourself.
Writ attests the authority. It never produces the signature — your e-signature platform still does that — and none of this is a qualified electronic signature (eIDAS/QES).
Low-level primitives
The core cryptography is re-exported for direct use (e.g. building your own
onboarding or a headless test — this is exactly what @writhq/demo does):
import { generateKeypair, publicFromPrivate, buildAssertion, verifyAssertion, toMinor, formatMinor } from '@writhq/sdk';
const { publicJwk, privateJwk } = await generateKeypair(); // Ed25519 JWKs
toMinor('$1,000'); // 100000
formatMinor(50000); // "$500.00"
const jws = await buildAssertion(
{ agent: 'agt_x', mandate: 'mnd_x', action: 'account.refill', amount: 50_000, currency: 'USD', platform: 'plt_northbank' },
privateJwk,
);
const check = await verifyAssertion(jws, publicJwk); // { valid, payload }API
PassportAgent.load(config?)— load/create the local identity. Config:{ passportUrl?, name?, runtime? }.agent.publicJwk/agent.agentId/agent.identity— the identity.agent.setAgentId(id)— record the registeredagt_...id.agent.setContext({ mandate?, platform? })— persist demo/default context.agent.sign(args)→ compact JWS.args:{ action, amount, currency?, platform?, mandate? }.agent.present(url, args)→{ status, body }— sign + POST with the header.agent.signDocument(args)→ compact JWS.args:{ document_hash, document_class, counterparty, liability_minor, currency?, action?, platform?, mandate? }.agent.presentDocument(url, args)→{ status, body }— sign + POST a signing assertion, with the document block in the body.- Local keystore:
currentIdentity(),loadIdentity(),loadOrCreateIdentity(),saveIdentity(),publicKeyOf(),homeDir()— read/write the keypair on disk under$PASSPORT_AGENT_HOME. Most callers only needPassportAgent. unsafeDecode(jws)— read a JWS's claims WITHOUT verifying. Diagnostics only; never make a decision on its output.PASSPORT_DEFAULT_URL/resolvePassportUrl(explicit?, env?)— the production default (https://api.writhq.com) and the resolution order every Writ package shares.- Primitives:
generateKeypair,publicFromPrivate,buildAssertion,verifyAssertion,peekAgentId,signCompact,verifyCompact,tryVerifyCompact,toMinor,formatMinor,isValidMinor,ALG. - Signature authority:
buildSignAssertion,hashDocument,isDocumentAction,documentClass,documentContext,DOCUMENT_SIGN_ACTION,DOCUMENT_ACTION_PREFIX. - Types:
JWK,KeyPairJWK,BuildAssertionInput,BuildSignAssertionInput,AssertionPayload,AssertionCheck,DocumentClass,DocumentContext,VerifyResponse,PassportAgentConfig,PresentArgs,SignArgs,AgentIdentity.
Notes
- Amounts are always integer minor units (cents for USD). Never floats.
- The wire header stays
X-Passportand the assertion is a compact EdDSA JWS — Writ is the human-facing brand for the same protocol. - Home: https://writhq.com · Docs: https://writhq.com/docs/ · API: https://api.writhq.com
License
MIT © Tundra Industries
