Skip to main content
Not all protocols use the term “Points” for their rewards. Some use custom terminology like Minerals, XP, Drips, or Diamonds. This guide shows you how to implement custom terminology in your adapter.

Why Custom Terminology?

Protocols have unique branding and may call their reward systems by different names:
  • Dolomite uses Minerals
  • Bedrock uses Diamonds
  • Your protocol might use XP, Drips, Credits, or any other term
The adapter framework supports custom terminology by allowing you to wrap your data with custom labels.

How Custom Terminology Works

Instead of returning plain numbers or simple records, you wrap your data with a label that represents your custom term. This label must be consistent across both the data and total functions.

Basic Implementation

1

Wrap the data function with your custom label

Instead of returning a flat object, nest your data under your custom term:
data: (apiResponse) => {
  return {
    Minerals: {  // Custom label
      "Airdrop Amount": apiResponse.airdrop.amount,
      "Level Snapshot": apiResponse.airdrop.level_snapshot,
    },
  };
}
2

Wrap the total function with the same label

Use the exact same label in your total function:
total: (apiResponse) => ({
  Minerals: apiResponse.airdrop.amount  // Same label as data
})
3

Ensure label consistency

The label must match exactly (case-sensitive) between data and total:
  • Minerals
  • minerals ❌ (different case)
  • MINERALS ❌ (different case)
When total returns a single custom labelled object (e.g., { Minerals: 123 }), data must include the same top-level key (e.g., { Minerals: { ... } }). This ensures exports stay consistent.

Complete Example: Dolomite Adapter

Here’s the complete Dolomite adapter that uses “Minerals” instead of “Points”:
adapters/dolomite.ts
import type { AdapterExport } from "../utils/adapter.ts";
import { checksumAddress } from "viem";
import { maybeWrapCORSProxy } from "../utils/cors.ts";

const AIRDROP_URL = await maybeWrapCORSProxy(
  "https://api.dolomite.io/airdrop/regular/{address}"
);

export default {
  fetch: async (address: string) => {
    address = checksumAddress(address as `0x${string}`);

    const milestones = await (
      await fetch(AIRDROP_URL.replace("{address}", address), {
        headers: {
          "User-Agent": "Checkpoint API (https://checkpoint.exchange)",
        },
      })
    ).json();

    return milestones;
  },
  data: ({
    airdrop,
  }: {
    airdrop?: { amount: string; level_snapshot: number | null };
  }) => {
    return {
      Minerals: {
        "Airdrop Amount": airdrop ? parseFloat(airdrop.amount) ?? 0 : 0,
        "Level Snapshot": airdrop?.level_snapshot ?? 0,
      },
    };
  },
  total: ({ airdrop }: { airdrop?: { amount: string } }) => ({
    Minerals: airdrop ? parseFloat(airdrop.amount) ?? 0 : 0,
  }),
  claimable: ({ airdrop }: { airdrop?: unknown }) => Boolean(airdrop),
  deprecated: () => ({
    Minerals: 1736467200, // Jan 10th 00:00 UTC
  }),
  supportedAddressTypes: ["evm"],
} as AdapterExport;

Example: Bedrock Adapter (Diamonds)

Another example using “Diamonds” as custom terminology:
adapters/bedrock.ts
import type { AdapterExport } from "../utils/adapter.ts";
import { maybeWrapCORSProxy } from "../utils/cors.ts";
import {
  convertKeysToStartCase,
  convertValuesToNormal,
} from "../utils/object.ts";

const API_URL = await maybeWrapCORSProxy(
  "https://app.bedrock.technology/api/v2/bedrock/third-protocol/points"
);

