@ai-billing - AI SDK Middleware that returns usage in dollars

@ai-billing

If you are building AI applications with the Vercel AI SDK and struggling to implement usage-based billing, I wanted to share a new tool I’ve been working on: ai-billing.

Basic example

Here, I simply print the cost that is returned from the OpenRouter

import {
  UIMessage,
  convertToModelMessages,
  generateText,
  wrapLanguageModel,
} from 'ai';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { createOpenRouterV3Middleware } from '@ai-billing/openrouter';
import { consoleDestination } from '@ai-billing/core';

const openrouter = createOpenRouter({
  // eslint-disable-next-line turbo/no-undeclared-env-vars
  apiKey: process.env.OPENROUTER_API_KEY,
});

const billingMiddleware = createOpenRouterV3Middleware({
  destinations: [consoleDestination()],
});

export async function POST() {
  try {
    const messages: UIMessage[] = [
      {
        id: 'test-gen-1',
        role: 'user',
        parts: [{ type: 'text', text: 'What is the capital of Sweden?' }],
      },
    ];

    const model = 'google/gemini-2.0-flash-001';

    const wrappedModel = wrapLanguageModel({
      model: openrouter(model),
      middleware: billingMiddleware,
    });

    const result = await generateText({
      model: wrappedModel,
      messages: await convertToModelMessages(messages),
    });

    return Response.json(result);
  } catch (error) {
    console.error('Generate Error:', error);
    return Response.json({ error: (error as Error).message }, { status: 500 });
  }
}

Really looking for your feedback

OpenAI example

Since OpenAI does not include cost, I use a custom pricing map to return it

Codeblock
import {
  UIMessage,
  convertToModelMessages,
  generateText,
  wrapLanguageModel,
} from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createOpenAIMiddleware } from '@ai-billing/openai';
import {
  consoleDestination,
  createObjectPriceResolver,
  ModelPricing,
} from '@ai-billing/core';

const openai = createOpenAI({
  // eslint-disable-next-line turbo/no-undeclared-env-vars
  apiKey: process.env.OPENAI_API_KEY,
});

const customPricingMap: Record<string, ModelPricing> = {
  'gpt-5': {
    promptTokens: 1.25 / 1_000_000,
    completionTokens: 10.0 / 1_000_000,
    inputCacheReadTokens: 0.125 / 1_000_000,
  },
  'gpt-4o': {
    promptTokens: 5.0 / 1_000_000,
    completionTokens: 15.0 / 1_000_000,
  },
};

const priceResolver = createObjectPriceResolver(customPricingMap);

const billingMiddleware = createOpenAIMiddleware({
  destinations: [consoleDestination()],
  priceResolver: priceResolver,
});

export async function POST() {
  try {
    const messages: UIMessage[] = [
      {
        id: 'test-gen-1',
        role: 'user',
        parts: [{ type: 'text', text: 'What is the capital of Sweden?' }],
      },
    ];

    const model = 'gpt-5';

    const wrappedModel = wrapLanguageModel({
      model: openai(model),
      middleware: billingMiddleware,
    });

    const result = await generateText({
      model: wrappedModel,
      messages: await convertToModelMessages(messages),
    });

    return Response.json(result);
  } catch (error) {
    console.error('Generate Error:', error);
    return Response.json({ error: (error as Error).message }, { status: 500 });
  }
}

@ai-billing also sends usage events to Billing Providers

Here is an infographic how:

And a codesnippet showing how to do it for:

Stripe

Codesnippet
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import {
  streamText,
  convertToModelMessages,
  UIMessage,
  wrapLanguageModel,
} from 'ai';

import { createOpenRouterV3Middleware } from '@ai-billing/openrouter';
import { consoleDestination } from '@ai-billing/core';
import { createStripeDestination } from '@ai-billing/stripe';

type BillingTags = {
  org_name?: string;
  stripe_customer_id?: string;
};

const stripeDestination = createStripeDestination<BillingTags>({
  apiKey: `${process.env.STRIPE_SECRET_KEY}`, // Make sure this is in your .env
  meterName: 'llm_usage', // The slug from your Stripe dashboard
});

const openrouter = createOpenRouter({
  // eslint-disable-next-line turbo/no-undeclared-env-vars
  apiKey: process.env.OPENROUTER_API_KEY,
});

//const consoleLogger = new ConsoleDestination();
const billingMiddleware = createOpenRouterV3Middleware<BillingTags>({
  destinations: [consoleDestination(), stripeDestination],
});

export async function POST() {
  const messages: UIMessage[] = [
    {
      id: 'test-message-123',
      role: 'user',
      parts: [
        {
          type: 'text',
          text: 'What is the capital of Sweden?',
        },
      ],
    },
  ];

  const model = 'google/gemini-2.0-flash-001';

  const wrappedModel = wrapLanguageModel({
    model: openrouter(model),
    middleware: billingMiddleware,
  });

  const result = streamText({
    model: wrappedModel,
    messages: await convertToModelMessages(messages),
    providerOptions: {
      'ai-billing-tags': {
        stripe_customer_id: 'cus_UIMLD4AuBpC8Ux', // This has to be defined as customer_mapping in your Stripe meter
        org_name: 'acme corp', // This will end up in Stripe metadata
      } as BillingTags,
    },
  });

  return result.toUIMessageStreamResponse();
}

Polar

Codesnippet
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import {
  streamText,
  convertToModelMessages,
  UIMessage,
  wrapLanguageModel,
} from 'ai';

