npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sperax/plugin-sdk

v0.2.2

Published

SDK for building and publishing SperaxOS DeFi tool plugins

Downloads

687

Readme

@sperax/plugin-sdk

Install

npm install @sperax/plugin-sdk

Build, 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-sdk

Create 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/submit

API 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: true and 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.parent or inject <script> tags.

Review Process

  1. validateManifest() passes locally
  2. Submit via /community/plugins/submit or the submitPlugin API
  3. Automated checks: manifest validation, dependency audit, bundle size scan
  4. First submission: manual review by a SperaxOS maintainer (typically 48h)
  5. Updates to approved plugins: automated checks only
  6. Approved → listed in marketplace, installable by users

Examples

See the full examples in the monorepo:

License

plugin-sdk is released under the Apache-2.0 license.