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 classifyProtocolYieldStats (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
Submitting an integration
Two paths:
- Pull request — implement the adapter, add tests in
tests/protocols/, and open a PR. Include a test address with known activity. - 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.



