dedicated service that guarantees idempotency, preventing double-charging during network retries or webhook redelivery.
4. Asset Matrix Support: The system supports hierarchical assets and bundles. A "matrix" allows monetization of intersections (e.g., "Dataset A + Model B" has a different price than individual components).
Technical Implementation
The following TypeScript implementation demonstrates a Policy Engine and Access Service. This pattern uses a rule-based evaluation system compatible with Open Policy Agent (OPA) concepts or custom logic.
1. Domain Models
// types/monetization.ts
export interface Asset {
id: string;
type: 'content' | 'code' | 'data' | 'model';
tags: string[];
matrixId?: string; // Links to asset matrix bundles
metadata: Record<string, unknown>;
}
export interface UserContext {
userId: string;
tier: 'free' | 'pro' | 'enterprise';
tokenBalance: number;
entitlements: string[];
}
export interface AccessRequest {
assetId: string;
action: 'read' | 'download' | 'execute' | 'derive';
context: UserContext;
timestamp: number;
}
export interface AccessDecision {
allowed: boolean;
reason?: string;
cost?: number; // Tokens or currency required
policyId: string;
}
export interface PricingRule {
id: string;
priority: number;
condition: (request: AccessRequest) => boolean;
action: (request: AccessRequest) => AccessDecision;
}
2. Policy Engine
// engine/policy-engine.ts
export class MonetizationPolicyEngine {
private rules: PricingRule[] = [];
constructor(rules: PricingRule[]) {
// Sort by priority for deterministic evaluation
this.rules = rules.sort((a, b) => b.priority - a.priority);
}
evaluate(request: AccessRequest): AccessDecision {
for (const rule of this.rules) {
if (rule.condition(request)) {
return rule.action(request);
}
}
// Default deny
return { allowed: false, reason: 'no_matching_policy', policyId: 'default_deny' };
}
addRule(rule: PricingRule): void {
this.rules.push(rule);
this.rules.sort((a, b) => b.priority - a.priority);
}
}
3. Rule Definitions and Access Service
// services/access-service.ts
import { MonetizationPolicyEngine, PricingRule, AccessRequest, AccessDecision } from './types';
// Example Rules
const enterpriseUnlimitedRule: PricingRule = {
id: 'enterprise_unlimited',
priority: 100,
condition: (req) => req.context.tier === 'enterprise',
action: (req) => ({ allowed: true, policyId: 'enterprise_unlimited' }),
};
const tokenConsumptionRule: PricingRule = {
id: 'token_consumption',
priority: 50,
condition: (req) => req.context.tokenBalance > 0,
action: (req) => {
// Dynamic pricing based on asset type and action
const cost = calculateTokenCost(req);
return { allowed: true, cost, policyId: 'token_consumption' };
},
};
const paywallRule: PricingRule = {
id: 'paywall_conversion',
priority: 10,
condition: (req) => req.context.tier === 'free' && req.action === 'read',
action: (req) => ({ allowed: false, reason: 'upgrade_required', policyId: 'paywall_conversion' }),
};
function calculateTokenCost(req: AccessRequest): number {
// Base cost logic; can be extended with matrix pricing
let base = req.action === 'execute' ? 5 : 1;
if (req.assetId.startsWith('premium_')) base *= 2;
return base;
}
export class AccessService {
private policyEngine: MonetizationPolicyEngine;
constructor() {
this.policyEngine = new MonetizationPolicyEngine([
enterpriseUnlimitedRule,
tokenConsumptionRule,
paywallRule,
]);
}
async checkAccess(request: AccessRequest): Promise<AccessDecision> {
const decision = this.policyEngine.evaluate(request);
if (decision.allowed && decision.cost) {
// Atomic deduction to prevent race conditions
// In production, use a distributed lock or database transaction
const deducted = await this.deductTokens(request.context.userId, decision.cost);
if (!deducted) {
return { allowed: false, reason: 'insufficient_balance', policyId: 'token_consumption' };
}
}
return decision;
}
private async deductTokens(userId: string, amount: number): Promise<boolean> {
// Implementation depends on storage (Redis atomic decrement or DB transaction)
// Returns true if deduction succeeded
return true;
}
}
4. Asset Matrix Handler
For monetizing combinations of assets, the matrix handler resolves bundle pricing.
// services/matrix-handler.ts
export class AssetMatrixHandler {
private bundles: Map<string, { assets: string[]; priceMultiplier: number }> = new Map();
registerBundle(bundleId: string, assets: string[], multiplier: number) {
this.bundles.set(bundleId, { assets, priceMultiplier: multiplier });
}
getBundlePrice(assetIds: string[]): number {
// Check if requested assets match a registered bundle
const bundle = Array.from(this.bundles.values()).find(b =>
b.assets.every(id => assetIds.includes(id))
);
if (bundle) {
// Bundle pricing logic
return this.calculateBasePrice(assetIds) * bundle.priceMultiplier;
}
return this.calculateBasePrice(assetIds);
}
private calculateBasePrice(assetIds: string[]): number {
// Sum of individual asset costs
return assetIds.reduce((sum, id) => sum + 1, 0);
}
}
Rationale
- Separation of Concerns: The
AccessService handles flow; the MonetizationPolicyEngine handles logic. Rules can be loaded from a database or configuration service, allowing non-engineers to adjust pricing via a CMS.
- Atomicity: Token deduction must be atomic. The architecture assumes a storage layer that supports atomic operations (e.g., Redis
DECRBY or SQL UPDATE ... WHERE balance >= cost).
- Extensibility: New rules (e.g., geo-based pricing, time-limited discounts) can be added without modifying core services. The matrix handler demonstrates how bundle logic integrates seamlessly.
- Auditability: Every access decision emits a decision record. This is crucial for dispute resolution and revenue recognition.
Pitfall Guide
Production monetization systems face unique failure modes. The following pitfalls are derived from real-world incidents in high-scale platforms.
-
Race Conditions in Quota Enforcement:
- Mistake: Checking balance then deducting in two separate steps without atomicity.
- Impact: Users can consume assets beyond their balance under concurrent load, causing revenue loss.
- Fix: Use atomic operations in the storage layer. If using a database, employ
UPDATE tokens SET balance = balance - :cost WHERE balance >= :cost. If using Redis, use Lua scripts or atomic commands.
-
Idempotency Failures in Webhooks:
- Mistake: Processing payment webhooks without idempotency keys.
- Impact: Payment providers retry webhooks on network errors. Duplicate processing leads to double-granting access or double-charging.
- Fix: Store a processed event log keyed by provider event ID. Validate idempotency before executing business logic.
-
Tight Coupling with Payment Providers:
- Mistake: Embedding Stripe/Paddle SDK calls directly in controllers.
- Impact: Vendor lock-in. Changing providers requires refactoring core business logic. Provider API changes break the application.
- Fix: Implement a
PaymentGateway interface. Use an adapter pattern. All provider interactions go through a dedicated service that normalizes events and exposes a consistent API to the rest of the system.
-
Ignoring Tax Compliance in Architecture:
- Mistake: Treating tax as a post-calculation step or ignoring digital service taxes (DST).
- Impact: Legal liability and revenue reconciliation errors. Different jurisdictions require different tax treatments for digital goods.
- Fix: Integrate a tax calculation service (e.g., Stripe Tax, Avalara) into the pricing engine. Tax rules should be part of the policy evaluation, not an afterthought. Store tax codes per asset.
-
Static Pricing in Dynamic Markets:
- Mistake: Hardcoding prices in configuration files.
- Impact: Inability to respond to demand fluctuations, competitor moves, or asset value changes.
- Fix: Support dynamic pricing rules. Prices can be functions of usage volume, time of day, or user segment. The policy engine should support parameterized rules that fetch current market data.
-
Revenue Leakage via Edge Cases:
- Mistake: Failing to handle partial failures, cancellations, or refunds correctly in the access layer.
- Impact: Users retain access after refunds, or access is revoked incorrectly during disputes.
- Fix: Implement a robust state machine for entitlements. Refunds and chargebacks must trigger immediate access revocation events. Ensure the telemetry pipeline captures the full lifecycle of a transaction.
-
Poor Telemetry Granularity:
- Mistake: Logging only successful payments.
- Impact: Inability to diagnose conversion drop-offs or understand why users are churning. Missing data on access denials prevents optimization of paywalls.
- Fix: Log all access decisions, including denials with reasons. Track the funnel from content view to payment initiation to success. Correlate access patterns with revenue data.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-Volume, Low-Value Content | Dynamic Token/Usage | Reduces friction; users pay for exact value. High volume justifies infra cost. | High infra cost, High LTV |
| Enterprise B2B Access | Subscription + SSO | Compliance and audit requirements. Predictable revenue. | Low infra cost, High sales effort |
| AI Model Inference | Usage-Based API | Compute costs vary; pricing must align with resource consumption. | Variable cost, Margin sensitive |
| Premium Dataset Sales | Pay-Per-Asset | One-time value exchange. Simpler user journey for data consumers. | Low infra, Medium conversion |
| Bundle/Matrix Offers | Policy Engine with Bundle Rules | Enables cross-selling and value optimization across asset combinations. | Medium infra, High ARPU |
Configuration Template
This YAML template defines a pricing policy configuration that can be loaded by the Policy Engine. It demonstrates dynamic rules and bundle definitions.
# monetization-policy.yaml
version: "1.0"
currency: "USD"
rules:
- id: "enterprise_unlimited"
priority: 100
condition:
user.tier: "enterprise"
action:
allow: true
cost: 0
- id: "dynamic_token_pricing"
priority: 50
condition:
user.token_balance: "> 0"
action:
allow: true
cost:
base: 1
multipliers:
- if: "asset.type == 'model'"
value: 5
- if: "asset.tags contains 'premium'"
value: 2
- id: "free_tier_paywall"
priority: 10
condition:
user.tier: "free"
action:
allow: false
reason: "upgrade_required"
redirect: "/pricing"
bundles:
- id: "ai_starter_kit"
assets:
- "model:llm-base"
- "dataset:training-corpus"
price_multiplier: 0.8 # 20% discount for bundle
- id: "enterprise_data_suite"
assets:
- "dataset:financial"
- "dataset:market"
- "api:analytics"
price_multiplier: 0.7 # 30% discount
tax:
default_code: "digital_goods"
rules:
- region: "EU"
code: "digital_services_eu"
rate: "vat"
Quick Start Guide
-
Initialize the Engine:
Install the policy engine package and load the configuration template.
npm install @codcompass/monetization-engine
const config = loadYaml('monetization-policy.yaml');
const engine = new MonetizationPolicyEngine(config.rules);
-
Define Asset Registry:
Register your assets with metadata.
assetRegistry.register({
id: 'model:llm-base',
type: 'model',
tags: ['premium'],
metadata: { computeCost: 0.05 }
});
-
Integrate Access Check:
Wrap asset access with the access service.
const decision = await accessService.checkAccess({
assetId: 'model:llm-base',
action: 'execute',
context: currentUser,
timestamp: Date.now()
});
if (decision.allowed) {
executeModel();
} else {
handleDenial(decision);
}
-
Configure Webhook Listener:
Set up the webhook endpoint to handle payment events.
app.post('/webhooks/payment', async (req, res) => {
const event = await webhookService.verifyAndParse(req);
await paymentService.processEvent(event);
res.sendStatus(200);
});
-
Validate with Sandbox:
Run integration tests using sandbox credentials. Verify that access decisions align with policy rules and that telemetry events are emitted correctly. Check idempotency by replaying webhook payloads.