back to docs
contributions

Solomon USDv

Track Solomon USDv staking yield distribution

My third contribution to DefiLlama, tracking yield distributions for Solomon Protocol’s sUSDv (staked USDv) on Solana.

  • Protocol: Solomon USDv
  • Chain: Solana
  • Type: Staking yield distribution (API-based)
  • Start Date: 2025-06-04
  • Approach: Fetch yield data from Solomon Labs API
Status

Status: Merged

PR: #5292

Merged: December 22, 2024

1. Background story

a. Learning from previous mistakes

After the Bitfi experience where I misunderstood protocol slug structure, I wanted to apply those learnings:

  • Verify protocol structure first
  • Read documentation carefully
  • Understand fee/revenue model before coding

b. Choosing Solomon Protocol

I found Solomon Protocol which offers:

  • USDv: Stablecoin backed by delta-neutral basis trading
  • sUSDv: Staked USDv that receives 100% of yield generated
  • Clear API endpoint for yield distributions
  • Simple model: all yield → stakers (no protocol revenue cut)

This seemed straightforward. Track yield distributions from their public API.

c. Initial research

From Solomon’s documentation:

  • Revenue Model: 100% yield from delta-neutral basis trading goes to sUSDv holders
  • Data Source: https://data.solomonlabs.io/api/solomon-protocol/staking/yield-distributions
  • Start Date: June 4, 2025 (first yield distribution)
  • Chain: Solana

The model was clear:

  • Fees: Yield from backing assets
  • Revenue: None (no protocol takes a cut)
  • Supply Side Revenue: All fees go to sUSDv stakers

2. Initial implementation

a. My original code (commit 7f2cb81)

import { Adapter, FetchOptions } from "../adapters/types";
import { CHAIN } from "../helpers/chains";
import fetchURL from "../utils/fetchURL";

interface YieldDistribution {
  yieldAmount: string;
  transactionTimestamp: number;
  allTimeApy: string;
}

interface YieldDistributionsResponse {
  data: YieldDistribution[];
}

const fetch = async (options: FetchOptions) => {
  const { startTimestamp, endTimestamp } = options;

  // Fetch all yield distributions
  const response: YieldDistributionsResponse = await fetchURL(
    "https://data.solomonlabs.io/api/solomon-protocol/staking/yield-distributions?limit=1000"
  );

  // Filter distributions within the time range
  const distributions = response.data.filter(
    (d) => d.transactionTimestamp >= startTimestamp * 1000 &&
      d.transactionTimestamp < endTimestamp * 1000
  );

  // Sum up yield amounts for the day (amounts are in raw units with 9 decimals for USDC)
  const totalYield = distributions.reduce(
    (sum, d) => sum + Number(d.yieldAmount) / 1e9,
    0
  );

  const dailyFees = options.createBalances();
  dailyFees.addUSDValue(totalYield);

  // All yield goes to stakers (sUSDv holders)
  const dailyHoldersRevenue = options.createBalances();
  dailyHoldersRevenue.addUSDValue(totalYield);

  return {
    dailyFees,
    dailyRevenue: dailyHoldersRevenue,  // ← Used dailyHoldersRevenue
    dailyHoldersRevenue,                 // ← Added this field
  };
};

const adapter: Adapter = {
  version: 2,  // ← Used version 2
  adapter: {
    [CHAIN.SOLANA]: {
      fetch,
      start: '2025-06-04',
    },
  },
  methodology: {
    Fees: 'Yield generated from basis trading strategies (funding rates on perpetual futures).',
    Revenue: 'All yield is distributed to sUSDv holders (staked USDv).',
    HoldersRevenue: 'Yield earned by sUSDv token holders from the protocol\'s delta-neutral basis trading strategy.',
  },
};

export default adapter;

b. What I thought

I was confident this was correct because:

  • API endpoint was documented and public
  • Yield distribution model was clear (100% to stakers)
  • Start date matched first distribution (June 4, 2025)
  • Timestamp filtering handled correctly (API uses milliseconds, adapter uses seconds)
  • Decimal conversion accounted for (USDC has 9 decimals, divide by 1e9)
  • Methodology clearly explained the model

3. The evolution: three commits

a. Commit 1 (my original): what I thought was right

Key characteristics:

  • Used Adapter type with version: 2
  • Nested adapter structure: adapter: { [CHAIN.SOLANA]: { fetch, start } }
  • Used dailyHoldersRevenue field
  • Set dailyRevenue: dailyHoldersRevenue (thinking all yield = revenue)
  • Client-side filtering with ?limit=1000
  • Fetch function signature: async (options: FetchOptions)

