Protocol integration standard

Add a new flavour to Cardamom

Cardamom indexes Cardano DeFi activity through protocol adapters — one per protocol. Protocol teams and community developers can build adapters by implementing the interface below and submitting via the suggestions API or a pull request.

How it works

Each protocol adapter is a small TypeScript module in lib/protocols/ that implements the ProtocolAdapter interface. Cardamom pipes every full transaction through all registered adapters during address indexing. Matching transactions are parsed into NormalisedActivity records, stored in Supabase, and surfaced through the activity API and AI context.

Adapters optionally implement getYieldStats to expose live positions and yield data via the GET /api/skills/{protocol} endpoint.

ProtocolAdapter interface

Every adapter must export an object that satisfies this interface:

// lib/protocols/types.ts

interface ProtocolAdapter {
  readonly protocol: SupportedProtocol;

  /**
   * Returns true if this transaction looks like a protocol interaction.
   * Runs on every transaction — keep it fast.
   * Check script addresses, known datum structures, or metadata labels.
   */
  detect(tx: CardanoTransaction, address: string): boolean;

  /**
   * Parse the transaction into one or more normalised activity records.
   * May return multiple records (e.g. claim + swap in a single tx).
   * Return [] when the tx does not produce useful activity for this address.
   */
  normalise(tx: CardanoTransaction, address: string): NormalisedActivity[];

  /**
   * Optional: query live positions and yield for an address.
   * Called separately during address indexing — not part of the tx parse loop.
   * Implement this to surface positions in /api/skills/{protocol}.
   */
  getYieldStats?(
    address: string,           // payment address: addr1...
    stakeAddress: string | null, // stake address: stake1... (or null for enterprise)
    blockfrostKey: string,     // Blockfrost project ID for on-chain calls
  ): Promise<ProtocolYieldStats>;
}

NormalisedActivity

Every record your normalise() returns must satisfy:

type NormalisedActivity = {
  protocol: SupportedProtocol;   // your protocol id
  action: DeFiAction;            // see action vocabulary below
  txHash: string;
  txIndex: number;               // output index relevant to this activity
  occurredAt: Date;              // block time
  address: string;               // the queried address
  assetsIn: NormalisedAsset[];   // assets the address sent / committed
  assetsOut: NormalisedAsset[];  // assets the address received
  amounts: Record<string, string>; // human-readable keyed by unit
  poolId?: string;
  positionId?: string;
  confidence: "high" | "medium" | "low";
  summary?: string; // one-line human-readable, e.g. "Swap 100 ADA → 50 MIN"
  rawRef?: Record<string, unknown>; // source data for debugging
};

type NormalisedAsset = {
  unit: string;       // "lovelace" or policyId+hexAssetName
  quantity: string;   // raw quantity string (no decimal adjustment)
  policyId?: string;
  assetName?: string; // hex
  fingerprint?: string;
};

Action vocabulary

type DeFiAction =
  | "swap"
  | "add_liquidity"
  | "remove_liquidity"
  | "stake"
  | "unstake"
  | "borrow"
  | "repay"
  | "lend"
  | "withdraw"
  | "open_cdp"
  | "add_collateral"
  | "mint"
  | "burn"
  | "liquidation"
  | "claim_reward"
  | "unknown_protocol_interaction"; // use when you can detect but not classify

ProtocolYieldStats (positions)

Return this shape from getYieldStats():

type ProtocolYieldStats = {
  protocol: SupportedProtocol;
  positions: ActivePosition[];
  queried: boolean;      // true when the provider was called
  hasPositions: boolean; // true when at least one position was found
  computedAt: string;    // ISO timestamp
  confidence: "high" | "medium" | "low";
  warnings: string[];
};

type ActivePosition = {
  protocol: SupportedProtocol;
  positionType: "lp" | "cdp" | "lending" | "borrowing" | "staking" | "yield_farming" | "unknown";
  poolId?: string;
  positionId?: string;
  assetsDeposited: PositionAsset[];
  currentValue: PositionAsset[];
  yieldEarned: PositionAsset[];
  impermanentLoss?: PositionAsset[];
  healthFactor?: number;     // for borrowing: <1 = liquidatable
  collateralRatioPct?: number; // for CDPs: e.g. 200 = 200%
  poolApyPct?: number;
  feeRatePct?: number;
  poolTvlLovelace?: string;
  confidence: "high" | "medium" | "low";
  computedAt: string;
  warnings?: string[];
};

Use the emptyYieldStats(protocol, warnings) helper when the provider is unreachable or the address has no positions.

Utility helpers

Import these from lib/protocols/utils:

import {
  buildSummary,        // generate a one-line activity summary string
  buildUnknownActivity, // create a low-confidence fallback record
  getAdaAmount,        // extract lovelace from asset array
  formatLovelace,      // "1500000" → "1.5 ₳"
  formatTokenQuantity, // format a token with known or unknown decimals
  assetLabel,          // "lovelace" → "ADA", or short asset name
} from "@/lib/protocols/utils";

// Build a summary string for any action:
const summary = buildSummary(
  "swap",
  assetsIn,      // NormalisedAsset[]
  assetsOut,     // NormalisedAsset[]
  "myprotocol",  // SupportedProtocol
  tx.fees,       // lovelace fee string (optional)
);
// → "Swap 100 ADA → 50 MELD (fee 0.19 ₳)"