export default {
  fetch: async (address: string) => {
    const res = await fetch(API_URL, {
      method: "POST",
      body: JSON.stringify({ address: address.toLowerCase() }),
      headers: {
        "User-Agent": "Checkpoint API (https://checkpoint.exchange)",
      },
    });
    return (await res.json()).data;
  },
  data: (data: Record<string, string>) => {
    const { address: _address, ...rest } = data;
    return { 
      Diamonds: convertKeysToStartCase(convertValuesToNormal(rest)) 
    };
  },
  total: (data: Record<string, string>) => ({
    Diamonds: parseFloat(data.totalPoint) || 0,
  }),
  supportedAddressTypes: ["evm"],
} as AdapterExport;

How It Appears in Output

When you test an adapter with custom terminology, the output reflects your custom labels:

Data Output

┌─────────────────┬───────────┐
│                 │ Minerals  │
├─────────────────┼───────────┤
│ Airdrop Amount  │ 1234.5    │
│ Level Snapshot  │ 5         │
└─────────────────┴───────────┘

Total Output

Total Points from Adapter export:
┌───────────┬────────┐
│           │ Values │
├───────────┼────────┤
│ Minerals  │ 1234.5 │
└───────────┴────────┘
Notice how “Minerals” appears instead of “Points” throughout the output.

Multiple Custom Terms

You can use multiple custom terms in the same adapter if your protocol has different types of rewards:
data: (apiResponse) => ({
  XP: {
    "Combat XP": apiResponse.combat_xp,
    "Trading XP": apiResponse.trading_xp,
  },
  Crystals: {
    "Purple Crystals": apiResponse.purple_crystals,
    "Blue Crystals": apiResponse.blue_crystals,
  },
})
With corresponding totals:
total: (apiResponse) => ({
  XP: apiResponse.total_xp,
  Crystals: apiResponse.total_crystals,
})

Validation Rules

The test script validates that custom labels in total exist in data:Valid:
data: () => ({ Minerals: { Amount: 100 } })
total: () => ({ Minerals: 100 })
Invalid:
data: () => ({ Diamonds: { Amount: 100 } })
total: () => ({ Minerals: 100 })  // Label mismatch!
When using a single custom term (not “Points”), it must appear in both data and total:Valid:
data: () => ({ XP: { Total: 100 } })
total: () => ({ XP: 100 })
Invalid:
data: () => ({ Combat: 50, Trading: 50 })
total: () => ({ XP: 100 })  // XP not in data keys!
Labels are case-sensitive and must match exactly:Valid:
data: () => ({ Minerals: { Amount: 100 } })
total: () => ({ Minerals: 100 })
Invalid:
data: () => ({ Minerals: { Amount: 100 } })
total: () => ({ minerals: 100 })  // Case mismatch!

Testing Custom Terminology

When you run the test script, it validates your custom terminology:
deno run -A test.ts adapters/dolomite.ts 0x3c2573b002cf51e64ab6d051814648eb3a305363
The test script will:
  1. Display your custom labels in the data table
  2. Show your custom labels in the total output
  3. Validate that labels match between data and total
  4. Report any label mismatches as errors

Validation Output

If labels don’t match, you’ll see an error:
Invalid total key found: Minerals
Custom total keys must also exist in the data export keys.

Common Terminology Examples

Standard implementation without custom terminology:
data: (data) => ({
  "Total Points": data.total,
  "Bonus Points": data.bonus,
})

total: (data) => data.total

Frontend Display

Your custom terminology will be reflected on the frontend:
  • Detail View: Shows your custom label as a section header
  • Total View: Displays your custom term instead of “Points”
  • Leaderboard: Uses your custom terminology in rankings
Custom terminology is purely cosmetic and doesn’t affect the underlying data structure or functionality.

Best Practices

  1. Use Protocol Branding: Match the terminology your protocol uses in its official documentation and UI
  2. Be Consistent: Use the exact same label (including capitalization) in all functions
  3. Keep It Simple: Use clear, recognizable terms that users will understand
  4. Test Thoroughly: Run the test script to validate your label consistency

Next Steps

Deprecated Points

Mark points programs as deprecated with timestamps

Testing Your Adapter

Validate your custom terminology implementation

Build docs developers (and LLMs) love