Create a Plugin
This guide walks you through creating a new protocol plugin. A plugin creates a protocol fiber (submonoid M_P), registers commands into it, and optionally adds API routes for server-side protocol calls.
Prerequisites: Read Architecture: Plugin System and Concepts: Algebraic Core first.
Step 1: Scaffold from Template
cp -r src/plugins/_template src/plugins/my-protocol
The template provides the standard directory structure:
src/plugins/my-protocol/
├── index.ts # Plugin metadata & initialize()
├── commands.ts # G_p command implementations
├── types.ts # Protocol-specific TypeScript types
└── ARCHITECTURE.md
Step 2: Define Plugin Metadata
Update src/plugins/my-protocol/index.ts:
import { createProtocolFiber, addCommandToFiber } from '@/core/monoid'
import type { Plugin, ProtocolFiber } from '@/plugins/types'
import type { ExecutionContext } from '@/core/types'
import { swapCommand, quoteCommand } from './commands'
export const myProtocolPlugin: Plugin = {
metadata: {
id: 'my-protocol', // Must be unique; used as fiber ID and in command registration
name: 'My Protocol',
version: '0.2.0',
description: 'My DeFi protocol integration',
tags: ['dex', 'swap'],
},
defaultConfig: {
enabled: true,
rpcUrl: undefined,
},
async initialize(context: ExecutionContext): Promise<ProtocolFiber> {
// Create the fiber (automatically injects protocol-specific identity)
const fiber = createProtocolFiber('my-protocol', 'My Protocol')
// Register commands into the fiber
addCommandToFiber(fiber, swapCommand)
addCommandToFiber(fiber, quoteCommand)
return fiber
},
}
Critical: metadata.id must match the protocol field on every command you register and the fiber.id returned from initialize. The plugin loader enforces this invariant.
Step 3: Define Commands
Update src/plugins/my-protocol/commands.ts:
import type { Command, CommandResult } from '@/core/types'
import type { SwapResult } from './types'
export const swapCommand: Command<string, SwapResult> = {
id: 'swap',
scope: 'G_p', // Required — must be 'G_p'
protocol: 'my-protocol', // Required — must match plugin metadata.id
description: 'Swap tokens via My Protocol',
async run(args: string, context): Promise<CommandResult<SwapResult>> {
// 1. Parse args
const [amountStr, tokenIn, tokenOut] = args.split(' ')
if (!amountStr || !tokenIn || !tokenOut) {
return {
success: false,
error: new Error('Usage: swap <amount> <tokenIn> <tokenOut>'),
}
}
// 2. Guard on wallet connection
if (!context.wallet.isConnected) {
return { success: false, error: new Error('Connect a wallet first') }
}
// 3. Call API route (keeps secret server-side)
const response = await fetch('/api/my-protocol/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tokenIn, tokenOut, amount: amountStr }),
})
const result = await response.json()
if (!result.success) {
return { success: false, error: new Error(result.error) }
}
// 4. Cache state for follow-up commands
context.protocolState.set('my-protocol', {
...context.protocolState.get('my-protocol'),
lastQuote: result.data,
lastQuoteAt: Date.now(),
})
// 5. Return structured result
return { success: true, value: result.data }
},
}
export const quoteCommand: Command<string, SwapResult> = {
id: 'quote',
scope: 'G_p',
protocol: 'my-protocol',
description: 'Get a swap quote without executing',
async run(args: string, context): Promise<CommandResult<SwapResult>> {
// Similar to swap but no execution
// ...
return { success: true, value: quoteData }
},
}
Rules for G_p commands:
scopemust be exactly'G_p'(string literal).protocolmust exactly match the plugin'smetadata.id.addCommandToFiberwill throw if either is wrong.
Step 4: Define Types
Update src/plugins/my-protocol/types.ts:
export interface SwapResult {
tokenIn: string
tokenOut: string
amountIn: string
amountOut: string
fee: string
route: string
timestamp: number
}
export interface MyProtocolState {
lastQuote?: SwapResult
lastQuoteAt?: number
}
Step 5: Add API Routes (Optional)
If your protocol requires server-side API calls (to keep keys secret, normalize responses, or proxy a third-party API):
mkdir -p src/app/api/my-protocol/quote
Create src/app/api/my-protocol/quote/route.ts:
import { NextRequest, NextResponse } from 'next/server'
import { authenticateRequest } from '@/lib/auth'
import { rateLimiter } from '@/lib/rate-limit'
export async function POST(request: NextRequest) {
const authError = authenticateRequest(request)
if (authError) return authError
const limitError = await rateLimiter.check(request, 30)
if (limitError) return limitError
try {
const body = await request.json()
if (!body.tokenIn || !body.tokenOut || !body.amount) {
return NextResponse.json(
{ success: false, error: 'Missing required parameters' },
{ status: 400 }
)
}
// Call protocol API using server-side key
const quote = await fetchMyProtocolQuote(
body,
process.env.MY_PROTOCOL_API_KEY
)
return NextResponse.json({ success: true, data: quote })
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.error('[my-protocol/quote]', error)
}
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}
See Guides: Add an API Route for the complete route authoring guide.
Step 6: Add a Handler (Optional)
If your command requires wallet interaction (signing, broadcasting), create src/plugins/my-protocol/handlers.ts:
import type { HandlerMap } from '@/plugins/types'
export const myProtocolHandlers: HandlerMap = {
'swap': async (result, context, walletClient) => {
const txData = result.value
// Present transaction to wallet
const hash = await walletClient.sendTransaction({
to: txData.routerAddress,
data: txData.calldata,
value: txData.value,
})
// Track the transaction
await trackSwapTransaction({
protocol: 'my-protocol',
txHash: hash,
walletAddress: context.wallet.address,
tokenIn: txData.tokenIn,
tokenOut: txData.tokenOut,
amountIn: txData.amountIn,
})
return { success: true, pipelineOutput: { txHash: hash } }
},
}
Step 7: Register the Plugin
Add your plugin to src/plugins/index.ts:
import { myProtocolPlugin } from './my-protocol'
export const plugins = [
oneinchPlugin,
lifiPlugin,
wormholePlugin,
stargatePlugin,
myProtocolPlugin, // ← Add here
]
Step 8: Document Your Plugin
Create src/plugins/my-protocol/ARCHITECTURE.md describing:
- What the protocol does.
- The command list and typical flow.
- API route conventions.
- State shape in
protocolState['my-protocol'].
Verification Checklist
Before submitting:
-
metadata.idmatchesprotocolon every command. - All commands use
scope: 'G_p'. - No secrets in client-side code (all API keys in server routes).
- Wallet chain checks added before tx execution.
-
protocolStatecache is serializable (no functions or class instances). - Error messages are user-friendly and actionable.
- Status tracking or explorer links available for transactions.
Testing Your Plugin
# Start the dev server
pnpm dev
# In the terminal:
use my-protocol
help # Verify your commands appear
quote 1 eth usdc
swap 1 eth usdc
The plugin loader validates the fiber invariant on startup. If metadata.id !== fiber.id, you'll see an error in the console.