My reasoning:

  • “All yield goes to holders, so that’s revenue”
  • “Version 2 adapter = more modern”
  • “Holders revenue = appropriate field for staker earnings”

b. Commit 2 (my fix, d7a8adb): simplified structure

After reviewer feedback, I changed to:

import { SimpleAdapter, FetchOptions } from "../adapters/types";  // Changed type

const fetch = async (_timestamp: number, _chainBlocks: any, options: FetchOptions) => {
  // Added standard parameters
  const { startTimestamp, endTimestamp } = options;

  // Changed API call to use query parameters
  const response = await fetchURL(
    `https://api.solomon.finance/yield-distributions?startTimestamp=${startTimestamp}&endTimestamp=${endTimestamp}`
  ) as YieldDistributionsResponse;

  // Simplified data processing
  let totalYield = 0;
  for (const distribution of response.data) {
    totalYield += distribution.totalYield;
  }

  const dailyFees = options.createBalances();
  dailyFees.addUSDValue(totalYield);

  const dailySupplySideRevenue = options.createBalances();  // Renamed from dailyHoldersRevenue
  dailySupplySideRevenue.addUSDValue(totalYield);

  return {
    dailyFees,
    dailyRevenue: dailySupplySideRevenue,  // Still thought this was right
    dailySupplySideRevenue,                 // Used correct field name
  };
};

const adapter: SimpleAdapter = {
  version: 1,  // Simplified to version 1
  fetch,
  chains: [CHAIN.SOLANA],  // Flat structure
  start: '2025-06-04',
  // Removed methodology (added by reviewer later)
};

Changes I made:

  1. Adapter → SimpleAdapter
  2. Version 2 → Version 1
  3. Flat structure instead of nested
  4. dailyHoldersRevenue → dailySupplySideRevenue
  5. API call with query parameters instead of client-side filtering
  6. Standard fetch function signature with _timestamp and _chainBlocks

c. Commit 3 (reviewer fix, d17ff46 “no revenue”): the real issue

Reviewer noateden made the final critical change:

return {
  dailyFees,
  dailyRevenue: 0,  // ← Changed from dailySupplySideRevenue to 0
  dailySupplySideRevenue: dailyFees,
};

And added methodology:

const adapter: SimpleAdapter = {
  version: 1,
  fetch,
  chains: [CHAIN.SOLANA],
  start: '2025-06-04',
  methodology: {
    Fees: 'All yield from backing assets.',
    Revenue: 'No revenue share from fees.',  // ← Key clarification
    SupplySideRevenue: 'All fees are distributed to sUSDv stakers.',
  }
};

The critical fix:

  • dailyRevenue: 0 instead of dailyRevenue: dailySupplySideRevenue
  • This clarifies: protocol takes zero revenue, all goes to supply side

4. The test results

a. CI test outputs

When the CI ran, llamabutler bot showed:

🦙 Running SOLOMON-USDV adapter 🦙
---------------------------------------------------
SOLOMON-USDV 👇
Backfill start time: 6/4/2025
Daily fees: 0.00
Daily revenue: 0.00
Daily supply side revenue: 0.00

All zeros. Even after my second commit (fix), the tests still showed zeros.

This suggests the data issues weren’t just about structure. There might have been actual data availability or API endpoint issues at the time of testing.

b. Reviewer feedback

noateden gave specific feedback on my first commit:

Line 36-39: “USDv is not governance token, I won’t count yields as dailyHoldersRevenue here, I will count yields for dailySupplySideRevenue instead”

Line 49-50: “please use version 1 adapter”

What this meant:

  1. dailyHoldersRevenue is for governance token holders earning from protocol
  2. dailySupplySideRevenue is for yield-bearing asset holders (like sUSDv stakers)
  3. Version 2 adapter is overkill for this simple case
  4. Version 1 (SimpleAdapter) is more appropriate

c. My iterations

  1. Commit 1 (Original): Used version 2, dailyHoldersRevenue
  2. Commit 2 (My fix): Changed to version 1, dailySupplySideRevenue, but still set dailyRevenue = dailySupplySideRevenue
  3. Commit 3 (Reviewer fix): Changed to dailyRevenue = 0

Even after my fix, I still didn’t understand that dailyRevenue = 0 for protocols that don’t take a cut.

5. What went wrong: the real issues

a. Conceptual misunderstanding: revenue vs supply side revenue

My mistake: dailyRevenue: dailyHoldersRevenue

I thought:

  • “100% yield goes to stakers”
  • “All yield = revenue”
  • “So dailyRevenue should equal the yield amount”

Reality:

  • Protocol Revenue = what the protocol itself earns
  • Supply Side Revenue = what goes to liquidity providers/stakers
  • Solomon takes zero protocol revenue
  • All yield = supply side, not protocol revenue

