Add a Command
This guide covers adding a single command to an existing protocol plugin. For creating a new plugin from scratch, see Guides: Create a Plugin.
Step 1: Define the Command Object
In src/plugins/[protocol]/commands.ts:
import type { Command, CommandResult } from '@/core/types'
export const statusCommand: Command<string, StatusResult> = {
id: 'status',
scope: 'G_p',
protocol: 'my-protocol', // Must match the plugin's metadata.id exactly
description: 'Check the status of the last transaction',
async run(args: string, context): Promise<CommandResult<StatusResult>> {
// Read cached state
const state = context.protocolState.get('my-protocol')
if (!state?.lastTxHash) {
return {
success: false,
error: new Error('No recent transaction. Run a swap first.'),
}
}
// Fetch status
const response = await fetch(`/api/my-protocol/status?tx=${state.lastTxHash}`)
const result = await response.json()
if (!result.success) {
return { success: false, error: new Error(result.error) }
}
return { success: true, value: result.data }
},
}
Required fields:
id— unique within the fiber (string, no colons).scope: 'G_p'— exact string literal, not a variable.protocol— exact match of the plugin'smetadata.id.run— async function accepting(args: string, context: ExecutionContext).
Step 2: Register with addCommandToFiber
In src/plugins/[protocol]/index.ts, import and register:
import { statusCommand } from './commands'
async function initialize(context: ExecutionContext): Promise<ProtocolFiber> {
const fiber = createProtocolFiber('my-protocol', 'My Protocol')
addCommandToFiber(fiber, swapCommand)
addCommandToFiber(fiber, quoteCommand)
addCommandToFiber(fiber, statusCommand) // ← Add here
return fiber
}
addCommandToFiber validates and will throw if:
command.scope !== 'G_p'command.protocol !== fiber.id
Step 3: Decide on API Route vs Direct SDK
Use an API route if:
- The call requires a secret API key.
- The response needs server-side transformation.
- You want authentication and rate limiting.
- The SDK must run server-side (e.g., Wormhole SDK).
Use direct client-side code if:
- The call is a public read-only RPC (
eth_getBalance, etc.). - You're using wagmi hooks directly.
- There's no secret involved.
Most protocol integrations use API routes for quotes and SDK calls, and direct wagmi calls for transaction submission.
Step 4: Handle Protocol State
If your command reads or writes cached data:
// Read
const cached = context.protocolState.get('my-protocol')?.lastQuote
// Write (always use spread to avoid overwriting other fields)
context.protocolState.set('my-protocol', {
...context.protocolState.get('my-protocol'),
lastStatus: statusResult,
lastStatusAt: Date.now(),
})
Rules:
- Only store serializable data (no functions, no Promise objects).
- Include
lastUpdatedtimestamps if you need staleness detection. - Invalidate quotes when
context.wallet.chainIdchanges.
Step 5: Pre-flight Quality Checks
Before considering the command done:
- Input parsing handles missing/invalid args with a helpful usage message.
- Output schema is deterministic (same input → same output shape).
- Error messages are user-facing strings, not raw exception messages.
- Uses the standard API response envelope (
{ success, data|error }) when calling routes. - Guards on
context.wallet.isConnectedif the command requires a wallet. - No secrets hardcoded in the command file.
Command Scope Rules
| Scope | Where defined | Who registers it |
|---|---|---|
G_core | src/core/commands.ts | Core — not plugins |
G_alias | Future | Core — not plugins |
G_p | src/plugins/[protocol]/commands.ts | Plugin via addCommandToFiber |
Never use G_core scope in a plugin. If you want a shorthand alias (like cprice for CoinPaprika), it should either be a G_core command registered by core (not ideal), or a protocol-local alias that resolves when the user is inside the fiber.
Example: Adding a Status Command to Stargate
// src/plugins/stargate/commands.ts
export const statusCommand: Command = {
id: 'status',
scope: 'G_p',
protocol: 'stargate',
description: 'Check the status of the last bridge transaction',
async run(args, context) {
const state = context.protocolState.get('stargate')
if (!state?.lastTxHash) {
return {
success: false,
error: new Error('No recent bridge transaction found. Run bridge first.'),
}
}
const response = await fetch(`/api/stargate/status?tx=${state.lastTxHash}`)
const result = await response.json()
if (!result.success) {
return { success: false, error: new Error(result.error) }
}
return {
success: true,
value: {
status: result.data.status,
explorerUrl: result.data.explorerUrl,
},
}
},
}
Then in src/plugins/stargate/index.ts:
addCommandToFiber(fiber, quoteCommand)
addCommandToFiber(fiber, bridgeCommand)
addCommandToFiber(fiber, statusCommand) // ← New
After restarting the dev server:
user@stargate> status
→ Status: CONFIRMED
→ Explorer: https://...