---
title: "Building a Monetized API, Part 1: Setting Up the Gateway"
description: "Before you can charge for your API, you need the gateway set up right. Set up origin auth, consumer isolation, and rate limiting as the foundation for adding monetization."
canonicalUrl: "https://zuplo.com/blog/2026/03/31/building-a-monetized-api-part-1"
pageType: "blog"
date: "2026-03-31"
authors: "martyn"
tags: "API Monetization, API Gateway"
image: "https://zuplo.com/og?text=Building%20a%20Monetized%20API%2C%20Part%201%3A%20The%20Gateway"
---
This is Part 1 of a four-part series on building a fully monetized API with
Zuplo, from gateway to self-serve API keys, usage-based plans, MCP server, and
developer portal. Before any of that works, you need the gateway set up right.
That's what we're covering here.

<CalloutVideo
  variant="card"
  title="Building a Monetized API, Part 1"
  description="Watch the video walkthrough of setting up the API gateway with origin auth, consumer isolation, and rate limiting."
  videoUrl="/videos/building-a-monetized-api-part-1"
  thumbnailUrl="https://img.youtube.com/vi/FMcYofeBoxE/maxresdefault.jpg"
  duration="12:55"
/>

## What we're building

The API behind all of this is a changelog and release notes service. Teams can
create projects, publish changelog entries into a database, and query them with
full-text search, filtering, and pagination. Think of it as a storage layer for
release notes that you can query programmatically.

It's built with Hono and TypeScript, deployed on Vercel with a Supabase backend.
Nothing exotic, but it has enough endpoints (12) and enough complexity (project
scoping, search, pagination) to make the monetization and MCP parts of this
series actually interesting. A to-do list API wouldn't cut it here.

![The Changeloggle developer portal showing the API reference with all endpoints](/media/posts/2026-03-31-building-a-monetized-api-part-1/changeloggle-portal.png)

This is a four-part series. After the gateway setup in this post, we'll cover
[adding monetization, meters, and plans (Part 2)](/blog/2026/04/01/building-a-monetized-api-part-2),
[an MCP server with plan-gated access (Part 3)](/blog/2026/04/02/building-a-monetized-api-part-3),
and
[developer portal polish (Part 4)](/blog/2026/04/03/building-a-monetized-api-part-4).

<CalloutAudience
  variant="useIf"
  items={[
    "You have an existing API deployed somewhere (Vercel, AWS, Cloudflare, etc.) and want to put a gateway in front of it",
    "You're planning to monetize your API and want to set up the right foundation from the start",
    "You want to understand how API key subjects and consumer IDs enable customer isolation",
  ]}
/>

## Step 1: Importing the OpenAPI spec

Starting from a blank Zuplo project, the fastest way to get going is to import
the OpenAPI specification that describes the existing API. This pulls in all 12
endpoints with their verbs, paths, and schema definitions.

After the import, every route's request handler points at `api.example.com/v1`,
which is the placeholder from the spec. We need to point these at the real API.

The cleanest way to do this is with an environment variable. In Zuplo's
settings, create a new variable called `BASE_URL` and set it to the actual API
URL. Apply it across all environments (working copy, staging, production).

Then update the service URL in any one route to use `$env(BASE_URL)`. Because
Zuplo's route designer shares the service configuration across routes, this
single change applies to all 12 endpoints.

<CalloutDoc
  title="Environment Variables"
  description="Learn more about managing configuration across environments in Zuplo."
  href="/docs/articles/environment-variables"
/>

## Step 2: Origin authentication with a shared secret

Here's where the first important architectural decision comes in. The
destination Changeloggle API (hosted on Vercel) is publicly accessible. Anyone
who discovers the URL could bypass the gateway entirely. We need to lock that
down.

The approach: a shared secret. The Changeloggle API checks for an
`x-gateway-secret` header on every incoming request. If it's missing or wrong,
the request gets a 401. Only Zuplo knows the secret, so only Zuplo can talk to
the origin.

![Shared secret flow: the API gateway forwards requests with the secret, while direct requests without it are rejected](/media/posts/2026-03-31-building-a-monetized-api-part-1/shared-secret.png)

To implement this, add `GATEWAY_SECRET` as a secret environment variable in
Zuplo's settings. Secrets are encrypted and not visible in the dashboard after
creation. We'll wire this into a custom policy alongside consumer isolation in
the next step.

## Step 3: Consumer isolation via API key subjects

With the origin locked down, the next step is making sure every request carries
the identity of the consumer making it.

