Credit Exchange
Credit exchange table for unit and currency conversions.
The exchange table is what allows a pool of abstract credits to drain across multiple discrete entitlements, and what enables margin calculation across any combination of resources.
Each entry is an object that maps one credit by name to another via a multiplication factor:
exchange: {
// credit_a_value = 0.5 * credit_b_value
credit_a: { value: 0.5, currency: 'credit_b' }
}
Every credit that defines a price is automatically added to the exchange table, and is convertable based on that price.
Remember we have a common base unit, the rune, for all prices, which allows them to be convertable.
Live Example
Go ahead and expand this example policy by clicking on it, and it will reveal the following setup:
- 2 credits defined:
- Abstract
x_credit— user-facing credit that can be used across all features & vendors - Claude Sonnet 5 AI credit to be used anywhere we call into Claude Sonnet 5 (simplified)
- Abstract
- 3 explicit exchange table entries:
- USD = 1 Rune = 1 USD (this is actually defined by default, but shown here for clarity)
- x_credit = 0.1 * USD (so 1 credit = $0.10, or $1 = 10 credits)
- euro = 1.14 * USD (1 Euro = 1.14 USD)
- 1 plan (pro plan - for demo purposes only)
- 1 entitlement (
ai_chat-> Claude Sonnet 5 AI credits)- 10k Claude Sonnet 5 tokens included with plan
- allows & charges for overage (soft limit) — anything beyond included + granted
- 1 included topup
- users on the pro plan get 50
x_creditincluded every month
- users on the pro plan get 50
- 1 entitlement (
Run it
policy: {
credits: {
x_credit: { description: "User-facing credit that can be used across the entire product" }
claude_sonnet_5: { overhead_cost: 2e-6, price: { amount: 3e-6 } }
}
exchange: {
// Included definition by default (in addition to the 'rune' itself)
usd: { value: 1, currency: 'rune' }
// Technically equivalent to setting a price on x_credit of $0.10, but for exchange purposes only
x_credit: { value: 0.1, currency: 'usd' }
// 1 Euro = 1.14 USD
euro: { value: 1.14, currency: 'usd' }
}
plans: {
pro: {
label: 'Pro Plan'
entitlements: {
ai_chat: {
description: 'AI chat feature'
limit: { credit: 'claude_sonnet_5', mode: 'soft', value: 10_000, resets: true, reset_sch: 'monthly:1' }
}
}
topups: {
included: {
description: "Pro users get 50 x_credits per month"
credit: 'x_credit'
value: 50
resets: true
included: true
reset_sch: 'monthly:1'
}
}
}
}
}import { Limitr } from '@formata/limitr';
const policy = await Limitr.new(`policy: { ... }`);
await policy.createCustomer('demo', 'pro');
await policy.ensureCustomerIncludedTopups('demo'); // adds included grant of 50 x_credit
// plan includes 10k ai_chat tokens before grants & overage, lets use 9k now
await policy.allow('demo', 'ai_chat', 9000);
console.log('Used 9k of the 10k included AI tokens - 1k left\n');
// remaining takes credit grants into account, using the exchange table!
const rem = await policy.remaining('demo', 'ai_chat');
console.log(`${Math.round(rem! * 100)/100} AI tokens left before overage starts (includes grant of x_credit)`);
console.log(`Which is 50 x_credits -> usd -> runes -> claude_sonnet_5 + 1k: ${Math.round(50 * 0.1 / 3e-6 + 1_000 * 100) / 100}\n`);
// but how much is remaining in usd & euros?
const ent: any = await policy.entitlement('demo', 'ai_chat');
const usd = await policy.creditExchange(ent.limit.credit, 'usd', rem!); // claude_sonnet_5 -> usd
const euros = await policy.creditExchange(ent.limit.credit, 'euro', rem!); // claude_sonnet_5 -> euro
console.log(`$${Math.round(usd!*1000)/1000} (€${Math.round(euros!*100)/100}) of AI spend overhead left before any usage revenue captured\n`);
// percentages are built in, also
const perc_inc = await policy.value('demo', 'ai_chat', true, false);
const perc_gnt = await policy.value('demo', 'ai_chat', true, true);
console.log(`Used ${Math.round(perc_gnt!*1000)/1000}% of total available right now, and ${perc_inc}% of included with plan`);What happened?
- Created a demo customer & granted them their included topup of 50
x_credit - Used 9k of the 10k included Claude Sonnet 5 AI tokens with Pro Plan
- Note: limit value is 10k, set to 0 if you want nothing included (grants & overage only)
- Grabbed and printed the remaining ai_chat tokens using exchanges (in the defined credit units,
claude_sonnet_5) - Converted the remaining ai_chat tokens to USD & Euros using the exchange table to see our remaining overhead before overage kicks in
- Put the current AI chat meter value for this customer in terms of 2 percentages
- Percent used of the total available to them right now (including their credit grants)
- Percent used of the included tokens in their plan (10k limit, 9k used = 90%)
Currency conversions
For exchange purposes, currencies are modeled as credits. This is another reason to keep the Rune defined as $1 USD (default definition).
The policy, unless otherwise overwritten, come with Rune rune and USD usd pre-defined, ready to use with spend caps, etc.
Limitr Cloud is completely separate and isolated for ledger and billing purposes. Additional flexibility exists on the managed side, including defining a policy exchange table, however, any rates defined or manipulated within your local policy are for client use only.
Adding explicit currency conversions
Like the live example above, adding additional currencies can be done via the exchange table:
Remember, because rune and usd are pre defined as $1, you do not need to redefine them in your table.
exchange: {
// 1 Euro = 1.14 USD
euro: { value: 1.14, currency: 'usd' }
cfa_franc: { value: 0.0017, currency: 'usd' }
}
What's needed to convert any currency to any other is a path in the exchange table to rune, which can be as many or as few steps as you need. Adding additional steps can be beneficial for modifying blocks of prices at once, like a premium_ai_models for example, that might add an additional cost multiplier to select models.
Adding real-time currency lookups
Limitr is a runtime, and runtimes do things like look up currency exchange rates when you need them to.
This involves some Stof know-how, but should be straight-forward even without prior Stof knowledge.
1. Add a Host function for currency lookups
// Load and init Limitr like normal (Limitr.cloud for Limitr Cloud)
import { Limitr } from '@formata/limitr';
const policy = await Limitr.new(`policy: { /* your policy goes here */ }`);
// Make this an HTTP fetch or whatever you need, return a JSON string with Exchange Pair
async function currencyToExchangePairJSON(currency: string): Promise<string> {
console.log('looking up currency:', currency); // just adding for visibility/testing
switch (currency) {
case ('cfa_franc'): { return `{ "value": 0.0017, "currency": "usd" }`; }
case ('euro'): { return `{ "value": 1.14, "currency": "usd" }`; }
default: { return `{ "value": 1, "currency": "rune" }`; }
}
}
// Expose the function to the Stof WASM sandbox so that we can use it in our policy runtime
policy.doc.lib('Host', 'currency_exchange_lookup', currencyToExchangePairJSON);
2. Replace the Exchange get_pair Stof function
This is the part that involves some Stof. We are going to replace the Exchange prototype's get_pair function so that we can use our (TypeScript) Host to lookup exchange rates dynamically, and cache them in the policy for one hour, when it is then re-fetched lazily.
// Could parse right into the policy.exchange, but better in the prototype obj, always under root.LimitrTypes
// Caches looked-up currency exchange rates for 1 hour each
const lookupStof = `
fn get_pair(c: str!) -> ExchangePair {
const cutoff = Time.now() - 1hr;
let def = self.get(c);
if (def != null && def.external && def.created < cutoff) { self.remove(c, shallow=false); def = null; }
if (def == null) {
const json = await ?Host.currency_exchange_lookup(c);
if (json != null) {
def = new { external: true, created: Time.now() };
parse(json, def, 'json');
self.insert(c, def);
}
}
def
}`;
policy.doc.parse(lookupStof, 'stof', policy.doc.get('root.LimitrTypes.Exchange') as string);
3. Try it out!
Now, anytime we need an exchange rate that isn't explicitely defined, it is looked it up dynamically, marked as external, and cached in our policy for 1 hour before looking it up again to capture any changes.
// Now try it out...
console.log(await policy.creditExchange('usd', 'cfa_franc', 1));
console.log(await policy.creditExchange('usd', 'cfa_franc', 1)); // cached, no additional log
console.log(await policy.creditExchange('cfa_franc', 'euro', 4258));
> bun run example.ts
looking up currency: cfa_franc
588.2352941176471
588.2352941176471
looking up currency: euro
6.349649122807018
USD spend caps
One of the most common use-cases for the exchange table is placing a cap on spend using common units (USD) across many usage steps that are not directly using those units (credits, tokens, seconds, etc.).
Spend caps are very capable and span many use-cases, however, the most common is to either observe or control USD spend.
Here's an example re-utilizing the policy defined from above:
policy: {
credits: {
x_credit: { description: "User-facing credit that can be used across the entire product" }
claude_sonnet_5: { overhead_cost: 2e-6, price: { amount: 3e-6 } }
}
exchange: {
// Included definition by default (in addition to the 'rune' itself)
usd: { value: 1, currency: 'rune' }
// Technically equivalent to setting a price on x_credit of $0.10, but for exchange purposes only
x_credit: { value: 0.1, currency: 'usd' }
// 1 Euro = 1.14 USD
euro: { value: 1.14, currency: 'usd' }
}
plans: {
pro: {
label: 'Pro Plan'
entitlements: {
ai_chat: {
description: 'AI chat feature'
limit: { credit: 'claude_sonnet_5', mode: 'soft', value: 10_000, resets: true, reset_sch: 'monthly:1' }
}
}
topups: {
included: {
description: "Pro users get 50 x_credits per month"
credit: 'x_credit'
value: 50
resets: true
included: true
reset_sch: 'monthly:1'
}
}
}
}
}await policy.createCustomer('demo', 'pro');
await policy.ensureCustomerIncludedTopups('demo');
// Add a USD spend cap of $5
await policy.addCustomerCap('demo', 5, {
cap_id: 'usd_pipeline_cap',
credit: 'usd',
ignore_grants: false, // so our 50 x_credits count towards this cap (typically you'd want true here)
overage_only: false, // so ALL usage is counted (typically you'd want true here)
});
// Now we spend - $5 / 3e-6 = 1_666_666.67 tokens
await policy.allow('demo', 'ai_chat', 1500000);
let cap = await policy.customerCap('demo', 'usd_pipeline_cap');
console.log(`Spent $${cap?.meter_value} on 1.5 MTok`);
// Any spend over our $5 cap will be blocked, even with soft limits
if (await policy.allow('demo', 'ai_chat', 1000000)) {
throw Error('should not get here');
}
const val = await policy.value('demo', 'ai_chat');
console.log(`Used ${val} tokens in pipeline`);