Practical Guides
End-to-end examples for common patterns, from building your first paid agent to running multi-agent systems in production.
Guide: Building a Research Agent
This guide builds a research agent that queries multiple paid data APIs, aggregates the results, and operates within a strict budget. It uses Dune Analytics for on-chain data, Browserbase for web scraping, and fal.ai for AI summarisation.
Setup
npm install @fixerprotocol/sdk
# Set your API key
export FIXER_API_KEY=fxp_live_xxxxxxxx
The agent
import {
FixerProtocol,
InsufficientFundsError,
PolicyViolationError,
} from "@fixerprotocol/sdk";
const fixer = new FixerProtocol({
agentId: "research-agent-v1",
apiKey: process.env.FIXER_API_KEY!,
});
// Configure guard-rails once on startup
async function init() {
await fixer.policies.set({
dailyBudget: { usdc: 25 },
perCallLimit: { usdc: 2.00 },
allowedDomains: ["api.dune.com", "api.browserbase.com", "fal.ai"],
rateLimit: { calls: 200, window: "1h" },
});
const { usdc } = await fixer.wallet.balance();
if (usdc < 5) throw new Error(`Low wallet balance: ${usdc} USDC`);
console.log(`Wallet: ${usdc} USDC ready`);
}
// Fetch on-chain data from Dune
async function fetchDuneData(queryId: number) {
const result = await fixer.pay({
endpoint: `https://api.dune.com/api/v1/query/${queryId}/results`,
method: "GET",
});
return result.data;
}
// Scrape a web page via Browserbase
async function scrapePage(url: string) {
const result = await fixer.pay({
endpoint: "https://api.browserbase.com/v1/sessions",
method: "POST",
body: { url, waitForSelector: "main" },
});
return result.data;
}
// Summarise with fal.ai
async function summarise(context: string, question: string) {
const result = await fixer.pay({
endpoint: "https://fal.run/fal-ai/any-llm",
method: "POST",
body: {
model: "anthropic/claude-sonnet-4-5",
prompt: `Context:\n${context}\n\nQuestion: ${question}`,
},
});
return (result.data as { output: string }).output;
}
// Main research task
async function research(question: string) {
console.log(`Researching: ${question}`);
const [duneData, webData] = await Promise.allSettled([
fetchDuneData(3326266), // Solana TVL query
scrapePage("https://defillama.com/chain/Solana"),
]);
const context = [
duneData.status === "fulfilled"
? JSON.stringify(duneData.value).slice(0, 2000)
: "(Dune unavailable)",
webData.status === "fulfilled"
? JSON.stringify(webData.value).slice(0, 2000)
: "(DeFiLlama unavailable)",
].join("\n\n");
const answer = await summarise(context, question);
// Log the payment trail
const { transactions } = await fixer.transactions.list({ limit: 5 });
console.log(`\nPayments made:`);
for (const tx of transactions) {
console.log(` ${tx.protocol.toUpperCase()} ยท ${tx.amount.usdc} USDC ยท ${tx.endpoint}`);
console.log(` Solana: ${tx.solanaTxLink}`);
}
return answer;
}
// Run
await init();
const answer = await research("What is the current Solana TVL and trend?");
console.log("\nAnswer:", answer);
Promise.allSettled(): not Promise.all(): for parallel paid calls. This way a failure from one API (e.g. 402 timeout) doesn't cancel the rest of your requests.
Guide: Multi-Agent Orchestration
This guide shows an orchestrator agent that delegates specialised tasks to sub-agents, each with its own budget cap. All payments appear in a traceable on-chain chain linked by parentTxHash.
Architecture
Orchestrator Agent
โโโ Budget allocation: 5 USDC per run
โ
โโโ โ Research Sub-Agent (cap: 2 USDC)
โ โโโ Dune query ยท fal.ai summary
โ
โโโ โ Writer Sub-Agent (cap: 1 USDC)
โ โโโ Report generation
โ
โโโ โ Validator Sub-Agent (cap: 0.50 USDC)
โโโ Fact-checking API calls
All payments linked on-chain via parentTxHash
Orchestrator implementation
import { FixerProtocol } from "@fixerprotocol/sdk";
const orchestrator = new FixerProtocol({
agentId: "orchestrator-prod",
apiKey: process.env.FIXER_API_KEY!,
});
async function runPipeline(topic: string) {
console.log(`Starting pipeline for: "${topic}"`);
// Step 1: Research sub-agent with 2 USDC cap
const researchResult = await orchestrator.request<{
result: { summary: string };
paymentChain: string[];
totalSpent: { usdc: number };
}>("POST", "/v1/agents/invoke", {
agentEndpoint: "https://agents.yourplatform.com/research/run",
task: { topic, depth: "medium" },
budget: { usdc: 2.00 },
});
console.log(`Research done. Spent: ${researchResult.totalSpent.usdc} USDC`);
console.log(` Tx chain: ${researchResult.paymentChain.join(" โ ")}`);
// Step 2: Writer sub-agent with 1 USDC cap
const writeResult = await orchestrator.request<{
result: { report: string };
totalSpent: { usdc: number };
}>("POST", "/v1/agents/invoke", {
agentEndpoint: "https://agents.yourplatform.com/writer/run",
task: {
topic,
research: researchResult.result.summary,
format: "markdown",
},
budget: { usdc: 1.00 },
});
console.log(`Writing done. Spent: ${writeResult.totalSpent.usdc} USDC`);
// Total spend audit โ all on-chain
const { transactions } = await orchestrator.transactions.list({ limit: 20 });
const totalSpent = transactions.reduce((sum, tx) => sum + tx.amount.usdc, 0);
console.log(`\nTotal pipeline spend: ${totalSpent.toFixed(4)} USDC`);
console.log(`Transactions on Solana:`);
transactions.forEach(tx => {
const parent = tx.parentTxHash ? ` (child of ${tx.parentTxHash.slice(0, 8)}...)` : "";
console.log(` ${tx.txHash.slice(0, 12)}...${parent} ยท ${tx.amount.usdc} USDC`);
});
return writeResult.result.report;
}
Guide: ZK Privacy in Practice
Use ZK privacy mode only for the calls that need it. Most agent workloads benefit from full transparency, ZK adds latency (<10 ms) and is intended for specific confidentiality requirements.
When to use each mode
| Situation | Recommended mode |
|---|---|
| Internal tools, dashboards, compliance-friendly workloads | Transparent (default) |
| Hiding payment amounts from competitors who can read Solana | confidential_amount |
| Hiding which services your agent uses (competitive intelligence) | full |
| Full privacy but a regulator or auditor needs to verify specific payments | full + disclosureKey |
Per-call privacy
import { FixerProtocol } from "@fixerprotocol/sdk";
const fixer = new FixerProtocol({
agentId: "trading-agent",
apiKey: process.env.FIXER_API_KEY!,
});
// Most calls: transparent (default)
const publicData = await fixer.pay({
endpoint: "https://api.coingecko.com/api/v3/simple/price",
});
// Sensitive call: hide amount and identity
const sensitiveData = await fixer.pay({
endpoint: "https://api.proprietary-alpha.com/v1/signals",
method: "POST",
body: { asset: "SOL", horizon: "1h" },
privacy: {
mode: "full",
},
});
// Store the private note โ Fixer Protocol cannot recover it if lost
await saveToSecureStorage("private_notes", sensitiveData.privateNote!);
Selective disclosure for compliance
const COMPLIANCE_PUBLIC_KEY = "7xKXtg2eH9sZ7pWb..."; // compliance officer's Solana key
// Pay with disclosure key attached
const result = await fixer.pay({
endpoint: "https://api.proprietary-alpha.com/v1/signals",
privacy: {
mode: "full",
disclosureKey: COMPLIANCE_PUBLIC_KEY,
},
});
// disclosureProof is a ZK viewing credential
// Send it to the compliance officer via secure channel
// They decrypt with their private key โ nobody else can
await notifyCompliance({
txHash: result.txHash,
disclosureProof: result.disclosureProof!,
timestamp: new Date().toISOString(),
});
// Later: generate a new disclosure for a different auditor
const { disclosureProof } = await fixer.request("POST", "/v1/disclosure", {
txHash: result.txHash,
note: result.privateNote,
disclosureKey: "AUDITOR_PUBLIC_KEY",
});
privateNote returned by private payments is not stored by Fixer Protocol. If you lose it, you cannot generate future disclosure proofs for that transaction. Use a secrets manager or encrypted database.
Guide: Error Handling Patterns
Payment failures in agent code need careful handling, some errors are transient and safe to retry, others indicate a configuration problem that retrying will not fix.
Error decision tree
| Error class | Retry? | Action |
|---|---|---|
AuthenticationError | No | Fix API key, do not retry automatically |
InsufficientFundsError | After funding | Trigger wallet top-up, then retry |
PolicyViolationError | No | Log violation, skip this call, alert operator |
RateLimitError | After window | Wait for Retry-After header, then retry |
APIError (5xx) | Yes (backoff) | Exponential backoff, max 3 retries |
| Network error (fetch failed) | Yes (backoff) | Exponential backoff, max 3 retries |
Retry helper with backoff
import {
FixerProtocol,
APIError,
AuthenticationError,
InsufficientFundsError,
PolicyViolationError,
RateLimitError,
type PayOptions,
type PayResult,
} from "@fixerprotocol/sdk";
const fixer = new FixerProtocol({
agentId: "resilient-agent",
apiKey: process.env.FIXER_API_KEY!,
});
async function payWithRetry(
options: PayOptions,
maxAttempts = 3,
): Promise {
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fixer.pay(options);
} catch (err) {
lastError = err;
// Non-retryable errors โ fail immediately
if (err instanceof AuthenticationError) {
console.error("Auth error โ check FIXER_API_KEY");
throw err;
}
if (err instanceof PolicyViolationError) {
console.warn(`Policy blocked: ${err.message}`);
throw err; // operator must fix policy, not worth retrying
}
if (err instanceof InsufficientFundsError) {
console.warn("Wallet low โ attempting top-up...");
await fixer.wallet.fund({ usdc: 10 });
// retry immediately after top-up
continue;
}
// Retryable: rate limits and server errors
const delay = err instanceof RateLimitError
? 60_000 // wait a full minute
: Math.pow(2, attempt - 1) * 500; // 500ms, 1s, 2s
if (attempt < maxAttempts) {
console.log(`Attempt ${attempt} failed โ retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
}
}
throw lastError;
}
Idempotency for safe retries
Always supply an idempotencyKey when retrying, this prevents duplicate payments if a request times out after the gateway has already processed it.
import { randomUUID } from "crypto";
// Generate once before the retry loop โ same key for all attempts
const idempotencyKey = `${fixer.agentId}-${randomUUID()}`;
const result = await payWithRetry({
endpoint: "https://api.dune.com/api/v1/query/3326266/results",
idempotencyKey, // gateway deduplicates on this key
});
Production Checklist
Review each item before deploying an agent to production.
Environment & keys
- API keys stored in a secrets manager (AWS Secrets Manager, Doppler, Vault), never in source code or
.envfiles committed to git - Separate API keys for development and production, rotate dev keys freely without affecting prod
- Key rotation procedure documented and tested, include in your incident runbook
agentIdis stable and descriptive, changing it in prod creates a new wallet and orphans the old one
Wallet & funding
- Production wallet funded with at least 3โ5ร your expected daily spend
- Low-balance alert configured in dashboard (Settings โ Alerts), notify before funds run out, not after
- Wallet address saved separately, you can send USDC directly to it from any Solana wallet if the dashboard is inaccessible
Spend policies
dailyBudgetset to 120% of expected daily spend, leaves headroom without unnecessary blockingperCallLimitset to the maximum single-call cost for your most expensive API + 20% bufferallowedDomainspopulated with all expected service hosts, blocks unintended calls from prompt injection- Policies verified via
fixer.policies.get()on agent startup, fail fast if misconfigured
Observability
- Webhooks configured for
policy.violatedandbudget.thresholdevents, see Webhooks - Solana Explorer bookmarked for your agent wallet address, shows all transactions directly
- Transaction
txHashvalues logged at INFO level in your agent, correlate logs to on-chain records - Dashboard at
app.fixerprotocol.orgadded to your team's monitoring rotation
Code
- All
fixer.pay()calls wrapped with retry logic andidempotencyKey: see Error Handling - Agent gracefully handles
PolicyViolationError: logs and continues instead of crashing - Integration test runs against the gateway (not mocked) before every production deploy
- Private notes from ZK payments stored in encrypted storage, not in logs, not in plaintext files