Files
puter/src/backend/src/services/MeteringService/README.md
T
Daniel Salazar 9394b5e204
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test (20.x) (push) Has been cancelled
test / test (22.x) (push) Has been cancelled
test / api-test (22.x) (push) Has been cancelled
docs: metering and examples (#1803)
* chore: expose meteringService methods in wrapper while not in an extension

* docs: metering and examples
2025-10-21 17:25:46 -07:00

4.6 KiB

Metering Service

This service provides all metering functionality in puter. It relies on our own KV infrastructure to track usage (note the implementation of kvStore affects performance, and atomicity, currently sqlite implementation is not atomic).

It will also slowly add functionality around credit purchasing in the future, but for now it is just metering and usage. This should be the primary, and ideally only, way to check for usage and record it.

Usage

Within Core Modules

To use the metering service within core modules, you can access it via the services object. Here's an example of how to check if an actor has enough credits for a specific usage type:

class SomeCoreModule extends BaseService {
    get #meteringService(): MeteringService {
        return this.services.get('meteringService') as MeteringService;
    }

    async someMeteredFunction(actor: Actor) {
        const hasEnoughCredits = await this.#meteringService.hasEnoughCreditsFor(actor, 'someUsageKey:units', 1000);  
        
        // ...
        
        const updatedUsage = await this.#meteringService.incrementUsage(actor, 'someUsageKey:units', 1000);
    }
}

Note you don't have to structure like that if you don't want, but it's a nice way to encapsulate the service access. You can also do:

const meteringService = this.services.get('meteringService') as MeteringService;
// or
const meteringService = Context.get('services').get('meteringService') as MeteringService;

or any other way you like to access services.

Within Extensions

To use the metering service within extensions, you can import it using the extension's import service method

/** @type {import('@heyputer/backend/src/services/MeteringService/MeteringServiceWrapper.mjs').MeteringServiceWrapper} */
const meteringService = extension.import('service:meteringService');

Note on imports

Due to the way we structure services, when importing the metering service in extensions, you get the MeteringServiceWrapper class. This is a bit of a middlestep while MeteringService is not an extension itself. Which is why you'll see some places doing:

const meteringService = this.services.get('meteringService').meteringService as MeteringService

but for usability, those same methods are exposed directly on the wrapper so you don't need to do that.

Cost maps

The metering service relies on cost maps to determine how much to charge for a given operation. Cost maps are simple JSON objects that map a usage type to a cost per unit in microcents (1 millionth of a cent). For example, a cost map for AWS Polly might look like this:

{
    "aws-polly:standard:character": 4,
    "aws-polly:neural:character": 16
}

We need to manually update these for now until we can automate it somehow. You can add more costs to the cost map as needed.

Cost overrides

In some cases, you may want to override the default cost for a specific actor, or give a cost if not provided in the cost map. you can do this by passing in the cost override when incrementing usage:

await meteringService.incrementUsage(actor, 'someUnmappedOperation:units', 1000, 5000000); // override cost to 5 cents = 5 million microcents for the whole 1000 units

Other util methods

See MeteringService.ts for more details on how metering works. Its all typescript so you can always just get intellisense on the methods.

Adding and Getting User Subscription Plans

Though the metering service itself doesn't handle subscriptions nor credit purchases (yet at least), it does emit events for extensions to provide them with the necessary data to limit usage for users. These following events are emitted:

  • metering:overrideDefaultSubscription - allows extension to override the default subscription plan for a user
  • metering:registerAvailablePolicies - allows extension to register available subscription policies/plans
  • metering:getUserSubscription - allows extension to provide the current subscription plan for a user For example on these see the extension meteringAndBilling for how to use these events to provide subscription plans.

Examples

Core Module example

See OpenAI module for an example of how to use the metering service within a core module: OpenAICompletionService.mjs

Extension example

See meteringAndBilling extension for an example of how to use the metering service within an extension: usage.js