> ## Documentation Index
> Fetch the complete documentation index at: https://tbd-6fc993ce-hypeship-add-mpp-common-pattern.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Buy a Browser with MPP

> buy a KERNEL browser through mpp with link

[machine payments protocol (mpp)](https://mpp.dev/) is an open protocol co-authored by stripe and tempo. it uses an http `402` challenge so an agent can pay for an api request without a checkout page. KERNEL accepts mpp payments for browser sessions: an agent pays with a stripe shared payment token, then connects to the browser over cdp. this route doesn't require a KERNEL account or api key.

the mpp price is \$0.50 for one stealth, headful browser for 30 minutes. read the `402` challenge to confirm the current price and duration before paying.

## How the mpp payment flow works

1. `link-cli mpp pay` sends `POST /mpp/browsers` without a payment credential. KERNEL returns `402 Payment Required` with the offer in `WWW-Authenticate: Payment`.
2. the link cli gives you an approval url. after you approve the payment through link, it retries the request with `Authorization: Payment <credential>`.
3. KERNEL verifies and charges the credential, creates the browser, and returns its connection urls in a `200` response. the `Payment-Receipt` header contains the mpp receipt.

link through the link cli is the only supported payment flow for now. after approval, the cli sends a credential containing a [stripe shared payment token](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens) (`spt_...`) to KERNEL.

you can't send card details or a standalone card credential to this endpoint, and stablecoin payments aren't offered. the payment challenge expires after 30 minutes by default; that is the time available to approve the offer, not the browser's lifetime.

## Pay with link

run [stripe's link cli](https://docs.stripe.com/agentic-commerce/link-cli/machine-payments) to pay through link. the command reads the `402` challenge, gives you an approval link, and completes the payment after you approve it. you don't need to create a spend request separately.

```bash theme={null}
link-cli mpp pay https://api.onkernel.com/mpp/browsers --method POST
```

to inspect the offer without paying, send an unauthenticated request:

```bash theme={null}
curl -i -X POST https://api.onkernel.com/mpp/browsers
```

you can send an optional `{"email":"agent@example.com"}` json body if you want the stripe receipt sent to that address. otherwise KERNEL uses the billing email shared by the payment token, if available.

the paid response includes `session_id`, `cdp_ws_url`, `webdriver_ws_url`, `browser_live_view_url` when available, `duration_minutes`, and `expires_at`. `payment.amount` is in us cents, `payment.reference` identifies the payment, and `access.type` is `session_urls`. treat the connection urls as credentials; anyone with them can access the browser while it is active.

## End-to-end example

save one of these files as `headlines.ts` or `headlines.py`. each buys a production browser through link, connects with playwright, and prints the first seven Hacker News headlines. the current offer makes a real \$0.50 payment; confirm the amount before approving.

install and sign in to the [link cli](https://docs.stripe.com/agentic-commerce/link-cli/machine-payments):

```bash theme={null}
npm install -g @stripe/link-cli@0.23.1
link-cli auth login
```

<Tabs>
  <Tab title="TypeScript">
    ```bash theme={null}
    npm init -y
    npm pkg set type=module
    npm install playwright-core tsx
    npx tsx headlines.ts
    ```

    ```ts headlines.ts theme={null}
    import { spawnSync } from "node:child_process";
    import { chromium } from "playwright-core";

    const MPP_URL = "https://api.onkernel.com/mpp/browsers";
    const CONTEXT =
      "Buy one KERNEL browser session to visit Hacker News, read its first seven story headlines, and print them to the terminal as a Link MPP payment demonstration.";
    const HEADLINE_COUNT = 7;

    type ApprovalRequest = {
      id: string;
      approval_url: string;
      _next: { pay_argv: { command: string; args: string[] } };
    };

    type PayResult = { status: number; body: string };
    type SpendRequest = { status: string };
    type BrowserSession = {
      session_id: string;
      cdp_ws_url: string;
      browser_live_view_url?: string;
      expires_at: string;
      payment?: { reference?: string };
    };

    function linkCli<T>(...args: string[]): T {
      const result = spawnSync("link-cli", [...args, "--format", "json"], {
        encoding: "utf8",
      });
      if (result.error) throw result.error;
      if (result.status !== 0) {
        const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
        throw new Error(`Link CLI failed: ${detail}`);
      }

      const output = JSON.parse(result.stdout) as T | T[];
      const last = Array.isArray(output) ? output.at(-1) : output;
      if (!last) throw new Error("Link CLI returned no result");
      return last;
    }

    function buyBrowser(): BrowserSession {
      let purchase = linkCli<PayResult | ApprovalRequest>(
        "mpp",
        "pay",
        MPP_URL,
        "--method",
        "POST",
        "--context",
        CONTEXT,
      );

      if ("_next" in purchase) {
        if (!purchase.id || !purchase.approval_url) {
          throw new Error("Link did not return an approval request");
        }
        console.log(`Spend request: ${purchase.id}`);
        console.log(`Approve the payment in Link: ${purchase.approval_url}`);
        const deadline = Date.now() + 10 * 60_000;
        const waiting = new Set(["created", "pending_approval", "requires_action"]);
        let approval: SpendRequest;
        do {
          const remaining = Math.ceil((deadline - Date.now()) / 2_000);
          if (remaining <= 0) throw new Error("Link approval timed out after 10 minutes");
          approval = linkCli<SpendRequest>(
            "spend-request", "retrieve", purchase.id,
            "--interval", "2", "--max-attempts", String(remaining),
          );
        } while (waiting.has(approval.status));
        if (approval.status !== "approved") {
          throw new Error(`Link spend request ended with status ${approval.status}`);
        }

        const continuation = purchase._next.pay_argv;
        if (
          continuation.command !== "mpp" ||
          continuation.args[0] !== "pay" ||
          continuation.args[1] !== MPP_URL
        ) {
          throw new Error("Unexpected Link payment continuation");
        }
        purchase = linkCli<PayResult>("mpp", ...continuation.args);
      }

      if (!("body" in purchase) || purchase.status !== 200 || !purchase.body) {
        throw new Error("Browser purchase did not return HTTP 200");
      }
      const session: unknown = JSON.parse(purchase.body);
      if (
        typeof session !== "object" || session === null ||
        !("session_id" in session) || typeof session.session_id !== "string" ||
        !("cdp_ws_url" in session) || typeof session.cdp_ws_url !== "string" ||
        !("expires_at" in session) || typeof session.expires_at !== "string"
      ) {
        throw new Error("Browser purchase returned an invalid session");
      }
      return session as BrowserSession;
    }

    const session = buyBrowser();
    console.log(`Browser session: ${session.session_id} (expires ${session.expires_at})`);
    console.log(`CDP URL: ${session.cdp_ws_url}`);
    if (session.browser_live_view_url) {
      console.log(`Live view: ${session.browser_live_view_url}`);
    }
    if (session.payment?.reference) {
      console.log(`Payment reference: ${session.payment.reference}`);
    }

    const browser = await chromium.connectOverCDP(session.cdp_ws_url);
    try {
      const context = browser.contexts()[0];
      if (!context) throw new Error("KERNEL browser has no default context");
      const page = context.pages()[0] ?? (await context.newPage());
      await page.goto("https://news.ycombinator.com/", { waitUntil: "domcontentloaded" });
      const headlines = page.locator(".athing .titleline > a");
      await headlines.nth(HEADLINE_COUNT - 1).waitFor();
      for (let index = 0; index < HEADLINE_COUNT; index++) {
        console.log(`${index + 1}. ${await headlines.nth(index).innerText()}`);
      }
    } finally {
      await browser.close();
    }
    ```
  </Tab>

  <Tab title="Python">
    ```bash theme={null}
    python3 -m pip install playwright
    python3 headlines.py
    ```

    ```python headlines.py theme={null}
    import json
    import math
    import subprocess
    import time

    from playwright.sync_api import sync_playwright

    MPP_URL = "https://api.onkernel.com/mpp/browsers"
    CONTEXT = (
        "Buy one KERNEL browser session to visit Hacker News, read its first seven story headlines, "
        "and print them to the terminal as a Link MPP payment demonstration."
    )
    HEADLINE_COUNT = 7


    def link_cli(*args: str) -> dict:
        result = subprocess.run(
            ["link-cli", *args, "--format", "json"],
            capture_output=True,
            text=True,
            check=False,
        )
        if result.returncode:
            detail = result.stderr.strip() or result.stdout.strip() or f"exit {result.returncode}"
            raise RuntimeError(f"Link CLI failed: {detail}")
        output = json.loads(result.stdout)
        last = (output[-1] if output else None) if isinstance(output, list) else output
        if not last:
            raise RuntimeError("Link CLI returned no result")
        return last


    def buy_browser() -> dict:
        purchase = link_cli(
            "mpp", "pay", MPP_URL, "--method", "POST", "--context", CONTEXT,
        )
        if purchase.get("_next"):
            request_id = purchase.get("id")
            approval_url = purchase.get("approval_url")
            if not request_id or not approval_url:
                raise RuntimeError("Link did not return an approval request")
            print(f"Spend request: {request_id}")
            print(f"Approve the payment in Link: {approval_url}")
            deadline = time.monotonic() + 10 * 60
            waiting = {"created", "pending_approval", "requires_action"}
            while True:
                remaining = math.ceil((deadline - time.monotonic()) / 2)
                if remaining <= 0:
                    raise RuntimeError("Link approval timed out after 10 minutes")
                approval = link_cli(
                    "spend-request", "retrieve", request_id,
                    "--interval", "2", "--max-attempts", str(remaining),
                )
                if approval.get("status") not in waiting:
                    break
            if approval.get("status") != "approved":
                raise RuntimeError(f"Link spend request ended with status {approval.get('status')}")

            continuation = purchase["_next"]["pay_argv"]
            args = continuation["args"]
            if continuation["command"] != "mpp" or args[:2] != ["pay", MPP_URL]:
                raise RuntimeError("Unexpected Link payment continuation")
            purchase = link_cli("mpp", *args)

        if purchase.get("status") != 200 or not purchase.get("body"):
            raise RuntimeError(f"Browser purchase returned HTTP {purchase.get('status')}")
        return json.loads(purchase["body"])


    session = buy_browser()
    print(f"Browser session: {session['session_id']} (expires {session['expires_at']})")
    print(f"CDP URL: {session['cdp_ws_url']}")
    if session.get("browser_live_view_url"):
        print(f"Live view: {session['browser_live_view_url']}")
    payment = session.get("payment") or {}
    if payment.get("reference"):
        print(f"Payment reference: {payment['reference']}")

    with sync_playwright() as playwright:
        browser = playwright.chromium.connect_over_cdp(session["cdp_ws_url"])
        try:
            if not browser.contexts:
                raise RuntimeError("KERNEL browser has no default context")
            context = browser.contexts[0]
            page = context.pages[0] if context.pages else context.new_page()
            page.goto("https://news.ycombinator.com/", wait_until="domcontentloaded")
            headlines = page.locator(".athing .titleline > a")
            headlines.nth(HEADLINE_COUNT - 1).wait_for()
            for index in range(HEADLINE_COUNT):
                print(f"{index + 1}. {headlines.nth(index).inner_text()}")
        finally:
            browser.close()
    ```
  </Tab>
</Tabs>

open the approval url printed by the script. a link push notification might not arrive. because the script captures cli output, it waits for approval and runs the returned payment continuation. it gives up after about 10 minutes. keep the paid session details private: the connection and live view urls grant access to the browser. [retry and expiration](#retry-and-expiration) explains what to do if the result is uncertain.

## Retry and expiration

a challenge buys one browser. save the paid response, especially `session_id`, `cdp_ws_url`, and `expires_at`. if you use the link cli, rerunning the command or script starts a new payment request and, if approved, buys another browser. the cli doesn't expose the credential it sent or pay an already completed spend request again. if the result is uncertain, inspect the existing spend request and payment before taking another action.

an mpp client that retains the same payment credential can retry the same paid request while the browser is active; KERNEL returns the same browser without another charge. this recovery path isn't available through the link cli.

the paid time starts when the charge succeeds; use `expires_at` in the response as the access deadline. after expiration, a retry returns a new `402` challenge with `code: session_expired`; paying that challenge buys a new browser. if KERNEL charges you but can't create the browser, it attempts a refund. a successful refund returns a `503` error with a `refund_id`.

## When to use mpp

mpp is a payment protocol, not a browser feature. when a merchant exposes an mpp endpoint, an agent can pay that merchant directly without opening a checkout page. a browser is still useful when the task requires a site's interface, login, or checkout and the site doesn't expose the needed action through mpp.

KERNEL's mpp endpoint handles a different purchase: **the agent pays KERNEL for browser access**. it lets an agent using the link cli acquire one browser without account setup. the purchase returns connection urls only; it doesn't provision a vault or merchant payment credential.

for merchant checkout with KERNEL's wallet integrations, use [payments for browser agents](/browsers/payments) with an account-based browser. for projects, api keys, and ongoing browser management, use [account-based browser access](/introduction/create).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.