@dainprotocol/service-sdk
v2.0.89
Published
DAIN Service SDK
Readme
DAIN Service SDK
The official TypeScript SDK for building AI-powered services on the DAIN Protocol. Create intelligent services that can be discovered and used by AI agents across multiple platforms.
📋 Table of Contents
- Features
- Installation
- Quick Start
- Core Concepts
- Multi-Runtime Support
- Tools
- OAuth2 Integration
- Plugins
- Client SDK
- Advanced Features
- API Reference
- Examples
✨ Features
- 🚀 Multi-Runtime Support - Works seamlessly on Node.js, Deno, Cloudflare Workers, and Next.js
- 🔧 Type-Safe Tools - Define tools with Zod schemas for automatic validation and type inference
- 🔐 OAuth2 Integration - Built-in OAuth2 support with flexible storage adapters
- 🔌 Plugin System - Extend functionality with crypto, citations, time, and custom plugins
- 📡 Real-Time Streaming - Server-Sent Events (SSE) for streaming responses
- 🔄 Long-Running Processes - State machine-based process management
- 💳 Payment Integration - Built-in support for Stripe payments
- 🎨 UI Components - Return rich UI responses alongside data
- 📊 Contexts & Datasources - Provide dynamic context and data to AI agents
- 🛡️ Signature-Based Auth - Secure authentication using Ed25519 signatures
📦 Installation
npm install @dainprotocol/service-sdk zodOptional Dependencies
For OAuth2 with persistent storage:
npm install @dainprotocol/oauth2-token-manager @dainprotocol/oauth2-storage-drizzleFor process management with Redis:
npm install ioredis🚀 Quick Start
Creating a Simple Weather Service (Node.js)
import { defineDAINService } from '@dainprotocol/service-sdk';
import { z } from 'zod';
// Define a tool
const getWeatherTool = {
id: 'get-weather',
name: 'Get Weather',
description: 'Fetches current weather for a city',
input: z.object({
city: z.string().describe('The name of the city'),
}),
output: z.object({
temperature: z.number().describe('Temperature in Celsius'),
condition: z.string().describe('Weather condition'),
}),
pricing: { pricePerUse: 0.01, currency: 'USD' },
handler: async ({ city }, agentInfo) => {
// Your logic here
return {
text: `The weather in ${city} is 22°C and Sunny`,
data: { temperature: 22, condition: 'Sunny' },
ui: undefined,
};
},
};
// Create and start the service
const service = defineDAINService({
metadata: {
title: 'Weather Service',
description: 'A service for weather information',
version: '1.0.0',
author: 'Your Name',
tags: ['weather'],
},
identity: {
apiKey: process.env.DAIN_API_KEY,
},
tools: [getWeatherTool],
});
// Start the service
service.startNode({ port: 3000 }).then(() => {
console.log('Weather service running on port 3000');
});Using the Client SDK
import { DainServiceConnection, DainClientAuth } from '@dainprotocol/service-sdk/client';
// Initialize authentication
const auth = new DainClientAuth({
apiKey: process.env.DAIN_API_KEY,
});
// Connect to a service
const service = new DainServiceConnection('http://localhost:3000', auth);
// Call a tool
const result = await service.callTool('get-weather', {
city: 'San Francisco',
});
console.log(result.text); // "The weather in San Francisco is 22°C and Sunny"
console.log(result.data); // { temperature: 22, condition: 'Sunny' }🧩 Core Concepts
Services
A service is a collection of tools, contexts, datasources, and widgets that provide specific functionality to AI agents.
const service = defineDAINService({
metadata: { /* ... */ },
identity: { /* ... */ },
tools: [ /* ... */ ],
contexts: [ /* ... */ ],
datasources: [ /* ... */ ],
widgets: [ /* ... */ ],
});Tools
Tools are functions that AI agents can call. Each tool has:
- A unique ID
- Input/output schemas (using Zod)
- A handler function
- Optional pricing information
const myTool = {
id: 'my-tool',
name: 'My Tool',
description: 'Does something useful',
input: z.object({ param: z.string() }),
output: z.object({ result: z.string() }),
handler: async (input, agentInfo, context) => {
return {
text: 'Human-readable response',
data: { result: 'structured data' },
ui: { /* optional UI component */ },
};
},
};Agent Info
Every tool handler receives agentInfo with details about the calling agent:
interface AgentInfo {
agentId: string; // Unique agent identifier
address: string; // Agent's blockchain address
smartAccountPDA?: string;
id: string;
webhookUrl?: string; // For sending async updates
}Tool Context
The context parameter provides access to:
- The Hono app instance
- OAuth2 client for authenticated API calls
- Custom extra data
- UI update functions
- Process management
interface ToolContext {
app: Hono;
oauth2Client?: OAuth2Client;
extraData?: any;
updateUI?: (update: { ui: any }) => Promise<void>;
addProcess?: (processId: string) => Promise<void>;
}🌐 Multi-Runtime Support
The SDK automatically adapts to your runtime environment through conditional exports.
Node.js
import { defineDAINService } from '@dainprotocol/service-sdk';
const service = defineDAINService({ /* config */ });
await service.startNode({ port: 3000 });Deno
import { defineDAINService } from '@dainprotocol/service-sdk';
const service = defineDAINService({ /* config */ });
await service.startDeno({ port: 3000 });Cloudflare Workers
import { defineDAINService } from '@dainprotocol/service-sdk';
const service = defineDAINService({ /* config */ });
export default {
fetch: service.startWorkers(),
};Next.js (App Router)
// app/api/dain/[...dain]/route.ts
import { createNextDainService } from '@dainprotocol/service-sdk/next';
const { GET, POST } = createNextDainService({
metadata: { /* ... */ },
identity: { /* ... */ },
tools: [ /* ... */ ],
});
export { GET, POST };🔧 Tools
Creating Tools
Use the createTool helper for better type inference:
import { createTool } from '@dainprotocol/service-sdk';
const weatherTool = createTool({
id: 'get-weather',
name: 'Get Weather',
description: 'Get current weather',
input: z.object({
lat: z.number(),
lon: z.number(),
}),
output: z.object({
temp: z.number(),
description: z.string(),
}),
handler: async ({ lat, lon }) => {
const response = await fetch(
`https://api.openweather.org/data/2.5/weather?lat=${lat}&lon=${lon}`
);
const data = await response.json();
return {
text: `Temperature: ${data.main.temp}°C`,
data: {
temp: data.main.temp,
description: data.weather[0].description,
},
ui: undefined,
};
},
});Tool Interfaces
Tools can implement standardized interfaces for better interoperability:
import { createToolWithInterface, ToolInterfaceType } from '@dainprotocol/service-sdk';
const searchTool = createToolWithInterface({
id: 'search-docs',
name: 'Search Documentation',
description: 'Search through documentation',
interface: ToolInterfaceType.KNOWLEDGE_SEARCH,
input: z.object({
query: z.string(),
}),
output: z.object({
results: z.array(z.object({
title: z.string(),
content: z.string(),
citations: z.array(CitationSchema),
})),
}),
handler: async ({ query }) => {
// Implementation
},
});Actionable Tools
Tools that send messages where users can trigger system actions (buttons, callbacks, links). These tools automatically enforce a standard schema for interoperability with automation systems.
Use Cases:
- Telegram bot with inline buttons
- Slack interactive messages
- Discord message components
- Email with action links
Quick Start with buildActionableTool:
import { buildActionableTool, z } from '@dainprotocol/service-sdk';
const sendTelegramMessage = buildActionableTool({
id: 'send-telegram-message',
name: 'Send Telegram Message',
description: 'Send a message with interactive buttons',
// Add platform-specific fields
additionalFields: {
parse_mode: z.enum(['HTML', 'Markdown']).optional(),
},
output: z.object({
messageId: z.number(),
}),
handler: async (input, agentInfo, context) => {
// input includes: recipient, message, actions, metadata, parse_mode
const { recipient, message, actions } = input;
// Build inline keyboard from actions
const keyboard = actions?.map(action => ({
text: action.label,
callback_data: action.value,
}));
// Send via your messaging API
const result = await sendToTelegram(recipient, message, keyboard);
return {
text: `Message sent to ${recipient}`,
data: { messageId: result.message_id },
};
},
});Standard Fields (automatically included):
recipient(required): Who receives the message (chatId, channel, email)message(required): The message textactions(optional): Interactive buttons/links arraymetadata(optional): Automation context (auto-injected)
Action Types:
approve- Approval buttonreject- Rejection buttoncallback- Custom callbackurl- External linkcustom- Custom action
Usage in Automations:
<action
tool="send-telegram-message"
recipient="{{config.chatId}}"
message="Bitcoin is ${{prev.price}}. Buy?"
actions='[
{"label": "✅ Buy", "type": "approve", "value": "buy"},
{"label": "❌ Skip", "type": "reject", "value": "skip"}
]'/>See example/actionable-tool-example.ts for complete examples (Telegram, Slack, Email).
Tool Pricing
Add pricing to monetize your tools:
const paidTool = {
id: 'premium-analysis',
name: 'Premium Analysis',
description: 'Advanced data analysis',
pricing: {
pricePerUse: 0.50, // $0.50 per use
currency: 'USD',
},
// ... rest of tool config
};Toolboxes
Group related tools into toolboxes:
import { createToolbox } from '@dainprotocol/service-sdk';
const weatherToolbox = createToolbox({
id: 'weather-toolbox',
name: 'Weather Tools',
description: 'Complete weather information toolkit',
tools: ['get-weather', 'get-forecast', 'get-alerts'],
metadata: {
complexity: 'simple',
applicableFields: ['weather', 'climate'],
},
recommendedPrompt: 'Use these tools to get comprehensive weather information',
});🔐 OAuth2 Integration
The SDK includes comprehensive OAuth2 support with automatic token management and refresh.
Basic OAuth2 Setup
import { defineDAINService } from '@dainprotocol/service-sdk';
import { InMemoryStorageAdapter } from '@dainprotocol/oauth2-token-manager';
const service = defineDAINService({
metadata: { /* ... */ },
identity: { /* ... */ },
tools: [ /* ... */ ],
oauth2: {
baseUrl: 'https://your-service.com',
tokenStore: new InMemoryStorageAdapter(),
providers: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
scopes: ['user', 'repo'],
reason: 'Access your GitHub repositories',
requiredTools: ['get-repos', 'create-gist'],
},
},
},
});Persistent Storage (PostgreSQL)
import { DrizzleStorageAdapter } from '@dainprotocol/oauth2-storage-drizzle';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const sql = postgres(process.env.DATABASE_URL!);
const db = drizzle(sql);
const service = defineDAINService({
oauth2: {
baseUrl: 'https://your-service.com',
tokenStore: new DrizzleStorageAdapter(db, { dialect: 'postgres' }),
providers: { /* ... */ },
},
// ... rest of config
});Custom Token Paths
For OAuth2 providers with non-standard response formats (like Slack):
const service = defineDAINService({
oauth2: {
baseUrl: 'https://your-service.com',
providers: {
slack: {
clientId: process.env.SLACK_CLIENT_ID!,
clientSecret: process.env.SLACK_CLIENT_SECRET!,
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
tokenUrl: 'https://slack.com/api/oauth.v2.access',
scopes: ['chat:write', 'channels:read'],
// Custom paths for extracting tokens from response
tokenPaths: {
accessToken: 'authed_user.access_token',
tokenType: 'token_type',
scope: 'scope',
},
},
},
},
// ... rest of config
});Using OAuth2 in Tools
import { createOAuth2Tool } from '@dainprotocol/service-sdk';
const getGitHubProfileTool = createOAuth2Tool({
id: 'get-github-profile',
name: 'Get GitHub Profile',
description: 'Get the authenticated user GitHub profile',
provider: 'github',
input: z.object({}),
output: z.object({
login: z.string(),
name: z.string(),
email: z.string().nullable(),
bio: z.string().nullable(),
}),
handler: async (input, agentInfo, context) => {
const accessToken = await context.oauth2Client!.getAccessToken(
'github',
`${agentInfo.agentId}@dain.local`
);
const response = await fetch('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github.v3+json',
},
});
const data = await response.json();
return {
text: `GitHub Profile: ${data.login}`,
data: {
login: data.login,
name: data.name,
email: data.email,
bio: data.bio,
},
ui: undefined,
};
},
});OAuth2 Provider Options
interface OAuth2ProviderConfig {
clientId: string;
clientSecret: string;
authorizationUrl: string;
tokenUrl: string;
scopes: string[];
usePKCE?: boolean; // Use PKCE flow (recommended for public clients)
useBasicAuth?: boolean; // Use Basic Auth for token exchange
reason?: string; // Explain why OAuth is needed (shown to users)
requiredTools?: string[]; // Tools that require this OAuth connection
extraAuthParams?: Record<string, string>; // Additional auth parameters
responseRootKey?: string; // Root key for token response
tokenPaths?: { // Custom token extraction paths
accessToken?: string | string[];
refreshToken?: string | string[];
expiresIn?: string | string[];
tokenType?: string | string[];
scope?: string | string[];
};
onSuccess?: (agentId: string, tokens: OAuth2Tokens) => Promise<void>;
}🔌 Plugins
Plugins extend functionality on both the service and client side.
Built-in Plugins
Crypto Plugin
import { CryptoPlugin } from '@dainprotocol/service-sdk/plugins';
const cryptoPlugin = new CryptoPlugin();
const service = defineDAINService({
plugins: [cryptoPlugin],
// ... rest of config
});Provides:
- Message signing and verification
- Encryption/decryption
- Hash generation
- Key management
Citations Plugin
import { CitationsPlugin } from '@dainprotocol/service-sdk/plugins';
const citationsPlugin = new CitationsPlugin({
namespace: 'my-service',
});
const service = defineDAINService({
plugins: [citationsPlugin],
// ... rest of config
});Provides:
- Citation tracking
- Source verification
- Citation formatting
Time Plugin
import { TimePlugin } from '@dainprotocol/service-sdk/plugins';
const timePlugin = new TimePlugin();
const service = defineDAINService({
plugins: [timePlugin],
// ... rest of config
});Provides:
- Timezone conversion
- Timestamp validation
- Time-based operations
Creating Custom Plugins
import { DainPlugin } from '@dainprotocol/service-sdk';
class MyCustomPlugin implements DainPlugin {
id = 'my-custom-plugin';
async initialize(service: DAINService): Promise<void> {
console.log('Plugin initialized');
}
async processInputClient?(input: any): Promise<any> {
// Process input on the client side
return { myData: 'client-processed' };
}
async processInputService?(input: any, agentInfo: AgentInfo): Promise<any> {
// Process input on the service side
return { myData: 'service-processed' };
}
async processOutputService?(output: any, agentInfo: AgentInfo): Promise<any> {
// Process output before sending to client
return output;
}
}
// Use it
const service = defineDAINService({
plugins: [new MyCustomPlugin()],
// ... rest of config
});📡 Client SDK
Connecting to a Service
import { DainServiceConnection, DainClientAuth } from '@dainprotocol/service-sdk/client';
const auth = new DainClientAuth({
apiKey: process.env.DAIN_API_KEY,
});
const service = new DainServiceConnection('http://localhost:3000', auth, {
plugins: [/* optional plugins */],
});Getting Service Metadata
const metadata = await service.getMetadata();
console.log(metadata.title);
console.log(metadata.description);
console.log(metadata.version);Listing Available Tools
const tools = await service.getTools();
tools.forEach(tool => {
console.log(`${tool.name}: ${tool.description}`);
});Calling Tools
const result = await service.callTool('tool-id', {
param1: 'value1',
param2: 'value2',
});
console.log(result.text); // Human-readable response
console.log(result.data); // Structured data
console.log(result.ui); // Optional UI componentStreaming Responses
const stream = await service.callToolStream('tool-id', {
query: 'What is the weather?',
});
for await (const chunk of stream) {
if (chunk.type === 'text-delta') {
process.stdout.write(chunk.textDelta);
} else if (chunk.type === 'tool-result') {
console.log('\nFinal result:', chunk.result);
}
}Working with Contexts
const contexts = await service.getContexts();
const userContext = await service.getContext('user-context', {
param: 'value',
});
console.log(userContext.data);Working with Datasources
const datasources = await service.getDatasources();
const data = await service.getDatasource('my-datasource', {
filter: 'active',
});
console.log(data.data);OAuth2 from Client
// Check available OAuth2 providers
const providers = await service.getOAuth2Providers();
// Initiate OAuth flow
const authResult = await service.callTool('oauth2-github', {});
console.log('Auth URL:', authResult.data.authUrl);
// After authentication, call OAuth-protected tools
const profile = await service.callTool('get-github-profile', {});Using with AI SDK
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const tools = await service.getToolsAsAISDKTools();
const result = await generateText({
model: anthropic('claude-3-5-sonnet-20241022'),
prompt: 'What is the weather in San Francisco?',
tools,
maxSteps: 10,
});
console.log(result.text);🚀 Advanced Features
Contexts
Contexts provide dynamic information about the service or user state:
const userContext = {
id: 'user-preferences',
name: 'User Preferences',
description: 'User-specific preferences and settings',
getContextData: async (agentInfo, extraData) => {
// Fetch user preferences from database
return {
theme: 'dark',
language: 'en',
timezone: 'America/Los_Angeles',
};
},
};
const service = defineDAINService({
contexts: [userContext],
// ... rest of config
});Datasources
Datasources provide queryable data to AI agents:
const productDatasource = {
id: 'products',
name: 'Product Catalog',
description: 'Search and browse products',
type: 'data' as const,
input: z.object({
category: z.string().optional(),
minPrice: z.number().optional(),
maxPrice: z.number().optional(),
}),
getDatasource: async (agentInfo, params, extraData) => {
// Query database
const products = await db.products.findMany({
where: {
category: params.category,
price: {
gte: params.minPrice,
lte: params.maxPrice,
},
},
});
return products;
},
};
const service = defineDAINService({
datasources: [productDatasource],
// ... rest of config
});Widgets
Widgets display information in the DAIN UI:
const dashboardWidget = {
id: 'dashboard',
name: 'Dashboard',
description: 'Overview dashboard',
icon: 'dashboard',
size: 'lg' as const,
getWidget: async (agentInfo, extraData) => {
const stats = await getStats(agentInfo.agentId);
return {
text: 'Dashboard overview',
data: stats,
ui: {
type: 'dashboard',
children: [
{ type: 'stat', label: 'Total Users', value: stats.users },
{ type: 'stat', label: 'Active Sessions', value: stats.sessions },
],
},
};
},
};
const service = defineDAINService({
widgets: [dashboardWidget],
// ... rest of config
});Long-Running Processes
For operations that take time to complete:
import { MemoryProcessStore } from '@dainprotocol/service-sdk';
const service = defineDAINService({
processStore: new MemoryProcessStore(),
// ... rest of config
});
// In a tool handler
const processTool = {
id: 'start-analysis',
name: 'Start Analysis',
description: 'Start a long-running analysis',
handler: async (input, agentInfo, context) => {
const processId = generateId();
// Start background process
startAnalysisProcess(processId, input);
return {
text: 'Analysis started',
data: { processId },
ui: undefined,
processes: [{
id: processId,
name: 'Data Analysis',
description: 'Analyzing your data...',
type: 'one-time',
}],
};
},
};Human-in-the-Loop Actions
Request human approval or input during tool execution:
const service = defineDAINService({
onHumanActionResponse: async ({ process, stepId, actionId, responseText, data }) => {
console.log('Human responded:', responseText);
// Continue the process based on human response
},
// ... rest of config
});Custom Routes
Add custom endpoints to your service:
const service = defineDAINService({
routes: (app) => {
app.get('/health', (c) => {
return c.json({ status: 'healthy' });
});
app.post('/webhook', async (c) => {
const body = await c.req.json();
// Handle webhook
return c.json({ received: true });
});
},
// ... rest of config
});Payment Integration
import { createStripePaymentIntent } from '@dainprotocol/service-sdk/payments';
const paidTool = {
id: 'premium-feature',
name: 'Premium Feature',
description: 'A premium paid feature',
pricing: { pricePerUse: 5.00, currency: 'USD' },
handler: async (input, agentInfo) => {
// Request payment
const paymentIntent = await createStripePaymentIntent({
amount: 500, // $5.00 in cents
currency: 'usd',
metadata: {
agentId: agentInfo.agentId,
toolId: 'premium-feature',
},
});
return {
text: 'Please complete payment to continue',
data: {},
ui: undefined,
pleasePay: paymentIntent,
};
},
};📚 API Reference
Service Configuration
interface DAINServiceConfig {
metadata: {
title: string;
description: string;
version: string;
author: string;
tags: string[];
logo?: string;
};
identity: {
apiKey?: string;
publicKey?: string;
privateKey?: string;
agentId?: string;
orgId?: string;
};
tools: ToolConfig[];
toolboxes?: ToolboxConfig[];
services?: ServiceConfig[];
contexts?: ServiceContext[];
datasources?: ServiceDatasource[];
widgets?: ServiceWidget[];
agents?: ServiceAgent[];
plugins?: DainPlugin[];
oauth2?: {
baseUrl: string;
tokenStore?: StorageAdapter;
providers: Record<string, OAuth2ProviderConfig>;
};
processStore?: ProcessStoreAdapter;
routes?: (app: Hono) => void;
serverExtensions?: NodeServerExtension[];
baseUrl?: string;
exampleQueries?: {
category: string;
queries: string[];
}[];
getUserWidgets?: (agentInfo: AgentInfo, extraData?: any) => Promise<string[]>;
homeUI?: string | ((agentInfo: AgentInfo, extraData?: any) => Promise<string>);
onHumanActionResponse?: (response: {
process: Process;
stepId: string;
actionId: string;
responseText?: string;
data?: any;
}) => Promise<void>;
}Tool Configuration
interface ToolConfig<TInput extends z.ZodType, TOutput extends z.ZodType> {
id: string;
name: string;
description: string;
input: TInput;
output: TOutput;
pricing?: {
pricePerUse: number;
currency: string;
};
interface?: string;
suggestConfirmation?: boolean;
suggestConfirmationUI?: (input: z.input<TInput>) => Promise<{
success?: boolean;
ui?: any;
}>;
handler: (
input: z.input<TInput>,
agentInfo: AgentInfo,
context: ToolContext
) => Promise<{
text: string;
data: z.output<TOutput>;
ui: any | undefined;
pleasePay?: PaymentIntent;
processes?: string[] | ProcessInfo[];
}>;
handleInputError?: (
error: ZodError,
agentInfo: AgentInfo,
extraData?: any
) => Promise<ToolResponse>;
}Runtime Methods
// Node.js
await service.startNode({ port?: number });
// Deno
await service.startDeno({ port?: number });
// Cloudflare Workers
export default { fetch: service.startWorkers() };
// Next.js
const { GET, POST } = createNextDainService(config);📖 Examples
Complete Weather Service
See example/simpleWeatherService-node.ts
OAuth2 Integration
See example/oauth-client-example.ts
Long-Running Processes
Using Plugins
See example/crypto-plugin-usage.ts
Next.js Integration
See examples/nextjs-app-router
🔄 Migration Guide
If you're upgrading from an older version, see OAUTH_MIGRATION.md for OAuth2 token store migration instructions.
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License
ISC License - see LICENSE file for details
🔗 Links
💬 Support
- GitHub Issues: Report a bug or request a feature
- Discord: Join our community
- Twitter: @dainprotocol
Built with ❤️ by the DAIN Protocol team