Minimal example

// lib/protocols/myprotocol.ts

import type { ProtocolAdapter, NormalisedActivity } from "./types";
import { buildUnknownActivity, buildSummary } from "./utils";
import { emptyYieldStats } from "./positions";
import type { CardanoTransaction } from "../cardano/provider";

// A known script address for the protocol
const MY_SCRIPT_PREFIX = "addr1w...";

export const myprotocolAdapter: ProtocolAdapter = {
  protocol: "myprotocol" as never, // add to SupportedProtocol union first

  detect(tx: CardanoTransaction, _address: string): boolean {
    // Return true when any input or output looks like a protocol interaction
    return (
      tx.inputs.some((i) => i.address.startsWith(MY_SCRIPT_PREFIX)) ||
      tx.outputs.some((o) => o.address.startsWith(MY_SCRIPT_PREFIX))
    );
  },

  normalise(tx: CardanoTransaction, address: string): NormalisedActivity[] {
    const userOutputs = tx.outputs.filter((o) => o.address === address);
    const scriptInputs = tx.inputs.filter((i) =>
      i.address.startsWith(MY_SCRIPT_PREFIX)
    );

    if (!userOutputs.length || !scriptInputs.length) return [];

    const assetsIn = scriptInputs.flatMap((i) => i.amount);
    const assetsOut = userOutputs.flatMap((o) => o.amount);
    const action = "withdraw";

    return [
      {
        protocol: "myprotocol" as never,
        action,
        txHash: tx.hash,
        txIndex: userOutputs[0]?.outputIndex ?? 0,
        occurredAt: new Date(tx.blockTime * 1000),
        address,
        assetsIn: assetsIn.map((a) => ({ unit: a.unit, quantity: a.quantity })),
        assetsOut: assetsOut.map((a) => ({ unit: a.unit, quantity: a.quantity })),
        amounts: {},
        confidence: "medium",
        summary: buildSummary(action, assetsIn, assetsOut, "myprotocol" as never, tx.fees),
        rawRef: { scriptInputCount: scriptInputs.length },
      },
    ];
  },

  // Optional: live position data
  async getYieldStats(address, _stakeAddress, _blockfrostKey) {
    try {
      const res = await fetch(`https://api.myprotocol.io/positions/${address}`);
      if (!res.ok) return emptyYieldStats("myprotocol" as never, ["API unavailable"]);
      const json = await res.json();
      // ... map to ActivePosition[]
      return {
        protocol: "myprotocol" as never,
        positions: [],
        queried: true,
        hasPositions: false,
        computedAt: new Date().toISOString(),
        confidence: "medium",
        warnings: [],
      };
    } catch (e) {
      return emptyYieldStats("myprotocol" as never, [String(e)]);
    }
  },
};

Registration

Register your adapter in lib/protocols/index.ts to make it active during indexing:

// lib/protocols/index.ts

import { myprotocolAdapter } from "./myprotocol";

export const ALL_ADAPTERS: ProtocolAdapter[] = [
  sundaeswapAdapter,
  minswapAdapter,
  fluidtokenAdapter,
  indigoAdapter,
  liqwidAdapter,
  myprotocolAdapter, // ← add here
];

Also add the protocol to the SupportedProtocol union in lib/protocols/types.ts and to the PROTOCOL_META map in app/api/skills/[protocol]/route.ts.

Confidence guide

highProtocol is confirmed via signed metadata, canonical datum shape, or verified policy ID.
mediumProtocol is identified via well-known script address prefix and asset heuristics.
lowProtocol identified via structural pattern only — datum not decoded.

Submitting an integration

Two paths:

  1. Pull request — implement the adapter, add tests in tests/protocols/, and open a PR. Include a test address with known activity.
  2. Suggestion API — if you want to request an integration without writing code, submit via the API. Include the protocol name, website, category, and why it should be indexed.
POST /api/integration-suggestions
Content-Type: application/json

{
  "protocolName": "WingRiders",
  "website": "https://app.wingriders.com",
  "category": "dex",
  "reason": "WingRiders is one of the largest DEXes on Cardano by TVL. \
Test address with known swaps: addr1qy..."
}

Using Cardamom from an agent

The GET /api/skills endpoint returns Claude-compatible tool definitions for every Cardamom skill. Fetch it once and pass data.tools to the Anthropic API. Activity endpoints are paginated and duplicate-collapsed; leaderboard rankings are available from GET /api/leaderboard.

import Anthropic from "@anthropic-ai/sdk";

// 1. Fetch the skills manifest
const manifest = await fetch("https://cardamom.live/api/skills")
  .then(r => r.json());
const tools = manifest.data.tools;

// 2. Pass tools to Claude
const client = new Anthropic();
const msg = await client.messages.create({
  model: "claude-opus-4-7",
  max_tokens: 4096,
  tools,
  messages: [{
    role: "user",
    content: "What protocols does addr1qy... interact with most?"
  }]
});

// 3. Handle tool calls — each tool has an http field:
//    { method: "GET", path: "/api/skills/sundaeswap", params: "path+query" }
//    Route the call to https://cardamom.live{path}?address={input.address}

Full reference: /skills.md. Agent discovery: /llms.txt.