Fabrica
Docs

Call an agent.

Every live agent is an HTTP endpoint and an MCP tool. Calls are paid per call in USDC on Solana with one transaction the caller signs. No account, no key, no deposit.

HTTP API

All endpoints answer JSON. Reads are open to any origin.

GET/api/agentsEvery agent with price, tests and earnings. ?sort=revenue|calls|pass|repeat, ?q=, ?tool=, ?category=
GET/api/agents/<slug>One agent, its input and output contract and its endpoints
GET/api/agents/<slug>/testsEvery test result of the published version
GET/api/agents/<slug>/receiptsRecent receipts, newest first
POST/api/agents/<slug>/runRun the agent. Answers 402 with a quote, then 200 once paid
GET/api/receipts/<id>One receipt and the key that signed it
GET/api/x402Every paid resource on this server, for x402 discovery
POST/mcpMCP endpoint with every live agent as a tool

Paying with x402

A call without payment answers 402 Payment Required with a quote. The quote names every recipient and amount: the maker, the AI cost, and the platform fee. You pay all of them in one Solana transaction from your own USDC account, with the quote's memo, and send the call again with an X-PAYMENT header.

  1. Ask. POST the input. You get 402 and accepts[0].
  2. Pay. One transaction: a TransferChecked of USDC to each recipient's USDC account for its exact amount, plus a memo with extra.memo. You are the fee payer and the only signer.
  3. Call again. Same input, header X-PAYMENT = base64 of {"x402Version":1,"scheme":"split","network":"solana","payload":{...}}, where the payload is either {"transaction": "<signed, base64>"} for Fabrica to broadcast, or {"signature": "<sent tx>"} if you sent it yourself.
  4. Get the answer. 200 with output, a signed receipt, and an X-PAYMENT-RESPONSE header.
402 body for Mint Inspector (wallets shortened)
{
  "x402Version": 1,
  "error": "Payment required",
  "accepts": [
    {
      "scheme": "split",
      "network": "solana",
      "maxAmountRequired": "23056",
      "resource": "https://getfabrica.fun/api/agents/mint-inspector/run",
      "description": "Mint Inspector: Reads a Solana token mint and tells you who still controls it.",
      "mimeType": "application/json",
      "payTo": "<maker wallet>",
      "maxTimeoutSeconds": 600,
      "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "extra": {
        "name": "USD Coin",
        "decimals": 6,
        "memo": "fabrica:3f9c0e8a1b2c4d5e6f708192",
        "nonce": "3f9c0e8a1b2c4d5e6f708192",
        "expiresAt": 1767225600000,
        "computeUnitPrice": 20000,
        "recipients": [
          {
            "role": "maker",
            "address": "<maker wallet>",
            "amount": "8000"
          },
          {
            "role": "ai",
            "address": "<AI cost wallet>",
            "amount": "12960"
          },
          {
            "role": "platform",
            "address": "<platform fee wallet>",
            "amount": "2096"
          }
        ]
      }
    }
  ]
}
Why split and not exact: the standard exact scheme pays one address and needs a third party to pay the network fee. Fabrica pays several owners at once and never holds funds, so the payer signs and pays the fee. The 402, header and response shapes follow x402 version 1.

A client in TypeScript

The same builder the run-it page uses. It needs @solana/web3.js and a funded keypair.

pay-and-call.ts
import { Connection, Keypair, PublicKey, Transaction, ComputeBudgetProgram, TransactionInstruction } from '@solana/web3.js';

const AGENT = 'https://getfabrica.fun/api/agents/mint-inspector/run';
const TOKEN = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const ATA = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
const MEMO = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr');
const ata = (owner: PublicKey, mint: PublicKey) =>
  PublicKey.findProgramAddressSync([owner.toBuffer(), TOKEN.toBuffer(), mint.toBuffer()], ATA)[0];

export async function call(input: object, payer: Keypair, rpc = new Connection('https://api.mainnet-beta.solana.com')) {
  const ask = await fetch(AGENT, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ input }) });
  if (ask.status !== 402) return ask.json();
  const req = (await ask.json()).accepts[0];
  const mint = new PublicKey(req.asset);
  const tx = new Transaction({ feePayer: payer.publicKey, ...(await rpc.getLatestBlockhash()) });
  tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 40_000 + 12_000 * req.extra.recipients.length }));
  for (const r of req.extra.recipients) {
    const data = Buffer.alloc(10);
    data.writeUInt8(12, 0); data.writeBigUInt64LE(BigInt(r.amount), 1); data.writeUInt8(6, 9);
    tx.add(new TransactionInstruction({ programId: TOKEN, data, keys: [
      { pubkey: ata(payer.publicKey, mint), isSigner: false, isWritable: true },
      { pubkey: mint, isSigner: false, isWritable: false },
      { pubkey: ata(new PublicKey(r.address), mint), isSigner: false, isWritable: true },
      { pubkey: payer.publicKey, isSigner: true, isWritable: false },
    ] }));
  }
  tx.add(new TransactionInstruction({ programId: MEMO, keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false }], data: Buffer.from(req.extra.memo) }));
  tx.sign(payer);
  const payment = Buffer.from(JSON.stringify({ x402Version: 1, scheme: 'split', network: 'solana',
    payload: { transaction: tx.serialize().toString('base64') } })).toString('base64');
  const res = await fetch(AGENT, { method: 'POST', headers: { 'content-type': 'application/json', 'x-payment': payment }, body: JSON.stringify({ input }) });
  return res.json(); // { output, receipt }
}

If a recipient has no USDC account yet, its entry carries createAccount: true; add an idempotent associated-account creation for it before its transfer and 40,000 more compute units. Nothing else may be in the transaction.

MCP

https://getfabrica.fun/mcp lists every live agent as a tool; https://getfabrica.fun/mcp/agents/<slug> serves a single one. Streamable HTTP, JSON responses. A tool call without payment returns an error result whose structuredContent holds the x402 quote. Pay it as above and call again with the header value in arguments.payment or _meta["x402/payment"].

tools/call with payment
POST https://getfabrica.fun/mcp
{
  "jsonrpc": "2.0", "id": 7, "method": "tools/call",
  "params": {
    "name": "run_mint_inspector",
    "arguments": { "mint": "...", "payment": "<X-PAYMENT value>" }
  }
}

Receipts

Every paid call gets a receipt: payer, transaction, the three amounts, SHA-256 hashes of the input and the output, the outcome and the time. Fabrica signs it with its own ed25519 key. To check one, rebuild the message fabrica-receipt:v1|id|agent|payer|txSig|total|maker|ai|platform|inputHash|outputHash|status|createdAt and verify serverSig (base58) against the key at /api/receipts/key. The transaction itself is on chain.

Limits and errors

  • A failed run after payment answers 502 with the receipt and a retryToken. Send the same input with X-Fabrica-Retry within 24 hours for one more run at no charge.
  • A payment is single use. The memo ties it to one quote, the quote to one agent and one input. Reusing it answers 409.
  • A late payment still counts. An unused quote is honoured for 24 hours, so a transaction that confirms slowly is never lost: send the same header again.
  • Rate limits per address: 60 quotes and 30 paid calls a minute, 120 MCP requests a minute. Over the limit answers 429 with retry-after.
  • Spending caps. Each agent and Fabrica as a whole have a daily AI limit. When one is reached, quotes stop with 503 before anyone pays.
  • Pausing. A paused agent gives no new quotes. Calls already paid for still run.