Runnable API examples
Complete Node programs for character rendering, skills policy and signed ACT verification.
These programs use the published packages below and Node 20+. Each file runs independently, uses real package implementations, and exits unsuccessfully if an assertion fails. For a persistent signing identity and an actual HTTP application, start with the first-agent quickstart or the local identity programs.
In a new project folder:
npm init -y
npm install @openagentid/sdk@0.7.2 @openagentid/skills-policy@0.1.1 @openagentid/crypto-wasm@0.1.1 cborg@6.1.3Save each JavaScript block with its displayed filename. These examples keep their generated demonstration state in memory and create no hosted account, production grant or remote identity record.
Render a character through the Fetch handler
This exercises the same stateless handler you can mount in your own server. The .invalid URL only supplies a request origin; the program performs no network request. The rendering DID is illustrative and carries no identity authority.
import assert from 'node:assert/strict';
import { createCharacterApiHandler } from '@openagentid/sdk/character-api';
const handle = createCharacterApiHandler();
const base = 'https://example.invalid/v1/characters';
const did = 'did:oas:example:agent:render-demo';
const response = await handle(new Request(`${base}/generate`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ did, skin: 'autos', animated: false, size: 128 }),
}));
assert.equal(response.status, 200);
const character = await response.json();
assert.equal(character.did, did);
assert.equal(character.skin, 'autos');
assert.match(character.svg, /<svg[\s>]/);
const invalid = await handle(new Request(`${base}/skins?unknown=true`));
assert.equal(invalid.status, 400);
console.log({ did: character.did, skin: character.skin, invalidStatus: invalid.status });node characters.mjsThe output identifies the requested character and a 400 response for the deliberately invalid catalog query. Rendering never authenticates the DID. See HTTP rendering options and handler declarations.
Authorize and record a skill
This program configures a policy for one illustrative agent, records an allowed invocation and confirms that an unlisted skill is denied. In an application, derive InvocationContext from verified identity and trusted request state, then require recordInvocation to succeed before executing the action.
import assert from 'node:assert/strict';
import {
Did, InvocationContext, NotAllowedError, SkillsPolicy,
} from '@openagentid/skills-policy';
const policy = SkillsPolicy.fromYaml(`
version: 1
agent: did:oas:example:agent:docs-demo
skills:
read-documentation:
allow: true
audit_level: hash
default:
allow: false
`);
const context = new InvocationContext({
agentDid: Did.parse('did:oas:example:agent:docs-demo'),
sessionId: 'local-policy-example',
arguments: { path: '/essentials/quickstart' },
});
const receipt = policy.recordInvocation('read-documentation', context);
assert.equal(receipt.skill, 'read-documentation');
assert.throws(
() => policy.recordInvocation('delete-documentation', context),
NotAllowedError,
);
assert.equal(policy.auditChain().length, 1);
assert.equal(policy.auditChain().verify(), true);
console.log({ allowed: receipt.skill, denied: 'delete-documentation', receipts: 1 });node skills.mjsThe receipt is an in-memory policy decision. It neither authenticates the illustrative DID nor invokes a tool. Persist and redact audit records according to your application. See skills governance and published declarations.
Sign and verify an ACT
This local example generates an Ed25519 issuer key, signs a CBOR grant, and verifies it with the published WASM binding. The receiver chooses its expected issuer, audience, trusted public key and required scope. It also refuses the same token under a different audience, altered claims with the original signature, and a correctly signed but expired token.
cborg encodes the example's CBOR bytes. @openagentid/crypto-wasm supplies the real verifier; its Node export includes the compiled WASM. No separate Rust/WASM build is needed for this program.
Supply the receiver's current clock with .atTime(Math.floor(Date.now() / 1000)) on every verification. The published WASM 0.1.1 default clock path calls Rust SystemTime, which traps in this WASM target. The explicit host clock preserves real expiry and not-before checks; never take the verification time from incoming token claims or reuse a startup timestamp. The SDK 0.7.2 middleware wrappers do not expose this clock option; see their separate integration limitation.
import assert from 'node:assert/strict';
import { generateKeyPairSync, randomUUID, sign } from 'node:crypto';
import { encode } from 'cborg';
import { act, setActCryptoBinding } from '@openagentid/sdk';
import { act_verify } from '@openagentid/crypto-wasm';
setActCryptoBinding({ act_verify });
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
const publicJwk = publicKey.export({ format: 'jwk' });
if (!publicJwk.x) throw new Error('Missing Ed25519 public key');
const trustedKey = new Uint8Array(Buffer.from(publicJwk.x, 'base64url'));
const now = Math.floor(Date.now() / 1000);
const issuer = 'urn:example:local-act-issuer';
const audience = 'urn:example:documentation';
const subject = 'did:oas:example:agent:act-demo';
const claims = {
jti: randomUUID(), sub: subject, iss: issuer, aud: audience,
iat: now, nbf: now, exp: now + 60,
tenant_id: 'local-demo', scope: ['docs:page:read'],
};
const claimsBytes = encode(claims);
const signature = new Uint8Array(sign(null, claimsBytes, privateKey));
const token = encode({ v: 1, alg: 'Ed25519', claims: claimsBytes, sig: signature });
/** @param {Uint8Array} bytes @param {string} expectedAudience */
const verify = (bytes, expectedAudience) => act.verify(bytes)
.trustedKeys([trustedKey])
.issuer(issuer)
.forAudience(expectedAudience)
.requireScope('docs:page:read')
.atTime(Math.floor(Date.now() / 1000))
.run();
const verified = await verify(token, audience);
assert.equal(verified.sub, subject);
await assert.rejects(() => verify(token, 'urn:example:other-service'), /audience mismatch/);
const tampered = encode({
v: 1, alg: 'Ed25519',
claims: encode({ ...claims, sub: 'did:oas:example:agent:someone-else' }),
sig: signature,
});
await assert.rejects(() => verify(tampered, audience), /signature did not validate/);
const expiredClaims = encode({ ...claims, iat: now - 120, nbf: now - 120, exp: now - 60 });
const expired = encode({
v: 1, alg: 'Ed25519', claims: expiredClaims,
sig: new Uint8Array(sign(null, expiredClaims, privateKey)),
});
await assert.rejects(() => verify(expired, audience), /token expired/);
console.log({ subject: verified.sub, audience: verified.aud, refused: ['wrong audience', 'altered claims', 'expired token'] });node act.mjsThe demo issuer and tenant exist only within this process. A real receiver must configure its own trusted issuer keys and tenant/action policy; it must not copy trust from an incoming token. ACT verification does not resolve an OAS document, check an application's revocation store or independently prove possession. See ACT format, SDK declarations and crypto binding declarations.