My fifth contribution to DefiLlama, tracking chain-level fees and revenue for Provenance blockchain.
- Protocol: Provenance Blockchain
- Type: Chain-level fees
- Start Date: 2021-05-06 (Chain genesis)
- Approach: API-based historical data with USD price fallback logic
1. Background story
a. After Variational
After Variational where maintainer couldn’t checkout my master branch, I was curious if that would be an issue again. This time I still used my master branch, but it worked fine. The maintainer could merge without problems.
Interesting: The git workflow issue in Variational might have been specific to that situation, not a hard rule.
b. Finding Provenance
I found issue #4838 requesting tracking for Provenance blockchain:
- Chain-level fees (like Taiko, my first contribution!)
- API endpoint:
https://service-explorer.provenance.io/api/v3/txs/history - Revenue model: 40% to HASH Market auction pool, 60% to validators
This felt familiar, similar to my first contribution but with API instead of events.
2. My implementation
a. The code
import { FetchOptions, ProtocolType, SimpleAdapter } from "../adapters/types";
import { CHAIN } from "../helpers/chains";
import { httpGet } from "../utils/fetchURL";
interface ITxHistoryItem {
date: string;
feepayer: null | string;
txCount: number;
feeAmountInBaseToken: number;
gasWanted: number;
gasUsed: number;
feeAmountInToken: number;
feesPaidInUsd: number | null; // ← Can be null!
maxTokenPriceUsd: number | null;
minTokenPriceUsd: number | null;
avgTokenPriceUsd: number | null; // ← Can be null!
}
const PROVENANCE_API_BASE =
"https://service-explorer.provenance.io/api/v3/txs/history";
const getPreviousDate = (dateString: string): string => {
const date = new Date(dateString + "T00:00:00Z");
date.setUTCDate(date.getUTCDate() - 1);
return date.toISOString().split("T")[0];
};
const fetch = async (_t: number, _b: any, options: FetchOptions) => {
// Fetch 2 days of data to support fallback to previous day's price
const prevDate = getPreviousDate(options.dateString);
const url = `${PROVENANCE_API_BASE}?fromDate=${prevDate}&toDate=${options.dateString}&granularity=DAY`;
const data: ITxHistoryItem[] = await httpGet(url);
const dayData = data.find((item: ITxHistoryItem) => {
const itemDate = item.date.split("T")[0];
return itemDate === options.dateString;
});
if (!dayData) {
throw new Error(`No data found for ${options.dateString}`);
}
let dailyFees: number;
// Fallback logic for USD price
if (dayData.feesPaidInUsd !== null) {
// Best case: API provides USD value directly
dailyFees = dayData.feesPaidInUsd;
} else if (dayData.avgTokenPriceUsd !== null) {
// Fallback 1: Use today's average price
dailyFees = dayData.feeAmountInToken * dayData.avgTokenPriceUsd;
} else {
// Fallback 2: Use previous day's average price
const prevDateString = getPreviousDate(options.dateString);
const prevDayData = data.find((item: ITxHistoryItem) => {
const itemDate = item.date.split("T")[0];
return itemDate === prevDateString;
});
if (prevDayData?.avgTokenPriceUsd) {
dailyFees = dayData.feeAmountInToken * prevDayData.avgTokenPriceUsd;
} else {
throw new Error(`No USD price data available for ${options.dateString}`);
}
}
return {
dailyFees,
dailyRevenue: dailyFees * 0.4, // 40% to HASH Market auction pool
};
};
const methodology = {
Fees: "Transaction fees paid by users on Provenance blockchain in HASH tokens, converted to USD.",
Revenue: "40% of transaction fees go to HASH Market auction pool (protocol revenue). 60% goes to validators.",
};
const adapter: SimpleAdapter = {
adapter: {
[CHAIN.PROVENANCE]: {
fetch,
start: "2021-05-06", // Chain genesis
},
},
protocolType: ProtocolType.CHAIN,
}; b. Key design decisions
1. Fallback price logic
The API can return null for price data, so I implemented a 3-tier fallback:
1. Use feesPaidInUsd (if available)
2. Use avgTokenPriceUsd * feeAmountInToken (if price available)
3. Use previous day's avgTokenPriceUsd * feeAmountInToken
4. Throw error if no price data available This handles cases where price oracles might be temporarily unavailable.
2. Fetch 2 days of data
const prevDate = getPreviousDate(options.dateString);
const url = `${PROVENANCE_API_BASE}?fromDate=${prevDate}&toDate=${options.dateString}&granularity=DAY`; Why? To have previous day’s price available as fallback.
3. Revenue split
dailyRevenue: dailyFees * 0.4 // 40% to protocol Revenue model:
- 40% → HASH Market auction pool (protocol revenue)
- 60% → Validators (supply side)
From my Solomon learning, I knew:
dailyRevenue= what protocol earns (40%)- Should also track
dailySupplySideRevenue= validators (60%)
But I only returned dailyRevenue. Did I make a mistake?
4. Chain addition
I had to add Provenance to the chains enum:
// helpers/chains.ts
PROVENANCE = "provenance", This was my first time modifying the core chains file!
c. My comment
Like before, I asked for validation:
“If this interpretation is incorrect, please let me know. Thank you.”
Learning to be proactive about feedback.
3. The CI issue
a. Test failure
llamabutler ran tests and got an error:
Request failed with status code 502 502 = Bad Gateway. The Provenance API endpoint was down or having issues!
This wasn’t a code problem. It was an external API availability issue.
b. Maintainer response
Despite the CI failure, noateden:
- Self-assigned the PR (December 26)
- Merged it (December 27)
The merge happened even though “1 of 2 checks passing”. The maintainer trusted the code despite temporary API issues.
4. What this shows
a. Code quality improvements
The maintainer merged despite CI failure because:
- Code structure was correct
- Error handling was proper
- Fallback logic was sound
- The 502 error was external, not code-related
Trust earned: My code quality was good enough that maintainers could see past temporary API issues.
b. Git workflow: master branch again
Interesting observation: I still used my master branch (like Variational), but this time:
- Maintainer could checkout and work with it
- No complaints about branch structure
- Merged successfully
Lesson refined: The Variational issue might not have been about “never use master”, but rather a specific technical issue that time. Master branch CAN work if there are no conflicts or checkout problems.
c. Domain knowledge application
From previous contributions, I correctly:
- Revenue taxonomy (
dailyRevenue= protocol share) - SimpleAdapter pattern
- Chain-level adapter structure
- API-based fetching with proper error handling
5. Potential improvement: supply side revenue
Looking at my code, I only returned:
return {
dailyFees,
dailyRevenue: dailyFees * 0.4, // Protocol (40%)
}; What I could have added:
return {
dailyFees,
dailyRevenue: dailyFees * 0.4, // Protocol (40%)
dailySupplySideRevenue: dailyFees * 0.6, // Validators (60%)
}; This would show:
- 40% goes to HASH Market (protocol)
- 60% goes to validators (supply side)
Similar to how I learned in Solomon that fees can split between protocol and supply side.
Why didn’t I add it? Probably focused on matching the issue requirements which mentioned “Fee/Revenue” but not explicitly supply side breakdown.
6. Technical highlights
a. Date handling
const getPreviousDate = (dateString: string): string => {
const date = new Date(dateString + "T00:00:00Z"); // ← UTC timezone
date.setUTCDate(date.getUTCDate() - 1);
return date.toISOString().split("T")[0];
}; Proper UTC handling to avoid timezone issues!
b. Type safety
interface ITxHistoryItem {
feesPaidInUsd: number | null; // ← Explicitly nullable
avgTokenPriceUsd: number | null;
// ...
} TypeScript interfaces that match API reality (null values possible).
c. Defensive programming
if (!dayData) {
throw new Error(`No data found for ${options.dateString}`);
}
if (prevDayData?.avgTokenPriceUsd) { // ← Optional chaining
dailyFees = dayData.feeAmountInToken * prevDayData.avgTokenPriceUsd;
} else {
throw new Error(`No USD price data available for ${options.dateString}`);
} Clear error messages and safe optional chaining.
7. Comparison with previous contributions
a. Growth progression:
Taiko (Event-based chain fees)
- Reviewer helped with calculations
- Merged
Bitfi (Multi-protocol)
- Protocol slug confusion
- Research used
Solomon (API + revenue taxonomy)
- Revenue classification learned
- Merged after fixes
Variational (Git workflow)
- Workflow issue, code good
- Used in new PR
Provenance (API + chain fees + fallback logic)
- Merged despite CI failure
- Git workflow correct
- Code quality trusted
b. Key evolution:
| Contribution | Main Challenge | Resolution | Trust Level |
|---|---|---|---|
| Taiko | Fee calculation | Reviewer fixed | Learning |
| Bitfi | Protocol structure | Maintainer refactored | Contributing |
| Solomon | Revenue taxonomy | Fixed after feedback | Improving |
| Variational | Git workflow | Code reused | Code trusted |
| Provenance | External API failure | Merged anyway | Quality trusted |
8. Resources
9. Key takeaways
a. What I did right:
- Proper git workflow (feature branch)
- Defensive programming (3-tier fallback logic)
- Type safety (proper nullable types)
- Error handling (clear error messages)
- Documentation (clear methodology)
- Date handling (UTC timezone awareness)
b. What went well:
- Merged despite CI failure - maintainer trusted code quality
- No code review needed - structure was solid
- Applied all previous learnings successfully
c. Potential enhancement:
// Could add:
dailySupplySideRevenue: dailyFees * 0.6 // Validators' 60% Show the complete revenue split, not just protocol share.
d. The milestone:
First contribution merged without code changes required!
Previous contributions:
- Taiko: Reviewer fixed calculation
- Bitfi: Maintainer refactored
- Solomon: Fixed after feedback
- Variational: Workflow issue
- Provenance: Merged as-is
This marks a turning point. Code quality is now consistently good enough to merge without modifications!
10. Lessons learned
a. External dependencies
502 errors are not always your fault!
When API endpoints fail:
- It’s not necessarily code issues
- Maintainers can see the difference
- Good error handling helps diagnose external issues
b. Fallback logic is valuable
The 3-tier price fallback showed:
- Thinking ahead about edge cases
- Handling data unavailability gracefully
- Production-ready mindset
c. Trust is earned
Being able to merge despite CI failure shows:
- Consistent quality builds trust
- Maintainers can evaluate code independently
- Past contributions create credibility
d. Git workflow clarification
Updated understanding:
- Master branch worked fine for Provenance
- Variational’s issue might have been specific/temporary
- Feature branches are recommended but not always mandatory
- What matters more: code quality + clean commit history
11. Final thoughts
Provenance marks a milestone: This is the first contribution that was merged without any code changes required.
The journey:
- Taiko: Needed calculation fix
- Bitfi: Full refactor
- Solomon: Structure + concept fixes
- Variational: Workflow issue (code was good)
- Provenance: Merged as-is
Each contribution built on previous learnings:
- Technical patterns (SimpleAdapter, API fetching)
- Domain knowledge (revenue taxonomy, chain fees)
- Process (git workflow, error handling)
- Communication (asking for feedback)
The 502 error during CI didn’t stop the merge because the code quality spoke for itself. That’s the goal. Write code good enough that maintainers trust it even when external factors cause test failures.
Finally! After four contributions with various issues:
- Taiko: Needed calculation fix
- Bitfi: Full refactor by maintainer
- Solomon: Revenue taxonomy corrections
- Variational: Git workflow closed
Provenance: Merged as-is. No corrections. No refactoring. No issues.
This is what consistent learning looks like. Each mistake taught something. Each feedback improved the next attempt. From needing heavy fixes to being trusted enough to merge despite external CI failures.
Not perfect, but ready. That’s progress!