@sperax/plugin-sdk
v0.2.2
Published
SDK for building and publishing SperaxOS DeFi tool plugins
Downloads
687
Maintainers
Readme
@sperax/plugin-sdk
Install
npm install @sperax/plugin-sdkBuild, test, and publish SperaxOS DeFi tool plugins in under 30 minutes.
What you're building
SperaxOS has 89 built-in DeFi tools (GoPlus Security, CoinGecko, GMGN, etc.). This SDK lets you ship your own — with the same type-safe interface, rich chat rendering, and marketplace distribution.
When installed, your plugin appears in the user's AI agent as a callable tool:
User: "What's the risk level for 0xabc...?"
Agent: [calls your tool → returns structured data → renders your React component]Quickstart (10 minutes)
1. Scaffold
mkdir my-awesome-tool && cd my-awesome-tool
npm init -y
npm install @sperax/plugin-sdkCreate src/index.ts:
import { defineTool, z } from '@sperax/plugin-sdk';
export const myTool = defineTool({
id: 'my-org-awesome-tool', // unique, kebab-case
name: 'My Awesome Tool',
description: 'Does something awesome with DeFi data',
author: 'your-github-handle',
version: '1.0.0',
category: 'analytics', // see categories below
permissions: {
network: ['api.example.com'], // hosts ctx.fetch is allowed to reach
rateLimit: '60/minute', // per API name; this is the default
},
apis: [
{
name: 'getData',
description: 'Fetches awesome DeFi data for a given token',
parameters: z.object({
symbol: z.string().describe('Token ticker, e.g. "ETH"'),
limit: z.number().int().min(1).max(100).optional().default(10),
}),
},
],
});2. Implement the executor
Create src/executor/index.ts:
import { defineExecutor } from '@sperax/plugin-sdk';
import { myTool } from '../index';
export const executor = defineExecutor(myTool, {
// `ctx.fetch` is scoped to permissions.network — always use it, not the global fetch.
async getData({ symbol, limit }, ctx) {
const res = await ctx.fetch(`https://api.example.com/v1/data?symbol=${symbol}&limit=${limit}`);
if (!res.ok) return { content: `API error: ${res.status}`, success: false };
const data = await res.json();
return {
content: `Fetched ${data.items.length} records for ${symbol}`,
state: data, // passed to your renderer
success: true,
};
},
});3. Test locally
// src/__tests__/executor.test.ts
import { createToolTest } from '@sperax/plugin-sdk/testing';
import { executor } from '../executor';
const test = createToolTest(executor);
it('fetches data for ETH', async () => {
const result = await test.invoke('getData', { symbol: 'ETH' });
expect(result.success).toBe(true);
});Run: npx vitest run src/__tests__/
4. Add rich UI (optional)
Create src/client/index.tsx:
import { defineRenderer } from '@sperax/plugin-sdk';
import { myTool } from '../index';
export const renderer = defineRenderer(myTool, {
getData: ({ pluginState }) => (
<div style={{ padding: 12, borderRadius: 8, background: '#0d1117' }}>
<strong>{pluginState?.items?.length ?? 0} items</strong>
</div>
),
});5. Validate before publishing
import { validateManifest } from '@sperax/plugin-sdk';
import { myTool } from './src/index';
const result = validateManifest(myTool);
if (!result.valid) {
console.error('Errors:', result.errors);
process.exit(1);
}
console.log('Warnings:', result.warnings);6. Publish
Add your plugin to the SperaxOS registry via the marketplace UI at /community/plugins or via the API:
# POST to pluginMarketplace.submitPlugin tRPC endpoint
# Or use the web form at https://app.speraxos.com/community/plugins/submitAPI Reference
defineTool(config)
Converts your Zod parameter schemas to JSON Schema (required by the LLM) and assembles a valid BuiltinToolManifest.
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | ✓ | Unique kebab-case identifier: "my-org-tool-name" |
| name | string | ✓ | Display name shown in UI |
| description | string | ✓ | Short description for marketplace + tool tooltip |
| author | string | ✓ | Your npm/GitHub handle |
| version | string | ✓ | Semantic version: "1.0.0" |
| category | ToolCategory | ✓ | See categories below |
| apis | PluginApiConfig[] | ✓ | API surface (see below) |
| avatar | string | | Base64 SVG or URL icon |
| systemRole | string | | System prompt for the agent. Auto-generated if omitted. |
| permissions | PluginPermissions | | Declare required network access, rate limits, etc. |
Categories: trading | lending | yield | analytics | portfolio | social | productivity
API config:
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | ✓ | Method name, camelCase, e.g. "getTokenPrice" |
| description | string | ✓ | LLM-facing description of what this API does |
| parameters | z.ZodObject | ✓ | Zod schema for parameters. Use .describe() on fields. |
defineExecutor(tool, implementation)
Returns an IBuiltinToolExecutor. Each key in implementation is a method name matching an API defined in tool. Methods receive fully-typed params (inferred from your Zod schema).
defineExecutor(myTool, {
async myApiName(params, ctx) {
// params is typed from your z.object(...)
// ctx has: messageId, signal, stepContext, ... plus `fetch`, which is
// scoped to permissions.network (see "Plugin Permissions" below).
return { content: 'string shown to LLM', state: any, success: boolean };
},
});BuiltinToolResult:
{
success: boolean; // required
content?: string; // text shown to LLM in tool message
state?: any; // passed to React renderer
error?: { type, message, body };
}defineRenderer(tool, renderers) (optional)
Returns a BuiltinRender function. Each key maps to an API name. The component receives BuiltinRenderProps:
{
apiName: string;
args: TypedFromZodSchema; // the parameters passed to the API
pluginState: any; // the `state` your executor returned
content: string;
messageId: string;
}validateManifest(tool) → ValidationResult
{ valid: boolean; errors: string[]; warnings: string[] }Checks identifier format, API surface, system role quality, and schema conformance.
createToolTest(executor) (from @sperax/plugin-sdk/testing)
const test = createToolTest(executor);
// Direct invocation
const result = await test.invoke('apiName', { param: 'value' });
// Batch test cases
const results = await test.run([
{ apiName: 'foo', description: 'should succeed', params: {...}, expected: { success: true } },
]);
// Assert (throws on failure — use inside test() blocks)
await test.assert('apiName', params, { success: true });Plugin Permissions
Declare what your plugin needs:
defineTool({
// ...
permissions: {
network: ['api.myservice.com', '*.anotherapi.io'], // allowlisted hostnames
rateLimit: '60/minute', // enforced per API name by the SDK
storage: '1MB', // plugin-local KV quota, granted by the host
wallet: false, // cannot read wallet addresses
},
});What is enforced, and where
rateLimit and network are enforced by defineExecutor itself — they are not
advisory:
| Declaration | Enforced by | Behaviour |
| --- | --- | --- |
| rateLimit | The SDK, on every invoke() | A sliding window per API name. Over budget, the call returns success: false with error.type = 'RateLimitExceeded' and a retry delay; your implementation is never entered. Defaults to 60/minute. A malformed value throws at defineTool() time, not at runtime. |
| network | The SDK, through ctx.fetch | ctx.fetch rejects any host outside the allowlist with a NetworkPermissionError. Entries are exact hosts (api.example.com) or leading wildcards (*.example.com, subdomains only — not the apex). Omitting network allows everything; network: [] blocks everything. |
| storage | The SperaxOS host | Read from tool.permissions when granting the KV quota. |
| wallet | The SperaxOS host | Read from tool.permissions when deciding whether to pass wallet context. |
Always call ctx.fetch, never the global fetch. The global bypasses your
declared allowlist, and a plugin that uses it will be rejected in marketplace
review:
export const executor = defineExecutor(myTool, {
async getPrice({ symbol }, ctx) {
// ✅ scoped to permissions.network
const res = await ctx.fetch(`https://api.myservice.com/price/${symbol}`);
return { content: `${symbol}: $${(await res.json()).price}`, success: true };
},
});The normalised permission block is available on the definition as
myTool.permissions, with rateLimit already defaulted.
Package Structure
Follow this structure so SperaxOS can load your plugin at runtime:
my-awesome-tool/
├── package.json # "main": "./src/index.ts", exports: "./executor", "./client"
├── src/
│ ├── index.ts # export: myTool (PluginToolDefinition)
│ ├── executor/
│ │ └── index.ts # export: executor (IBuiltinToolExecutor)
│ └── client/ # optional
│ └── index.tsx # export: renderer (BuiltinRender)package.json exports:
{
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./executor": "./src/executor/index.ts",
"./client": "./src/client/index.tsx"
}
}Security Guidelines
- Network: Only call hostnames declared in
permissions.network. Undeclared calls are blocked. - Wallet: Never log or transmit wallet addresses unless
permissions.wallet: trueand the user approves. - No eval: Dynamic code execution (
eval,Function(),vm) is blocked. - No file system: Server executors run in a restricted environment with no FS access.
- Rate limits: Respect the declared rate limit. Excessive calls will be throttled.
- Sandboxed renderers: Client renderers run in a React context. Don't access
window.parentor inject<script>tags.
Review Process
validateManifest()passes locally- Submit via
/community/plugins/submitor thesubmitPluginAPI - Automated checks: manifest validation, dependency audit, bundle size scan
- First submission: manual review by a SperaxOS maintainer (typically 48h)
- Updates to approved plugins: automated checks only
- Approved → listed in marketplace, installable by users
Examples
See the full examples in the monorepo:
packages/example-plugin-price-feed/— simple data fetcher, no UIpackages/example-plugin-portfolio-analytics/— React chart rendererpackages/example-plugin-token-approvals/— wallet integration pattern
License
plugin-sdk is released under the Apache-2.0 license.