Every API key in Zuplo has a "subject", which is a unique identifier for the
consumer. When a request comes in with a valid API key, `request.user.sub`
contains that subject. By forwarding it to the origin as an `x-consumer-id`
header, the backend can scope all data operations to that specific consumer.

![Consumer isolation: each API key's subject is forwarded as x-consumer-id, scoping data to individual customers in Supabase](/media/posts/2026-03-31-building-a-monetized-api-part-1/consumer-isolation.png)

This means each API key can only create and access its own projects and
changelog entries. There's no way for one consumer to read another consumer's
data, because the origin uses that consumer ID as a partition key for all
database queries.

To implement both the shared secret and consumer isolation, first add the
built-in [API Key Authentication policy](/docs/policies/api-key-inbound) to
every route. This handles key validation and populates `request.user` with the
consumer's identity. Then create a custom inbound policy that runs after it. In
Zuplo, create a new module called `set-request-headers`:

```typescript
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
  request.headers.set("x-gateway-secret", context.env.GATEWAY_SECRET);
  request.headers.set("x-consumer-id", request.user.sub);

  return request;
}
```

The API key policy upstream has already validated the request and populated
`request.user`, so this policy just sets two headers on the outgoing request to
the origin: the gateway secret for authentication, and the consumer ID for
isolation.

Why does this matter for monetization? When we add metering and plans in Part 2,
Zuplo's monetization system will handle API key creation and consumer identity
automatically. The `request.user.sub` value will still be there, and it will
still flow through this same policy to the origin. We won't need to change this
code at all.

<CalloutDoc
  title="API Key Authentication"
  description="Details on API key subjects, metadata, and key management in Zuplo."
  href="/docs/policies/api-key-inbound"
/>

## Step 4: Rate limiting

Add a rate limiting inbound policy set to 100 requests per minute, keyed by IP
address. Apply it to all routes, and make sure it executes **before** the
header-setting policy so abusive traffic is rejected as cheaply as possible.

This is distinct from the request limits that monetization adds in Part 2.
Request limits are business logic: a free tier gets 20 requests/month, a paid
tier gets 50,000. Rate limiting is infrastructure protection: no single IP
should be able to send 1,000 requests in a minute regardless of their plan. You
need both.

## Step 5: Testing end to end

To verify the full chain works, we need a temporary API key. In Zuplo's API key
service, create a consumer with a UUID as the subject. Generate a key for that
consumer.

Then make a test request to the create project endpoint with the API key as a
Bearer token:

```bash
curl -X POST https://your-gateway.zuplo.dev/v1/projects \
  -H "Authorization: Bearer zpka_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "My Project", "description": "A test project", "url": "https://example.com"}'
```

A 201 response confirms the full chain is working: Zuplo received the request,
validated the API key, set the gateway secret and consumer ID headers, forwarded
it to the Changeloggle API on Vercel, and the origin accepted it.

Note: this API key setup is temporary. When we add monetization in the next
post, API key creation and management moves to the self-serve developer portal.
Consumers will sign up, choose a plan, and generate their own keys. The manual
key service configuration goes away.

## What we built in Part 1

At this point, the gateway is set up with:

- [x] All 12 endpoints imported from the OpenAPI spec
- [x] Environment-based upstream URL configuration
- [x] Origin authentication via a shared secret
- [x] Consumer isolation via API key subjects forwarded as headers
- [x] IP-based rate limiting as an abuse backstop

Every one of these decisions was made with monetization in mind. The shared
secret locks down the origin. The consumer ID enables per-customer data
isolation. The rate limiting sits in a position where it can coexist with
per-plan metering. And none of this needs to be rewritten when we add billing.

## Next up: monetization

In [Part 2](/blog/2026/04/01/building-a-monetized-api-part-2), we'll add
metering, create subscription plans (free, starter, and pro), configure the
developer portal's pricing page, and test the full flow of a user signing up,
choosing a plan, and making authenticated requests with usage tracking.

- [ ] Configure metering on API requests
- [ ] Create subscription plans (free, starter, pro)
- [ ] Connect Stripe for checkout and billing
- [ ] Set up the developer portal pricing page
- [ ] Test the full sign-up and usage tracking flow

![The Changeloggle pricing page with Free, Starter, and Pro plans](/media/posts/2026-03-31-building-a-monetized-api-part-1/changeloggle-pricing.png)

If you want to skip ahead and see the full monetization product in action, check
out the launch post:
[Monetize Your API with Zuplo](/blog/2026/03/30/monetize-your-api-in-10-mins).