The fix: dailyRevenue: 0 and dailySupplySideRevenue: dailyFees

This is a fundamental conceptual error about DefiLlama’s revenue taxonomy.

b. Adapter type confusion

My mistake: Used Adapter with version 2 and nested structure

const adapter: Adapter = {
  version: 2,
  adapter: {
    [CHAIN.SOLANA]: {
      fetch,
      start: '2025-06-04',
    },
  },
};

Reality: Should use SimpleAdapter with flat structure

const adapter: SimpleAdapter = {
  version: 1,
  fetch,
  chains: [CHAIN.SOLANA],
  start: '2025-06-04',
};

Lesson: Different adapter types for different use cases:

  • SimpleAdapter: Single-chain or simple multi-chain
  • Adapter (version 2): Complex multi-chain with different logic per chain

c. API query strategy

My approach: Fetch all data with ?limit=1000, filter client-side

const response = await fetchURL(
  "https://data.solomonlabs.io/api/solomon-protocol/staking/yield-distributions?limit=1000"
);

const distributions = response.data.filter(
  (d) => d.transactionTimestamp >= startTimestamp * 1000 &&
    d.transactionTimestamp < endTimestamp * 1000
);

Better approach: Let API filter with query parameters

const response = await fetchURL(
  `https://api.solomon.finance/yield-distributions?startTimestamp=${startTimestamp}&endTimestamp=${endTimestamp}`
);

Why better:

  • Less data transferred
  • API does the filtering (more efficient)
  • No risk of missing data if there are >1000 records
  • No need for client-side timestamp conversion

d. Field naming confusion: HoldersRevenue vs SupplySideRevenue

Evolution of understanding:

  1. First attempt: dailyHoldersRevenue

    • I thought: “sUSDv holders get the yield = holders revenue”
    • Reviewer correction: “USDv is not governance token”
    • Problem: HoldersRevenue is for governance token holders who earn from protocol operations
  2. Second attempt: dailySupplySideRevenue

    • Correctly identified this is supply-side (stakers providing liquidity)
    • But still set dailyRevenue: dailySupplySideRevenue (wrong)
    • Missed: Protocol revenue = 0
  3. Final version:

    • dailyRevenue: 0 (protocol takes nothing)
    • dailySupplySideRevenue: dailyFees (all goes to stakers)

The key distinction:

FieldWho Gets ItExample
dailyHoldersRevenueGovernance token holdersGMX holders earning from GMX protocol fees
dailySupplySideRevenueYield-bearing asset holderssUSDv stakers earning yield from basis trading

Solomon case:

  • sUSDv = yield-bearing staked asset (like aUSDC, stETH)
  • NOT a governance token
  • Therefore: dailySupplySideRevenue

Lesson: Learn DefiLlama’s revenue taxonomy:

  • dailyFees: Total fees generated
  • dailyRevenue: What protocol earns (can be 0!)
  • dailyProtocolRevenue: Subset of revenue that goes to protocol treasury
  • dailySupplySideRevenue: What goes to LPs/stakers (yield-bearing positions)
  • dailyHoldersRevenue: What goes to governance token holders

6. What I learned

a. Critical conceptual lessons

  • Revenue taxonomy: The biggest mistake wasn’t code. It was not understanding DefiLlama’s revenue model:

    dailyFees = total fees collected
    ├─ dailyRevenue = protocol's share
    │  └─ dailyProtocolRevenue = to treasury
    └─ dailySupplySideRevenue = to LPs/stakers

    For Solomon: Fees = 100%, Revenue = 0%, SupplySideRevenue = 100%

  • “All yield to stakers” ≠ “Protocol Revenue”: Just because users earn yield doesn’t mean the protocol earns revenue. Solomon passes through 100% to stakers, earning nothing itself.

b. Technical lessons

  • Adapter Types Matter:

    • SimpleAdapter: Use for single-chain or uniform multi-chain
    • Adapter (v2): Use when each chain needs different logic
    • I used v2 when v1 was appropriate
  • API Query Strategy:

    • Fetch all + filter client-side (my approach)
    • Let API filter with query params (reviewer’s fix)
    • Server-side filtering is more efficient and scalable
  • Standard Function Signatures:

    // Wrong
    const fetch = async (options: FetchOptions) => { }
    
    // Correct
    const fetch = async (_timestamp: number, _chainBlocks: any, options: FetchOptions) => { }

    DefiLlama expects standard signatures even if you don’t use all parameters