import { createOpenRouterV3Middleware } from '@ai-billing/openrouter';
import { consoleDestination } from '@ai-billing/core';
import { createPolarDestination } from '@ai-billing/polar';

type BillingTags = {
  customer_id?: string;
  org_name?: string;
};

const polarDestination = createPolarDestination<BillingTags>({
  accessToken: process.env.POLAR_ACCESS_TOKEN, // Make sure this is in your .env
  eventName: 'llm_usage', // The slug from your Polar dashboard
  server: 'sandbox', // Good for testing!
});

const openrouter = createOpenRouter({
  // eslint-disable-next-line turbo/no-undeclared-env-vars
  apiKey: process.env.OPENROUTER_API_KEY,
});

//const consoleLogger = new ConsoleDestination();
const billingMiddleware = createOpenRouterV3Middleware<BillingTags>({
  destinations: [consoleDestination(), polarDestination],
});

export async function POST() {
  const messages: UIMessage[] = [
    {
      id: 'test-message-123',
      role: 'user',
      parts: [
        {
          type: 'text',
          text: 'What is the capital of Sweden?',
        },
      ],
    },
  ];

  const model = 'google/gemini-2.0-flash-001';

  const wrappedModel = wrapLanguageModel({
    model: openrouter(model),
    middleware: billingMiddleware,
  });

  const result = streamText({
    model: wrappedModel,
    messages: await convertToModelMessages(messages),
    providerOptions: {
      'ai-billing-tags': {
        customer_id: '4a874ea3-53ec-432d-9d55-c55bf957e18f', // This triggers internalCustomerId in Polar
        org_name: 'Acme Corp', // This will end up in Polar metadata
      } as BillingTags,
    },
  });

  return result.toUIMessageStreamResponse();
}

Lago

Codesnippet
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import {
  streamText,
  convertToModelMessages,
  UIMessage,
  wrapLanguageModel,
} from 'ai';
import { createOpenRouterV3Middleware } from '@ai-billing/openrouter';
import { consoleDestination } from '@ai-billing/core';
import { createLagoDestination } from '@ai-billing/lago';

type BillingTags = {
  userId?: string;
};

const lagoDestination = createLagoDestination<BillingTags>({
  apiKey: `${process.env.LAGO_API_KEY}`,
  apiUrl: process.env.LAGO_API_URL,
  meterCode: 'llm_usage',
});

const openrouter = createOpenRouter({
  // eslint-disable-next-line turbo/no-undeclared-env-vars
  apiKey: process.env.OPENROUTER_API_KEY,
});

const billingMiddleware = createOpenRouterV3Middleware<BillingTags>({
  destinations: [consoleDestination(), lagoDestination],
});

export async function POST() {
  const messages: UIMessage[] = [
    {
      id: 'test-message-123',
      role: 'user',
      parts: [
        {
          type: 'text',
          text: 'What is the capital of Sweden?',
        },
      ],
    },
  ];

  const model = 'openai/gpt-4o';

  const wrappedModel = wrapLanguageModel({
    model: openrouter(model),
    middleware: billingMiddleware,
  });

  const result = streamText({
    model: wrappedModel,
    messages: await convertToModelMessages(messages),
    providerOptions: {
      'ai-billing-tags': {
        userId: 'user_lago_test',
      } as BillingTags,
    },
  });

  return result.toUIMessageStreamResponse();
}

OpenMeter

Codesnippet
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import {
  streamText,
  convertToModelMessages,
  UIMessage,
  wrapLanguageModel,
} from 'ai';
import { createOpenRouterV3Middleware } from '@ai-billing/openrouter';
import { consoleDestination } from '@ai-billing/core';
import { createOpenMeterDestination } from '@ai-billing/openmeter';

type BillingTags = {
  userId?: string;
  org_name?: string;
};

const openMeterDestination = createOpenMeterDestination<BillingTags>({
  apiKey: `${process.env.OPENMETER_API_KEY}`,
});

const openrouter = createOpenRouter({
  // eslint-disable-next-line turbo/no-undeclared-env-vars
  apiKey: process.env.OPENROUTER_API_KEY,
});

const billingMiddleware = createOpenRouterV3Middleware<BillingTags>({
  destinations: [consoleDestination(), openMeterDestination],
});

export async function POST() {
  const messages: UIMessage[] = [
    {
      id: 'test-message-123',
      role: 'user',
      parts: [{ type: 'text', text: 'What is the capital of Sweden?' }],
    },
  ];

  const model = 'openai/gpt-4o';

  const wrappedModel = wrapLanguageModel({
    model: openrouter(model),
    middleware: billingMiddleware,
  });

  const result = streamText({
    model: wrappedModel,
    messages: await convertToModelMessages(messages),
    providerOptions: {
      'ai-billing-tags': {
        userId: 'user_openmeter_test',
        org_name: 'Acme Corp',
      } as BillingTags,
    },
  });

  return result.toUIMessageStreamResponse();
}

Installation

npm install @ai-billing/core @ai-billing/openrouter # Example for OpenRouter

Full-stack @ai-billing examples

Name Demo Link Repo Deploy
Chatbot (OpenRouter + Polar) View Demo GitHub Deploy with Vercel
Chatbot (OpenAI + Polar) View Demo GitHub Deploy with Vercel
Chatbot (Stripe) View Demo GitHub Deploy with Vercel