Two agents, one limit, one refused.
All posts
5 min read
by

The TypeScript SDK for agents that do things

One typed TypeScript client for letting an AI agent act on production: invoke an action by name, Anlyon holds the credential, and risky calls wait for a human.

typescriptsdkagentstutorial

Editor's note, 3 October 2026. This post covers an earlier namespace. It still runs on the same API. Anlyon's product today is shared limits and receipts for AI agent actions.

The Anlyon TypeScript SDK is how your agent asks for something to happen in production. The agent invokes a named action. Anlyon holds the credential, waits for a human when the action needs one, and makes the call to Stripe, GitHub or your own API itself. The same client also carries the earlier namespaces: memory, schedules and message delivery.

Installation

Install the SDK using your preferred package manager:

npm install @anlyonhq/sdk
# or
pnpm add @anlyonhq/sdk
# or
yarn add @anlyonhq/sdk

Quick Start

1. Initialize the Client

import { Client } from '@anlyonhq/sdk';

const client = new Client({
  apiKey: process.env.ANLYON_API_KEY!,
});

2. Define an action (operator key, once)

Define the production call from an operator key, not the key your agent runs with. This is the only place the credential appears.

const ops = new Client({ apiKey: process.env.ANLYON_OPERATOR_KEY! });

await ops.secrets.put('STRIPE_KEY', { value: process.env.STRIPE_KEY! });

await ops.actions.create({
  name: 'refund-order',
  description: 'Refund a Stripe charge.',
  method: 'POST',
  urlTemplate: 'https://api.stripe.com/v1/refunds',
  headers: { Authorization: 'Bearer {{secret:STRIPE_KEY}}', 'Content-Type': 'application/x-www-form-urlencoded' },
  inputSchema: {
    type: 'object',
    properties: { charge: { type: 'string' }, amount: { type: 'integer' } },
    required: ['charge', 'amount'],
  },
  requiresApproval: true,
});

3. Invoke it from the agent

const anlyon = new Client({ apiKey: process.env.ANLYON_AGENT_KEY! });

const { data } = await anlyon.actions.invoke(
  'refund-order',
  { charge: 'ch_3P9x', amount: 12000 },
  { idempotencyKey: 'refund:ord_42' },
);

if (data!.pendingApproval) {
  console.log('Parked for a human:', data!.approvalId);
} else {
  console.log('Stripe replied', data!.responseStatus);
}

The agent's half carries no URL, header or key. With the idempotency key, a same-key, same-request invoke is answered from the recorded invocation. It is not executed again.

4. The local gate, for calls that can't move

When a call cannot become an HTTP action (an ORM write, a native SDK), wrap your own function instead:

import { ApprovalDeniedError } from '@anlyonhq/sdk';

const guardedRefund = client.approvals.gate(
  { title: 'Refund a customer?', timeoutMs: 10 * 60_000 },
  refund,
);

try {
  await guardedRefund('ord_42', 129.99); // blocks until a human approves
} catch (err) {
  if (err instanceof ApprovalDeniedError) {
    console.log('Denied:', err.approval.decisionNote);
  }
}

This is the fallback path. Your process still runs your function with your credential, so you get the human decision and the record, not credential isolation.

5. Publish a message (durable delivery)

// Send a message immediately
const result = await client.messages.publish({
  url: 'https://api.example.com/webhook',
  method: 'POST',
  body: { event: 'user.created', userId: '123' },
});

console.log(`Message published: ${result.data.messageId}`);

6. Schedule a delayed message

// Send a message 60 seconds from now
const result = await client.messages.publish({
  url: 'https://api.example.com/webhook',
  method: 'POST',
  body: { event: 'reminder', message: 'Don\'t forget!' },
  executeAt: Math.floor(Date.now() / 1000) + 60,
});

Using the Fluent Builder API

The SDK provides a fluent builder API for a more intuitive experience:

// Using the fluent builder
await client.messages
  .createMessage()
  .to('https://api.example.com/webhook')
  .body({ event: 'test', data: { value: 42 } })
  .in(60) // 60 seconds from now
  .maxRetries(5)
  .send();

Next.js Integration

The SDK ships a Next.js App Router handler that verifies the webhook signature, so a delivery callback is verified before your handler processes it.

The simplest way to handle Anlyon webhooks in Next.js:

// app/api/scheduler/route.ts
import { serve } from '@anlyonhq/sdk/nextjs';

export const { POST } = serve(async (request) => {
  const data = await request.json();

  // Process the verified webhook
  await processJob(data);

  return { success: true };
});

Using verifySignature() Middleware

For more control over your handler:

// app/api/webhook/route.ts
import { verifySignature } from '@anlyonhq/sdk/nextjs';

export const POST = verifySignature(async (request) => {
  const data = await request.json();

  // Signature is already verified
  await handleWebhook(data);

  return Response.json({ ok: true });
});

Set your signing secret in your environment:

ANLYON_SIGNING_SECRET=whsec_your_signing_secret_here

Advanced Features

CRON Scheduling

Create recurring tasks with CRON expressions:

// Create a daily schedule
await client.schedules.create({
  name: 'Daily Report',
  cronExpression: '0 9 * * *', // 9 AM every day
  timezone: 'America/New_York',
  url: 'https://api.example.com/report',
  method: 'POST',
  body: { type: 'daily_report' },
});

// Using the fluent builder
await client.schedules
  .createSchedule()
  .name('Hourly Ping')
  .hourly()
  .to('https://api.example.com/ping')
  .body({ type: 'ping' })
  .create();

URL Groups (Fan-Out Messaging)

Broadcast messages to multiple endpoints:

// Create a URL group
const group = await client.urlGroups.create({
  name: 'Production Webhooks',
  endpoints: [
    { url: 'https://service1.example.com/webhook' },
    { url: 'https://service2.example.com/webhook' },
  ],
});

// Broadcast to all endpoints
await client.urlGroups.publish(group.data.id, {
  body: { event: 'broadcast', data: { userId: 123 } },
});

Dead Letter Queue Management

Handle failed messages:

// List failed messages
const failed = await client.dlq.list();

// Retry a specific message
await client.dlq.retry('msg_id');

// Retry all failed messages
await client.dlq.retryAll();

// Get DLQ statistics
const stats = await client.dlq.getStats();

Error Handling

The SDK provides typed error classes for different scenarios:

import { 
  AnlyonError,
  AuthenticationError,
  QuotaExceededError,
  ValidationError 
} from '@anlyonhq/sdk';

try {
  await client.messages.publish({ /* ... */ });
} catch (error) {
  if (error instanceof QuotaExceededError) {
    console.error('Quota exceeded:', error.message);
    // Handle quota exceeded
  } else if (error instanceof ValidationError) {
    console.error('Validation error:', error.message);
    // Handle validation error
  } else if (error instanceof AuthenticationError) {
    console.error('Authentication failed');
    // Handle auth error
  }
}

Pagination

The SDK provides helpers for iterating over paginated results:

// List all messages (automatically handles pagination)
for await (const message of client.messages.listAll()) {
  console.log(message.messageId);
}

// Or manually paginate
const result = await client.messages.list({ limit: 50 });
// result.data contains messages
// result.pagination contains pagination info

Best Practices

  1. Store API keys securely: Use environment variables, never commit them to version control
  2. Handle errors gracefully: Always wrap SDK calls in try-catch blocks
  3. Use TypeScript: The SDK is fully typed for better developer experience
  4. Set appropriate retries on deliveries: Messages, schedules and workflows retry. Action invocations deliberately do not. An invocation whose outcome is unknown needs reconciling, not retrying
  5. Monitor delivery: Use the analytics API to track message delivery

Next Steps

Free tier, no credit card. One command if you use Claude or Cursor.

$ claude mcp add anlyon -- npx -y @anlyonhq/mcp-server