c. Process lessons

  • Read the taxonomy docs first: Before implementing, I should have studied DefiLlama’s fee/revenue definitions. I jumped straight to coding.

  • Reviewer fixes are teaching moments: The commit message “no revenue” was a hint. I was still treating supply-side revenue as protocol revenue.

  • Incremental improvements: Even though my second commit fixed structure issues, I still missed the conceptual issue. Progress happens in steps.

c. What went right

Despite the issues, several things worked well:

  • Correctly identified this as an API-based adapter (not event-based)
  • Understood the revenue model (100% to stakers, 0% protocol)
  • Used TypeScript interfaces for API response
  • Clear methodology documentation
  • Applied learnings from previous contributions

7. Evolution comparison

AspectCommit 1 (Original)Commit 2 (My Fix)Commit 3 (Final)
Adapter TypeAdapter (v2)SimpleAdapter (v1)SimpleAdapter (v1)
StructureNested adapter: {}Flat chains: []Flat chains: []
Fetch Signature(options)(_timestamp, _chainBlocks, options)(_timestamp, _chainBlocks, options)
API Query?limit=1000 + filterQuery paramsQuery params
Revenue FielddailyHoldersRevenuedailySupplySideRevenuedailySupplySideRevenue
dailyRevenue= dailyHoldersRevenue= dailySupplySideRevenue= 0
MethodologyIncludedRemovedAdded back

8. Questions I should have asked

Before submitting, I should have asked:

  1. Revenue Taxonomy:

    • What’s the difference between dailyRevenue and dailySupplySideRevenue?
    • If 100% goes to stakers, does that mean protocol revenue is 0?
    • Where can I find DefiLlama’s definitions for these fields?
  2. Adapter Types:

    • When should I use SimpleAdapter vs Adapter?
    • What’s the difference between version 1 and version 2?
    • Is nested structure adapter: { [CHAIN]: {} } still recommended?
  3. Function Signatures:

    • What’s the standard fetch function signature?
    • Why include _timestamp and _chainBlocks if I don’t use them?
  4. API Best Practices:

    • Should I fetch all data and filter, or use API query params?
    • How do I handle pagination for large datasets?
    • What if the API has rate limits?

9. The final result

Despite the issues during review, the PR was merged successfully

This means:

  • The core approach was sound (API-based tracking)
  • Structural issues were fixable
  • With reviewer’s help on revenue taxonomy, it works correctly
  • The adapter now works in production

a. What worked:

  • PR merged (not closed like Bitfi)
  • Responsive to feedback (pushed fixes within 1 day)
  • API-based approach was correct
  • Start date and data source were accurate

b. What needed fixing:

  • Adapter type (v2 → v1)
  • Adapter structure (nested → flat)
  • Revenue classification (supply side ≠ protocol revenue)
  • API query strategy (fetch all → query params)
  • Function signature (missing standard params)

c. The key lesson:

Conceptual understanding > Code correctness

My code structure issues were easily fixable. The harder lesson was understanding DefiLlama’s revenue taxonomy. This is domain knowledge that can’t be guessed. It must be learned.

10. Resources

11. Key takeaways

a. The main lesson: revenue taxonomy

The core issue wasn’t code. It was conceptual.

I needed to learn:

Fees = Total collected
├─ Protocol Revenue (what protocol keeps)
│  └─ Protocol Treasury
└─ Supply Side Revenue (what goes to LPs/stakers)

For Solomon: 0% protocol, 100% supply side

b. Technical improvements made:

IssueLearning
Used Adapter v2Use SimpleAdapter for simple cases
Nested structureFlat structure for single/simple chains
Wrong function signatureAlways include _timestamp, _chainBlocks
?limit=1000 + filterLet API filter with query params
dailyRevenue = yieldProtocol revenue ≠ user earnings

c. Growth trajectory:

  1. Taiko

    • Simple chain fees
    • Event-based tracking
    • Merged with reviewer help on fee calculation
  2. Bitfi

    • Multi-protocol complexity
    • Learned: UI ≠ backend structure
    • Closed, but contributed research
  3. Solomon

    • API-based tracking (new pattern)
    • Learned: Revenue taxonomy
    • Merged after fixes

d. Progress indicators:

  • Learning from mistakes (applied Bitfi lessons)
  • Responsive to feedback (same-day fixes)
  • New patterns (API vs events)
  • Conceptual depth (revenue models)

e. For next time:

  1. Study taxonomy first - Read DefiLlama’s field definitions
  2. Check adapter patterns - Look at similar existing adapters
  3. Ask conceptual questions - “Does 100% to users = 0% protocol revenue?”
  4. Verify assumptions - SimpleAdapter vs Adapter, v1 vs v2

The fact that it merged means progress! Each contribution builds understanding of both technical patterns and domain concepts.

Command Palette
Search for a command to run...