GitHub fixerprotocol.org โ†’
Guides

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

bash
npm install @fixerprotocol/sdk
# Set your API key
export FIXER_API_KEY=fxp_live_xxxxxxxx

The agent

typescript
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);
๐Ÿ’ก
Use 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

typescript
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;
}
โ„น๏ธ
Budget caps on sub-agent invocations are enforced at the gateway, a sub-agent that tries to spend beyond its cap is rejected before funds are deducted. The orchestrator is never responsible for policing sub-agent spend manually.

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

SituationRecommended mode
Internal tools, dashboards, compliance-friendly workloadsTransparent (default)
Hiding payment amounts from competitors who can read Solanaconfidential_amount
Hiding which services your agent uses (competitive intelligence)full
Full privacy but a regulator or auditor needs to verify specific paymentsfull + disclosureKey

Per-call privacy

typescript
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

typescript
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",
});
โš ๏ธ
Store your private notes securely. The 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 classRetry?Action
AuthenticationErrorNoFix API key, do not retry automatically
InsufficientFundsErrorAfter fundingTrigger wallet top-up, then retry
PolicyViolationErrorNoLog violation, skip this call, alert operator
RateLimitErrorAfter windowWait 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

typescript
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.

typescript
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 .env files 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
  • agentId is 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

  • dailyBudget set to 120% of expected daily spend, leaves headroom without unnecessary blocking
  • perCallLimit set to the maximum single-call cost for your most expensive API + 20% buffer
  • allowedDomains populated 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.violated and budget.threshold events, see Webhooks
  • Solana Explorer bookmarked for your agent wallet address, shows all transactions directly
  • Transaction txHash values logged at INFO level in your agent, correlate logs to on-chain records
  • Dashboard at app.fixerprotocol.org added to your team's monitoring rotation

Code

  • All fixer.pay() calls wrapped with retry logic and idempotencyKey: 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