Docs

@breeja/sdk

Cross-chain USDC settlement for people and agents. One call quotes a route, signs a gasless permit, and submits — no permits, nonces, chain IDs, or gas to reason about.

For agents

Everything on this page is machine-readable in one fetch, and the same rail is reachable over MCP for any agent host that speaks it.

llms.txt

The full API surface — SDK, HTTP API, MCP, x402 — in one machine-readable file, published at the docs root.

/llms.txt →
MCP server

@breeja/mcp exposes quote, pay, status, history, and chains as MCP tools with spend caps.

Jump to MCP config →

Install

terminal
npm install @breeja/sdk

Quick start

pay internally quotes routes, selects one, builds the EIP-3009 typed data, requests a signature from signer, posts to the relayer, and returns once accepted.

quickstart.ts
import { Breeja } from "@breeja/sdk";

const breeja = new Breeja({ apiKey: process.env.BREEJA_API_KEY! });

const payment = await breeja.pay({
  from: "base-sepolia",
  to: "arbitrum-sepolia",
  amount: "10.00",
  recipient: "0xAbC0000000000000000000000000000000dEaD",
  signer: {
    type: "private-key",
    key: process.env.PAYER_PRIVATE_KEY as `0x${string}`,
  },
});

console.log(payment.id, payment.status, payment.destExplorerUrl);

Chains

Accept both human slugs and numeric chain IDs. Slugs are stable across environments; chain IDs are not.

types.ts
type ChainSlug =
  | "base-sepolia"
  | "arbitrum-sepolia"
  | "optimism-sepolia"
  | "hedera-testnet"
  | "arc-testnet"
  | "ethereum-sepolia";

type ChainRef = ChainSlug | number;

Amounts

Decimal strings in token units — "10.00" is ten USDC. Never a JavaScript number; floating-point money is a correctness bug, not a style preference.

amounts.ts
// Decimal strings in token units, never a JS number.
const amount = "10.00"; // ten USDC

await breeja.pay({
  from: "base-sepolia",
  to: "arbitrum-sepolia",
  amount,
  recipient: "0xAbC0000000000000000000000000000000dEaD",
  signer: { type: "private-key", key: process.env.PAYER_PRIVATE_KEY as `0x${string}` },
});

quote()

Ranked viable routes with fees and ETAs, backed by POST /quote. quote.routes is ordered best-first under the caller's preference; quote.recommended is routes[0] when viable.

quote.ts
const quote = await breeja.quote({
  from: "base-sepolia",
  to: "arbitrum-sepolia",
  amount: "500.00",
});

for (const route of quote.routes) {
  console.log(route.type, route.feeAmount, route.estimatedSeconds, route.viable);
}

console.log(quote.recommended); // routes[0] when viable, else null

pay()

Sign and submit a payment, backed by POST /pay. Idempotent on the EIP-3009 nonce.

pay.ts
import { Breeja } from "@breeja/sdk";

const breeja = new Breeja({ apiKey: process.env.BREEJA_API_KEY! });

const payment = await breeja.pay({
  from: "base-sepolia",
  to: "arbitrum-sepolia",
  amount: "10.00",
  recipient: "0xAbC0000000000000000000000000000000dEaD",
  signer: {
    type: "private-key",
    key: process.env.PAYER_PRIVATE_KEY as `0x${string}`,
  },
});

console.log(payment.id, payment.status, payment.destExplorerUrl);

Signers

pay() takes a signer, not a raw key wherever that can be avoided. The custom variant is a plain function, so any wallet or key-management backend can plug in without the SDK depending on it.

types.ts
type Signer =
  | { type: "viem"; account: Account }
  | { type: "private-key"; key: `0x${string}` }
  | { type: "custom"; address: `0x${string}`; signTypedData: (data: TypedData) => Promise<`0x${string}`> };
Privy embedded wallets

Breeja's own /pay widget uses this to let a user pay by signing in with email or a social account instead of installing a browser extension. Privy issues an embedded wallet on login; useSignTypedData from @privy-io/react-auth signs the same EIP-3009 TransferWithAuthorization payload the widget already builds for a browser-extension wallet, wrapped as a custom signer.

usePrivySigner.ts
import { usePrivy, useSignTypedData, useWallets } from "@privy-io/react-auth";
import type { Signer, TypedData } from "@breeja/sdk";

