MonetizationOS Docs
Getting StartedIntegrate with MOS

Custom Proxy Integration

If your platform isn't covered by one of the ready-made proxies, such as Cloudflare, build your own with the @monetizationos/proxy package, the runtime-agnostic core that powers them.

View on GitHub

MonetizationOS Proxy Core

A proxy sits between your visitor and your website. It receives the request first, makes a surface decision about what that visitor should see, and can change the response before it reaches the browser.

Use one of the ready-made proxies if your platform is already covered. Build on @monetizationos/proxy directly for any other platform: another edge runtime, a custom Node.js or Deno server, or a service mesh sidecar.

Proxy Pipeline

A MonetizationOS proxy pipeline starts with the request your visitor made, and ends with the response they receive. In between, every request passes through the following steps:

  • Custom endpoint routing: requests meant for MonetizationOS itself, like redeeming an offer, go straight to the MonetizationOS API instead of your site.
  • Origin fetch: the proxy fetches the real response from your website's own server.
  • Link rewriting: links in that response are rewritten so future clicks route back through the proxy.
  • Surface decisions: the proxy asks MonetizationOS what should happen for this visitor and this page, for example showing an offer, a paywall, or nothing at all.
  • Surface behavior: if that decision requires a change to the response itself, like a redirect or a header, the proxy makes it here.
  • Surface components: if the decision requires a change to the page content, the proxy edits the HTML before it reaches the browser.

Only HTML responses go through link rewriting and everything after it. Anything else, like an image or a JSON response, is returned as soon as it's fetched.

Building a Proxy

The builder is responsible for handling that request and building the response. It uses the builder pattern, chaining methods together before calling .build() to produce the finished proxy. Here's an example of a simple proxy builder:

import { MOSProxyBuilder } from "@monetizationos/proxy";

const proxy = new MOSProxyBuilder()
    .withConfig({
        originUrl: "https://example.com",
        surfaceSlug: "web",
        mosHost: "https://api.monetizationos.com",
        mosSecretKey: env.MONETIZATION_OS_SECRET_KEY,
        anonymousSessionCookieName: "anon-session-id",
        authenticatedUserJwtCookieName: "__session",
    })
    .withHtmlRewriter(myHtmlRewriterAdapter)
    .build();

export default {
    fetch: (request: Request) => proxy.handle(request),
};

Some of the common builder methods for constructing the proxy are covered below. For a full list of all the methods available in the Proxy builder, see the builder source.

.withConfig()

This method defines the required options for the proxy and its behavior, like which surface to evaluate, your origin URL, your identity cookie names, and authentication. Find the full list of options here. A separate, optional set of settings covers things like script injection, path exclusions, and extra origin headers. Find the full list here.

It's also possible to configure the proxy per request, covered in Per-Request Config below.

.withOriginFetcher()

This is the utility that fetches your origin. It takes a Request and returns a Response. It's optional, and defaults to the standard fetch API. Supply your own by passing a fetcher adapter to .withOriginFetcher(myOriginFetcher) when you need custom logic for reaching your origin, for example:

function originFetcher(request: Request): Promise<Response> {
    const headers = new Headers(request.headers)
    headers.set("Accept-Encoding", "identity")

    return fetch(request, { headers })
}

.withApiFetcher()

This is the utility that sends requests from the proxy to MonetizationOS, for example the call that fetches a surface decision. It takes a Request and returns a Response. It's optional, and defaults to the standard fetch API. Supply your own when calls to MonetizationOS need something extra before they go out, for example passing through an internal gateway that requires its own authentication header:

.withApiFetcher((request) => {
    const headers = new Headers(request.headers)
    headers.set("X-Internal-Auth", myInternalGatewayToken)

    return fetch(request, { headers })
})

.withHtmlRewriter()

This is the adapter that lets the proxy stream and edit HTML as it passes through, handling the HTML mutations returned from the surface decision. It's required by default, unless you skip HTML transformation entirely, which the API-only mode section below covers. Every platform streams and edits HTML differently, so you supply a small wrapper exposing a common shape, where create() returns a new session for the proxy to use:

class BasicHtmlRewriterSession implements HtmlRewriterSession {
    rewriter = new HTMLRewriter();

    // Registers a change for elements matching `selector`.
    on(selector, handlers) {
        this.rewriter.on(selector, handlers);
        return this;
    }

    // Applies every registered change to `response`.
    transform(response) {
        return this.rewriter.transform(response);
    }
}

const proxy = new MOSProxyBuilder()
    .withConfig(config)
    .withHtmlRewriter({
        capabilities: { onEndTag: true, nthChild: true },
        create: () => new BasicHtmlRewriterSession(),
    })
    .build();

capabilities tells the proxy which of a few advanced rewriting features your platform's rewriter supports, so it can fall back gracefully when one isn't available.

API-Only Mode

If your proxy only needs to handle API traffic and never serves a page, skip the HTML pipeline entirely:

const proxy = new MOSProxyBuilder().withConfig(config).withoutHtmlTransformation().build();

Per-Request Config

.withConfig() also accepts a function instead of a fixed value, useful when a single deployment fronts more than one site or brand. The function receives the request and returns the config to use for it, worked out however you like, for example by looking up the request's host in a table.

hostPathMatcher is a ready-made version of this for the common case, picking a config by host and path prefix:

import { MOSProxyBuilder, hostPathMatcher } from "@monetizationos/proxy";

const proxy = new MOSProxyBuilder()
    .withConfig(
        hostPathMatcher(
            [
                { host: "news.example.com", config: { originUrl: "https://origin.news.example.com", surfaceSlug: "news-web" } },
                { host: "sports.example.com", config: { originUrl: "https://origin.sports.example.com", surfaceSlug: "sports-web" } },
            ],
            // Config that is shared between all per-request matchers
            {
                originUrl: "https://example.com",
                surfaceSlug: "web",
                mosHost: "https://api.monetizationos.com",
                mosSecretKey: env.MONETIZATION_OS_SECRET_KEY,
                anonymousSessionCookieName: "anon-session-id",
                authenticatedUserJwtCookieName: "__session",
            }
        ),
    )
    .withHtmlRewriter(myHtmlRewriterAdapter)
    .build();

See the project README here for details.

Logging and Error Handling

The proxy logs structured warning and error events. Pass .withLogger(...) to send those events wherever your platform expects logs to go, instead of the console default.

If a step in the HTML pipeline fails, the proxy fails open by default: it logs the error and serves the visitor the last safe version of the page it had, instead of failing the whole request. Pass .withHtmlPipelineErrorHandler(...) to control what's served instead, for example a custom error page or a specific status code. If the handler throws or returns a non-Response value, the proxy logs a warning and falls back to the last safe response.

const proxy = new MOSProxyBuilder()
    .withConfig(config)
    .withHtmlRewriter(myHtmlRewriterAdapter)
    .withHtmlPipelineErrorHandler(({ error, stage, lastSafeResponse }) => {
        // Inspect `error` / `stage`, or return your own Response.
        return lastSafeResponse;
    })
    .withLogger({
        log(event) {
            console[event.level](event.message, event.context, event.error);
        },
    })
    .build();

Next Steps

On this page