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.
Below is an oversimplified version of tracking usage based upon an API key this doesn't include:
However, the idea is the following:
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:
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:
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!
150,000 requests per month. No CC required.