// Wraps a Privy embedded wallet (email or social login, no extension) as
// the SDK's "custom" signer. The SDK never imports Privy — this hook is
// the only place that does.
function usePrivySigner(): Signer | null {
  const { wallets } = useWallets();
  const { signTypedData } = useSignTypedData();
  const wallet = wallets.find((w) => w.walletClientType === "privy") ?? wallets[0];
  if (!wallet) return null;

  return {
    type: "custom",
    address: wallet.address as `0x${string}`,
    async signTypedData(data: TypedData) {
      const { signature } = await signTypedData(
        { domain: data.domain, types: data.types, primaryType: data.primaryType, message: data.message },
        { address: wallet.address },
      );
      return signature as `0x${string}`;
    },
  };
}

ENS resolution

Pass an ENS name as recipient in pay() or quote() and the SDK resolves it to an address before building the EIP-3009 authorization. Resolution is a real RPC call against ENS's Universal Resolver on Ethereum mainnet (the same resolver architecture ENSv2 formalizes) -- it never runs through model output. The widget and the SDK share this one implementation, so a name that resolves in the SDK resolves the same way in the payment widget.

pay-with-ens.ts
import { Breeja } from "@breeja/sdk";

const breeja = new Breeja({ apiKey: process.env.BREEJA_API_KEY! });

// recipient can be an ENS name instead of a hex address. The SDK resolves
// it against ENS's real Universal Resolver on Ethereum mainnet before
// building the EIP-3009 authorization -- a live RPC call, never a guess.
const payment = await breeja.pay({
  from: "base-sepolia",
  to: "arbitrum-sepolia",
  amount: "10.00",
  recipient: "vitalik.eth",
  signer: {
    type: "private-key",
    key: process.env.PAYER_PRIVATE_KEY as `0x${string}`,
  },
});

console.log(payment.recipient); // resolved 0x address, not the name

resolveEnsName and resolveEnsAddress are also exported standalone, for a frontend or an agent that wants to resolve without paying. Reverse resolution (address to primary name) is what powers "paid to alice.eth" wherever Breeja shows a payment recipient -- the raw address stays visible alongside it, never hidden.

resolve.ts
import { resolveEnsName, resolveEnsAddress } from "@breeja/sdk";

// Forward: name -> address
const address = await resolveEnsName("vitalik.eth");
// 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

// Reverse: address -> primary name, for display ("paid to alice.eth"
// instead of a bare hex string). Returns null if no primary name is set.
const name = await resolveEnsAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
// "vitalik.eth"
Agent identity

The stronger play is treating an ENS name as an agent's identity: register a name for the agent's payment address so agent-to-agent payments address mytradingbot.eth rather than a hex string. Register today with the official ENS CLI or through app.ens.domains if you prefer a UI. Point the name's address record at the agent's Breeja payment address, and every Breeja surface that reverse-resolves a recipient will show the name from then on.

register-agent-identity.sh
# Register an ENS name for an agent's payment address using the
# official ENS CLI (or app.ens.domains for a point-and-click flow).
npx @ensdomains/ens-cli register mytradingbot.eth \
  --owner 0xYourAgentPaymentAddress \
  --set-address 0xYourAgentPaymentAddress

# Once registered, the agent's Breeja payments resolve as mytradingbot.eth
# everywhere Breeja shows a recipient -- the widget, the payment tracker,
# and any breeja.history() output -- instead of a bare hex string.

This build resolves against Ethereum mainnet's live ENS registry through ENS's Universal Resolver, the same proxy address ENSv2 deploys behind on both mainnet and Sepolia. ENSv2 itself is live in beta on Sepolia today (contracts verified on-chain against ensdomains/contracts-v2) but has not yet reached mainnet, so production resolution here targets the real, currently-live registry rather than a beta namespace that could change before launch.

status()

One-shot current state, backed by GET /status/:id.

status.ts
const payment = await breeja.status("pay_abc123");

console.log(payment.status); // "pending_deposit" | "deposit_confirmed" | "released" | "failed"
console.log(payment.sourceExplorerUrl, payment.destExplorerUrl);

watch()

Subscribe to transitions; returns an unsubscribe function. Push, not poll — an agent does not burn a loop waiting.

watch.ts
const stop = breeja.watch(payment.id, (update) => {
  if (update.status === "released") {
    console.log("settled", update.destTxHash);
    stop();
  }
});

// Call stop() any time to unsubscribe early — e.g. on component unmount.

history()

Past payments by address, backed by a subgraph. Not yet backed by the relayer — it throws today. This is a missing feature, not a payment failure; do not catch it as a BreejaError.

history.ts
// Not yet backed by the relayer — throws today, not a BreejaError.
// Do not catch it as a payment failure.
try {
  await breeja.history({ address: "0xAbC0000000000000000000000000000000dEaD" });
} catch (error) {
  console.log("history() is not yet implemented:", error);
}

chains()

Supported chains, tokens, and live liquidity, backed by GET /chains. Discoverable at runtime — an agent must not hardcode a chain list.

