// case_01
HiddenFees.ae
FinTech transparency at scale
A consumer-first platform that surfaces hidden charges across financial products — turning opaque fee structures into auditable, comparable data.
40+
Fee categories
Multi-bank
Data sources
Next.js 14
Core stack
// outcomes
- Normalized fee schemas across heterogeneous bank disclosures
- Sub-second comparison queries via materialized views
- Consumer-grade UX with engineering-grade data integrity
// technical_deep_dive
How we solved it
Domain-driven fee normalization
Problem
Each institution exposes fees in different formats — flat JSON, PDF tables, and legacy CSV. Without a canonical model, comparisons break down.
Solution
We introduced a FeeDescriptor pipeline: ingest → validate → map to a shared ontology → persist with provenance metadata for audit trails.
interface FeeDescriptor {
id: string;
category: "account" | "transfer" | "fx" | "card";
amount: { value: number; currency: string };
frequency: "once" | "monthly" | "annual";
source: { institutionId: string; capturedAt: string };
}
export async function normalizeFeePayload(
raw: unknown,
institutionId: string,
): Promise<FeeDescriptor> {
const parsed = FeeSchema.parse(raw);
return {
id: createFeeId(parsed),
category: mapCategory(parsed.type),
amount: { value: parsed.amount, currency: parsed.currency },
frequency: parsed.billingCycle,
source: { institutionId, capturedAt: new Date().toISOString() },
};
}Comparison API with cached aggregates
Problem
Real-time aggregation across millions of fee rows caused p95 latency spikes during peak traffic.
Solution
Materialized views refresh on a schedule; the public API reads from pre-computed comparison snapshots with stale-while-revalidate.
export async function GET(req: Request) {
const { productIds } = parseQuery(req);
const cacheKey = `compare:${productIds.sort().join(",")}`;
const cached = await redis.get(cacheKey);
if (cached) {
return Response.json(JSON.parse(cached), {
headers: { "X-Cache": "HIT" },
});
}
const snapshot = await db.comparisonSnapshot.findMany({
where: { productId: { in: productIds } },
});
await redis.setex(cacheKey, 300, JSON.stringify(snapshot));
return Response.json(snapshot, { headers: { "X-Cache": "MISS" } });
}