Implementing Variable API Route Costs with Unkey

Learn how to efficiently implement variable costs for API routes using Unkey, an open-source API developer platform.

Written by

James Perkins

Published on

We all know API endpoints aren't created equal; some are more expensive than others. They can be more expensive due to compute cycles, or they could be financially more expensive because you are using a different AI model. When selling access to an API based upon usage with endpoints that could be very expensive, you need to be able to charge credits based on what endpoint a user is requesting data from. Below is an example of two routes: a cheap route and a more expensive one.

Overrides example

How to implement cost based API usage

Below is an oversimplified version of tracking usage based upon an API key this doesn't include:

  • API key creation
  • Setting the credit amount for a user
  • Safe way to look up an API key to get the usage base.

However, the idea is the following:

  1. User requests one of the endpoints
  2. We retrieve the key from the header
  3. We get their remaining credits. If they have zero, we return an error
  4. We reduce the credit amount based on the endpoint used
  5. We update our Redis storage with the new value
  6. We finally return the data from our endpoint
1import { serve } from '@hono/node-server'
2import { Hono } from 'hono'
3import { createClient } from 'redis'
4import { createMiddleware } from 'hono/factory'
5
6const app = new Hono()
7
8
9const client = createClient();
10
11const verifyKeyWithCost = (cost: number) => createMiddleware(async (c, next) => {
12  const key = c.req.header('X-API-Key');
13  if (!key) {
14    return c.json({ error: "No API key provided" }, 401);
15  }
16
17  const credits = await client.get(key);
18  if (credits === null) {
19    c.json({ error: "Internal server error" }, 500);
20  }
21
22  const remainingCredits = parseInt(credits) - cost;
23  if (remainingCredits < 0) {
24    c.json({ error: "API key usage exceeded" }, 429);
25  }
26
27  await client.set(key, remainingCredits.toString());
28  await next();
29});
30
31// Cheap endpoint (costs 1 credit)
32app.get('/cheap-endpoint', verifyKeyWithCost(1), (c) => {
33  return c.json({
34    message: "Accessed cheap endpoint",
35    cost: 1,
36  });
37});
38
39// Expensive endpoint (costs 5 credits)
40app.get('/expensive-endpoint', verifyKeyWithCost(5), (c) => {
41  return c.json({
42    message: "Accessed expensive endpoint",
43    cost: 5,
44  });
45});
46
47serve(app)

There are a lot of issues we need to consider with the implementation above:

  • What happens if a user makes multiple requests? How can we ensure accuracy and speed?
  • We don't have rate-limiting requests, so we need to implement them to ensure there isn't an abuse vector.
  • What happens if a user gets a new key? How do we ensure everything is accurate?

How to simplify your implementation using Unkey

Unkey is an open-source API developer platform that allows developers to simplify the implementation of scalable and secure APIs.

With Unkey, with just a few lines of code, you can protect your API; the key holds all the information, so there isn't a requirement to look at another system to find this information. API keys can be created programmatically or through our dashboard.

1const { result, error } = await verifyKey({
2  key,
3  remaining: {
4    cost,
5  },
6  apiId: "API_ID_FROM_UNKEY",
7});

Below, we took the above implementation and removed the need for:

  • Implementing API key creation (Our example doesn't show this)
  • Setting the credit amount for a user (Our example doesn't show this)
  • Safe way to look up an API key to get the usage base
  • Redis
1import { Hono } from "hono";
2import { serve } from '@hono/node-server'
3import { createMiddleware } from "hono/factory";
4import { verifyKey } from "@unkey/api";
5
6const app = new Hono();
7
8type UnkeyResult = Awaited<ReturnType<typeof verifyKey>>["result"];
9
10declare module "hono" {
11  interface ContextVariableMap {
12    unkey: UnkeyResult;
13  }
14}
15
16// Middleware to verify API key with specified cost
17const verifyKeyWithCost = (cost: number) => createMiddleware(async (c, next) => {
18  const key = c.req.header("X-API-Key");
19  if (!key) {
20    return c.json({ error: "No API key provided" }, 401);
21  }
22
23  const { result, error } = await verifyKey({
24    key,
25    remaining: {
26      cost,
27    },
28    apiId: "API_ID_FROM_UNKEY",
29  });
30
31  /**
32  * Handle Unkey Errors
33  * We have others but not important for this example
34  */
35  if (error) {
36    switch (error.code) {
37      case "TOO_MANY_REQUESTS":
38        return c.json({ error: "Rate limit exceeded" }, 429);
39      case "BAD_REQUEST":
40        return c.json({ error: "Bad request" }, 400);
41      case "INTERNAL_SERVER_ERROR":
42        return c.json({ error: "Internal server error" }, 500);
43      default:
44        return c.json({ error: "Internal server error" }, 500);
45    }
46  }
47  /** Handle Unkey Result if it's not valid such as
48  * Ratelimited, disabled, expired or no remaining credits
49  * There are other errors but they're not needed for this example
50  **/
51  if (!result.valid) {
52    switch (result.code) {
53      case "DISABLED":
54        return c.json({ error: "API key is disabled" }, 401);
55      case "USAGE_EXCEEDED":
56        return c.json({ error: "API key usage exceeded" }, 429);
57      case "NOT_FOUND":
58        return c.json({ error: "API key not found" }, 404);
59      default:
60        return c.json({ error: "Internal server error" }, 500);
61    }
62  }
63  /** Add verification result to context for this example to show how Unkey works
64  * This can also be used in a route handler for additional business logic
65  **/
66  c.set("unkey", result);
67  await next();
68});
69
70// Cheap endpoint - costs 1 credit
71app.get("/cheap-endpoint", verifyKeyWithCost(1), (c) => {
72  return c.json({
73    message: "Accessed cheap endpoint",
74    cost: 1,
75    verificationResult: c.get("unkey")
76  });
77});
78
79// Expensive endpoint - costs 5 credits
80app.get("/expensive-endpoint", verifyKeyWithCost(5), (c) => {
81  return c.json({
82    message: "Accessed expensive endpoint",
83    cost: 5,
84    verificationResult: c.get("unkey")
85  });
86});
87
88serve(app)

As you can see we really simplified the code and removed additional lookups via databases, allowing your API to be performant and scalable. You can get started for free today!

Protect your API.
Start today.

150,000 requests per month. No CC required.