chains.ts
const chains = await breeja.chains();

for (const chain of chains) {
  console.log(chain.slug, chain.name, chain.poolLiquidity);
}

// Never hardcode a chain list — new chains appear here without an SDK upgrade.

Errors

Codes are stable and match the relayer's rejection reasons and the Solidity custom errors. Branch on code, never on message text.

errors.ts
import { Breeja, BreejaError } from "@breeja/sdk";

const breeja = new Breeja({ apiKey: process.env.BREEJA_API_KEY! });

try {
  await breeja.pay({
    from: "base-sepolia",
    to: "arbitrum-sepolia",
    amount: "10.00",
    recipient: "0xAbC0000000000000000000000000000000dEaD",
    signer: { type: "private-key", key: process.env.PAYER_PRIVATE_KEY as `0x${string}` },
  });
} catch (error) {
  if (error instanceof BreejaError) {
    switch (error.code) {
      case "InsufficientLiquidity":
        console.log("pool too shallow right now, try cctp preference");
        break;
      case "PoolPaused":
        console.log("fast pool paused, retry later or use trustless preference");
        break;
      default:
        console.log(error.code, error.message);
    }
  } else {
    throw error;
  }
}

Idempotency

pay is idempotent on the EIP-3009 nonce. Calling it twice with the same signed permit returns the same payment id and does not double-release. Agents retrying on network failure are the expected case.

idempotency.ts
// Safe to retry on network failure — same signed permit, same payment id,
// no double release.
async function payWithRetry(request: Parameters<typeof breeja.pay>[0]) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await breeja.pay(request);
    } catch (error) {
      if (attempt === 2) throw error;
    }
  }
  throw new Error("unreachable");
}

MCP

@breeja/mcp exposes the rail to any MCP-speaking agent — the concrete "an AI agent pays another agent" demonstration.

mcp-config.json
{
  "mcpServers": {
    "breeja": {
      "command": "npx",
      "args": ["-y", "@breeja/mcp"],
      "env": {
        "BREEJA_API_KEY": "…",
        "BREEJA_SIGNER_KEY": "…",
        "BREEJA_MAX_PAYMENT_USDC": "50",
        "BREEJA_MAX_SESSION_USDC": "200"
      }
    }
  }
}
ToolPurposeMutating
breeja_quoteRanked routes for a paymentno
breeja_payExecute a cross-chain payment — moves real fundsyes
breeja_statusState of one paymentno
breeja_historyPast payments for an address (not yet backed)no
breeja_chainsSupported chains and liquidityno

breeja_pay states plainly that it moves real funds, returns explorer URLs, and enforces both a per-call and a per-session cumulative spend cap read from BREEJA_MAX_PAYMENT_USDC / BREEJA_MAX_SESSION_USDC. Read-only tools are annotated readOnlyHint: true so a host can auto-approve them without auto-approving spending.

x402

POST /pay sits behind an x402 flow: an agent requests a paid resource, receives 402 Payment Required with payment details, settles through Breeja, and retries with proof. Cross-chain settlement is the differentiator — the agent holds funds on one chain, the resource server is paid on another, and neither side handles gas.

sequence
Agent                    Resource server              Breeja
  │  GET /resource              │                       │
  │ ───────────────────────────>│                       │
  │  402 + payment details      │                       │
  │ <───────────────────────────│                       │
  │  pay()                                              │
  │ ───────────────────────────────────────────────────>│
  │  payment id + proof                                 │
  │ <───────────────────────────────────────────────────│
  │  GET /resource + proof      │                       │
  │ ───────────────────────────>│                       │
  │  200 + resource             │                       │
  │ <───────────────────────────│                       │

The resource server verifies via breeja.status(paymentId) that the payment is released, paid to its own address, and meets the price — it never trusts the id alone. Full runnable demo: examples/x402/ (server/ and agent/).

Live chains

Read live from GET /chains — what the SDK's chains() method returns right now, so the examples above reflect reality.

Examples

Complete, runnable examples — pick a tab.

One call: quote, sign, submit, and return once accepted.

pay.ts
import { Breeja } from "@breeja/sdk";

const breeja = new Breeja({ apiKey: process.env.BREEJA_API_KEY! });

const payment = await breeja.pay({
  from: "base-sepolia",
  to: "arbitrum-sepolia",
  amount: "10.00",
  recipient: "0xAbC0000000000000000000000000000000dEaD",
  signer: {
    type: "private-key",
    key: process.env.PAYER_PRIVATE_KEY as `0x${string}`,
  },
});

console.log(payment.id, payment.status, payment.destExplorerUrl);
Deep link to one method

Every section above also has its own URL, e.g. /docs/pay or /docs/watch.