packages/cloud-shared/src/lib/services/proxy/services/README.md
Credit-based billing system for reselling third-party APIs.
Provider Abstraction > Provider Lock-in
Routes should never know which provider they're using. This makes:
Every service follows the same pattern:
// 1. Define provider mapping (ONLY place that knows provider details)
const PROVIDER_PATHS = {
methodName: "/provider/specific/path"
};
// 2. Configure service (auth, rate limits, caching, pricing)
export const serviceConfig: ServiceConfig = {
id: "service-id",
auth: "apiKeyWithOrg",
rateLimit: { windowMs: 60_000, maxRequests: 100 },
cache: { maxTTL: 30, hitCostMultiplier: 0.5 },
getCost: async (body) => getServiceMethodCost("service-id", body.method)
};
// 3. Implement handler (translate generic method to provider call)
export const serviceHandler: ServiceHandler = async ({ body }) => {
const { method, params } = body;
const path = PROVIDER_PATHS[method];
// ... call provider
};
Routes just call executeWithBody():
const body = { method: "getPrice", params: { address } };
return executeWithBody(serviceConfig, serviceHandler, request, body);
The engine handles:
solana-rpc.ts)Resells Helius Solana RPC with 20% markup.
Why Helius:
Provider-agnostic design:
Pricing: CU-based, $0.01 per 10,000 CUs
market-data.ts)Multi-chain token pricing and market data.
Why Birdeye:
Provider-agnostic design:
Pricing: CU-based, $0.00001 per CU + 20% markup
rpc.ts)Multi-chain JSON-RPC proxy for Solana and EVM chains.
Why unified:
/api/v1/rpc/[chain] for all chainsArchitecture:
rpcConfigForChain(chain) returns solanaRpcConfig for Solana, builds EVM config dynamicallycalculateBatchCost utility)?network=mainnet|testnet query paramBackward compatibility:
/api/v1/solana/rpc still works (delegates to unified handler)rpcConfigForChain("solana")Supported chains:
Pricing: CU-based per provider, 20% markup, separate service_id for commodity vs premium tiers
chain-data.ts)Enhanced blockchain data for EVM chains (NFTs, tokens, transfers).
Why separate from standard RPC:
Dual-mode handler:
buildRpcParams transforms named params to positional (provider abstraction)Convenience routes:
GET /api/v1/chain/nfts/[chain]/[address] - getNFTsForOwnerGET /api/v1/chain/tokens/[chain]/[address] - getTokenBalancesGET /api/v1/chain/transfers/[chain]/[address] - getAssetTransfersChain support: EVM chains only (Solana has its own DAS-based convenience routes)
Pricing: Alchemy enhanced CU * $0.00000045 * 1.2, ranges from $0.000005 to $0.000259
Let's add Twitter API as an example:
// lib/services/proxy/services/twitter.ts
const PROVIDER_PATHS = {
getTweet: "/2/tweets/:id",
searchTweets: "/2/tweets/search/recent",
getUser: "/2/users/:id"
};
export const twitterConfig: ServiceConfig = {
id: "twitter",
name: "Twitter API",
auth: "apiKeyWithOrg",
rateLimit: { windowMs: 60_000, maxRequests: 50 },
cache: {
maxTTL: 300, // Tweets don't change, longer cache OK
hitCostMultiplier: 0.5
},
getCost: async (body) => getServiceMethodCost("twitter", body.method)
};
export const twitterHandler: ServiceHandler = async ({ body }) => {
const { method, params } = body;
const path = PROVIDER_PATHS[method];
const response = await retryFetch({
url: `https://api.twitter.com${path}`,
init: {
method: "GET",
headers: { "Authorization": `Bearer ${process.env.TWITTER_API_KEY}` }
},
// ... retry config
});
return { response };
};
INSERT INTO service_pricing (service_id, method, cost)
VALUES
('twitter', '_default', 0.001),
('twitter', 'getTweet', 0.001),
('twitter', 'searchTweets', 0.005); -- More expensive
// app/api/v1/twitter/tweet/[id]/route.ts
export async function GET(request, { params }) {
const { id } = await params;
const body = {
method: "getTweet",
params: { id }
};
return executeWithBody(twitterConfig, twitterHandler, request, body);
}
Done. Full credit billing, caching, rate limiting, and usage tracking work automatically.
Classes encourage large files:
class MarketDataService {
private config: Config;
private client: HttpClient;
async getPrice() { }
async getOHLCV() { }
async getTrades() { }
async getPortfolio() { }
// ... grows to 500+ lines
}
Functions encourage small modules:
// Each piece is independently testable and readable
const config = { /* 20 lines */ };
const handler = async () => { /* 30 lines */ };
// Routes import just what they need
import { config, handler } from "./market-data";
Why this matters for LLMs:
method field not REST paths?Option A: Pure REST
GET /api/v1/market/price/solana/EPj...
GET /api/v1/market/ohlcv/solana/EPj...
GET /api/v1/market/trades/solana/EPj...
Option B: Method field (chosen)
Body: { method: "getPrice", chain: "solana", params: { address } }
Body: { method: "getOHLCV", chain: "solana", params: { address } }
Body: { method: "getTrades", chain: "solana", params: { address } }
Why we chose B:
method for cache decisionsTrade-off: Less RESTful, but more flexible and maintainable.
Alternative: Hardcoded
const PRICES = {
getPrice: 0.00012,
getOHLCV: 0.00048
};
Why DB is better:
Trade-off: DB query latency, but we cache pricing in Redis (300s TTL).
Provider costs are volatile:
20% margin provides:
Industry comparison:
Options:
Math:
Provider cost: $0.00012
Our price: $0.00012 × 1.2 = $0.000144
Cache miss: User pays $0.000144, we pay $0.00012 → $0.000024 margin
Cache hit: User pays $0.000072, we pay $0 → $0.000072 margin
Win-win: User saves money, we increase margin
Incentives:
Cache-Control: max-age=30 to save 50%Always validate before billing:
// Bad: Bill first, validate later
await creditsService.reserve(cost);
if (!isValid(input)) throw Error(); // Credits lost!
// Good: Validate first, then bill
if (!isValid(input)) throw Error();
await creditsService.reserve(cost);
What to validate:
Never log API keys:
// Bad
logger.info(`Calling ${url}?api-key=${key}`);
// Good
const sanitized = url.replace(/api-key=[^&]+/, "api-key=***");
logger.info(`Calling ${sanitized}`);
The retryFetch utility does this automatically.
Why per-org not per-user:
Why 100 req/min default:
Key metrics:
# Unit tests (mock handler)
bun test lib/services/proxy/services/market-data.test.ts
# Integration tests (real provider)
MARKET_DATA_PROVIDER_API_KEY=test_xxx bun test tests/integration/market-data.test.ts
Why integration tests matter:
Multi-provider redundancy
const providers = [birdeye, coingecko, dexscreener];
for (const provider of providers) {
try { return await provider.getPrice(); }
catch (e) { continue; } // Try next provider
}
Smart caching
Cost optimization
Advanced features