> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runpulse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pay Per Request (MPP)

> Let AI agents pay for extractions with stablecoins over HTTP 402 — no account or API key required

> Bring Pulse to autonomous agents: no signup, no API key — pay per document with USDC on Tempo using the open [Machine Payments Protocol (MPP)](https://mpp.dev).

Every Pulse endpoint normally requires an [API key](/authentication). MPP adds a second door for agents: call `POST /extract` with **no credentials**, receive an HTTP `402 Payment Required` challenge, pay the quoted amount on the [Tempo](https://tempo.xyz) blockchain, and retry to get your extraction — plus a 24-hour session key for follow-up work on the same document.

<Note>
  MPP payments use **push mode**: your client broadcasts the stablecoin transfer itself and presents the transaction hash as proof. Clients that only send pull-mode credentials (for example `pympp` 0.9.1, or the Tempo CLI's `tempo request` as of v0.6.7) cannot pay Pulse today — use [`mppx`](https://mpp.dev/payment-methods/tempo/charge) configured for push mode, or the manual flow below.
</Note>

## Before you start

You need a Tempo wallet holding enough USDC to cover your extractions (a few cents per document) plus gas:

* **Wallet + funds**: create a wallet and buy or bridge USDC by following the [Tempo docs](https://docs.tempo.xyz), or use the [Tempo CLI](https://tempo.xyz) (`tempo wallet login`, `tempo wallet fund`).
* **RPC**: broadcast transactions through `https://rpc.tempo.xyz` (or your own node).

## How it works

```mermaid theme={null}
sequenceDiagram
    participant A as Agent
    participant P as Pulse API
    participant T as Tempo chain
    A->>P: POST /extract (no credentials)
    P-->>A: 402 + WWW-Authenticate: Payment challenge
    A->>T: Transfer exact USDC amount to challenge recipient
    T-->>A: Transaction hash
    A->>P: POST /extract (identical request) + Authorization: Payment credential
    P-->>A: 200 extraction result + mpp_session key + Payment-Receipt
```

The challenge quotes a price for **your exact document and parameters** — the server counts the pages before charging you. The paid retry must re-send the same document (byte-identical bytes, uploaded the same way with the same filename extension, or the same `file_url` serving unchanged bytes) with the same `model` and `pages` parameters; the challenge is cryptographically bound to them. Both JSON `file_url` requests and multipart `file` uploads are supported — re-send the full request body on every retry.

<Warning>
  The server re-fetches `file_url` on the paid retry. If the bytes behind the URL change — or a pre-signed URL expires — verification fails and your payment cannot be applied. Use content-stable URLs with a lifetime comfortably beyond the 10-minute challenge window, or upload the file directly.
</Warning>

## Pricing

MPP charges the same credits as authenticated pay-as-you-go, settled in USDC at **\$0.015 per credit**, rounded up to the next whole cent per payment.

| Endpoint   | Credits                                                   |
| ---------- | --------------------------------------------------------- |
| `/extract` | 1 per page (`pulse-ultra-2`: 10 per page)                 |
| `/split`   | 2 per page                                                |
| `/schema`  | 1 per page (+3 per page with `effort: true`)              |
| `/tables`  | 0.25 per table (+0.25 per table with merge, +1 per chart) |

For example, extracting a 2-page PDF costs 2 credits = **\$0.03**, quoted in the challenge as `30000` base units of USDC (6 decimals).

## Quick start

<Steps>
  <Step title="Request an extraction with no credentials">
    ```bash theme={null}
    curl -i -X POST https://api.runpulse.com/extract \
      -H "Content-Type: application/json" \
      -d '{"file_url": "https://www.impact-bank.com/user/file/dummy_statement.pdf"}'
    ```

    The response is `402 Payment Required` with the challenge in the `WWW-Authenticate` header:

    ```
    WWW-Authenticate: Payment id="33v1kk…", realm="pulse", method="tempo",
      intent="charge", expires="2026-07-20T01:09:49Z",
      request="eyJhbW91bnQiOiIzMDAwMCIs…", opaque="eyJjcmVkaXRzIjoi…"
    ```
  </Step>

  <Step title="Decode the challenge">
    `request` is base64url JSON with the `=` padding stripped (re-pad to a multiple of 4 before decoding):

    ```json theme={null}
    {
      "amount": "30000",
      "currency": "0x20c000000000000000000000b9537d11c60e8b50",
      "recipient": "0x8dc2668eb15aac7754a33195f5e1066f254f2db6",
      "description": "Pulse extraction: 2 page(s)",
      "methodDetails": { "chainId": 4217, "supportedModes": ["push"] }
    }
    ```

    `amount` is in USDC base units (divide by 10⁶ for dollars), `currency` is the USDC token contract, and `recipient` is a **one-time deposit address minted for this challenge only**. Pay on the chain the challenge names — `chainId` `4217` is Tempo mainnet — using the `currency` and `recipient` values exactly as given.
  </Step>

  <Step title="Pay on Tempo">
    Transfer **exactly** `amount` of the `currency` token to `recipient` on the challenge's `chainId`, and keep the transaction hash. The challenge expires 10 minutes after it was minted; a payment broadcast in time stays redeemable for roughly 10 more minutes after expiry.
  </Step>

  <Step title="Retry with the payment credential">
    Re-send the **identical** request with an `Authorization: Payment <credential>` header (format below). On success you get the normal [extraction response](/api-reference/endpoint/extract) plus an `mpp_session` object and a `Payment-Receipt` header:

    ```json theme={null}
    {
      "markdown": "…",
      "extraction_id": "0ff87d8a-8234-4c4f-a3e1-5d0371b8ff7b",
      "page_count": 2,
      "credits_used": 2.0,
      "mpp_session": {
        "api_key": "YS735gm_…",
        "org_id": "eorg-5cb0983714345e1b",
        "expires_at": "2026-07-21T01:30:55Z"
      }
    }
    ```

    `Payment-Receipt` is unsigned base64url JSON — `{"status": "success", "method": "tempo", "timestamp": …, "reference": "<tx hash>"}` — keep it with your records; you don't need it for later calls.
  </Step>
</Steps>

<Warning>
  Pay the **exact amount** to the **exact recipient** of the challenge you will present, within its window. Deposit matching is exact-match per challenge: funds sent to a different challenge's address, or an over- or underpaid transfer, do not fulfill your challenge and are **not automatically returned**. An underpaid deposit to the correct address is a terminal failure — never top it up or pay the same challenge twice. If you strand funds, contact [support](/support) with your transaction hash.
</Warning>

## The payment credential

The `Authorization` header value is `Payment ` followed by base64url JSON (padding stripped):

```json theme={null}
{
  "challenge": {
    "id": "…", "realm": "pulse", "method": "tempo", "intent": "charge",
    "expires": "…", "request": "…", "opaque": "…"
  },
  "payload": { "type": "hash", "hash": "0x40bfb1de…" }
}
```

Echo the challenge fields **verbatim as received** — keep `request` and `opaque` as their original base64url strings. `payload.hash` is your Tempo transaction hash. Only `payload.type: "hash"` is accepted; pull-mode `transaction` payloads are rejected. You may additionally include a `source` field (`did:pkh:eip155:<chainId>:<address>`) identifying the payer — it is optional and not validated.

<CodeGroup>
  ```typescript mppx (automatic) theme={null}
  import { Mppx, tempo } from "mppx/client";
  import { createClient, http } from "viem";
  import { privateKeyToAccount } from "viem/accounts";
  import { tempo as tempoChain } from "viem/chains";

  const account = privateKeyToAccount(process.env.TEMPO_PRIVATE_KEY as `0x${string}`);
  const mppx = Mppx.create({
    methods: [
      tempo.charge({
        account,
        mode: "push", // Pulse is push-only; local accounts default to pull
        getClient: () =>
          createClient({ chain: tempoChain, transport: http("https://rpc.tempo.xyz") }),
        expectedChainId: 4217,
      }),
    ],
    polyfill: false, // don't patch global fetch; use mppx.fetch explicitly
  });

  // mppx handles the 402 → pay → retry loop for you
  const response = await mppx.fetch("https://api.runpulse.com/extract", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      file_url: "https://www.impact-bank.com/user/file/dummy_statement.pdf",
    }),
  });
  const result = await response.json();
  console.log(result.markdown, result.mpp_session.api_key);
  ```

  ```python Python (manual) theme={null}
  import base64, json, re, requests

  url = "https://api.runpulse.com/extract"
  body = {"file_url": "https://www.impact-bank.com/user/file/dummy_statement.pdf"}

  def b64url_decode(value):
      return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))

  # 1. Get the challenge
  r = requests.post(url, json=body)
  header = r.headers["WWW-Authenticate"]
  fields = dict(re.findall(r'(\w+)="([^"]*)"', header))
  request = json.loads(b64url_decode(fields["request"]))

  # 2. Pay request["amount"] of request["currency"] to request["recipient"]
  #    on Tempo with your wallet library, and capture the tx hash
  tx_hash = pay_on_tempo(request)  # your signer here

  # 3. Build the credential and retry the identical request
  credential = {
      "challenge": {k: fields[k] for k in
          ("id", "realm", "method", "intent", "expires", "request", "opaque")},
      "payload": {"type": "hash", "hash": tx_hash},
  }
  token = base64.urlsafe_b64encode(
      json.dumps(credential, separators=(",", ":")).encode()
  ).decode().rstrip("=")

  r = requests.post(url, json=body, headers={"Authorization": f"Payment {token}"})
  print(r.json()["markdown"])
  ```
</CodeGroup>

## Paying from an agent with the Tempo wallet

If your agent uses the [Tempo Wallet CLI](https://docs.tempo.xyz/docs/cli), it can complete the whole flow with wallet commands — no signing code required. Set it up once:

```bash theme={null}
# Teach your agent the Tempo wallet (Claude Code shown; works the same for other agents)
claude -p "Read https://tempo.xyz/SKILL.md and set up tempo"

# Or expose the wallet to your agent as an MCP server
tempo wallet mcp add --agent claude-code
```

Then hand the agent this recipe along with your document:

```text theme={null}
Extract <document URL> with Pulse (https://api.runpulse.com/extract), paying over MPP:

1. POST {"file_url": "<document URL>"} with no credentials and read the 402
   WWW-Authenticate: Payment challenge.
2. Decode the challenge's base64url `request` field: it gives `amount` (USDC base
   units), `currency` (token contract), and a one-time `recipient` address.
3. Pay with:  tempo wallet transfer <amount ÷ 10⁶> <currency> <recipient>
   and keep the tx hash. Do not use `tempo request` — it cannot pay push-mode
   servers.
4. Retry the identical request with an Authorization: Payment <credential>
   header: base64url JSON echoing the challenge fields verbatim plus
   payload {"type": "hash", "hash": "<tx hash>"}.
5. Stop and ask me before paying if the quoted amount exceeds my budget.
```

<Note>
  Cap what an agent can spend with your wallet's access-key spending limits (`tempo wallet keys`) — the key it holds cannot move more than the limit you set, whatever the agent does.
</Note>

## The session key

A successful payment returns `mpp_session.api_key` — a real API key scoped to an ephemeral organization created just for your payment. Use it as `x-api-key` for **24 hours** to continue working with the document you paid for:

| Capability              | How                                                                             |
| ----------------------- | ------------------------------------------------------------------------------- |
| Retrieve results again  | `GET /job/{extraction_id}`                                                      |
| Split the document      | `POST /split` — priced per call, paid with a fresh 402 challenge                |
| Extract structured data | `POST /schema` — priced per call, paid with a fresh 402 challenge               |
| Extract tables          | `POST /tables` — runs first, then quotes a 402 priced on the actual table count |

Chained calls authenticate with the session key **and** pay per call: the key proves who you are, the 402 flow settles each new charge. The credential format is identical to the first payment, and paid responses carry a `Payment-Receipt` header. The session key cannot start extractions of new documents — begin a fresh anonymous 402 flow for those.

<Info>
  Session limits: synchronous requests only, `/schema` works on one extraction at a time (no batch or split mode), and up to 5 unpaid `/tables` computations per session. New chained challenges are refused in the last \~20 minutes of the session. Results are purged when the session's retention window ends, so collect everything you need within 24 hours.
</Info>

## Errors and retries

Payment errors are `402` responses in RFC 9457 `application/problem+json` format (`{"title", "detail", "status", "type"}`); other validation and rate-limit errors use the standard `{"error": {"code", "message"}}` envelope. The rule that matters: **only a 402 that carries a `WWW-Authenticate` challenge is payable.** For a 402 *without* a challenge, follow the table — never pay again.

| Response                                                            | Challenge attached? | What to do                                                                                                                |
| ------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `Payment detected but not yet confirmed` (`Retry-After: 30`)        | No                  | Retry the same request + same header after the delay, for up to \~10 minutes                                              |
| `Settlement is temporarily unavailable` (`Retry-After: 30`)         | No                  | Retry the same request + same header                                                                                      |
| `The submitted transaction hash does not match the settled payment` | No                  | Re-send with the tx hash that actually paid this challenge — do not pay again                                             |
| `Payment verification failed: …`                                    | Yes (fresh)         | Fix the cause (expired challenge, changed file or params, malformed hash) and pay the fresh challenge                     |
| `The payment was declined or the deposit never matched`             | Yes (fresh)         | First verify on-chain whether your earlier transfer confirmed to the earlier recipient; only then pay the fresh challenge |
| `The deposit was less than the amount due`                          | No                  | **Terminal** — do not pay this challenge again                                                                            |
| `This payment was already redeemed`                                 | No                  | **Terminal** — start a new request for another extraction                                                                 |

<Note>
  On `/tables` paid retries, verification failures are returned **without** a fresh challenge — re-present the same credential if the cause was transient, or start a new `/tables` request.
</Note>

Anonymous requests are also rate-limited to 30 per hour per IP (`429 GENERAL_004`; during busy periods a shared limit can briefly return 429s as well — honor `Retry-After`). Anonymous files are capped at **50 MB** regardless of model (`FILE_002`), and `async` mode is not available for anonymous requests (`REQ_003`).

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    Prefer a standing account? Get an API key instead.
  </Card>

  <Card title="Extract endpoint" icon="file-lines" href="/api-reference/endpoint/extract">
    Full request and response reference for POST /extract.
  </Card>

  <Card title="Pipeline steps" icon="diagram-project" href="/api-reference/endpoint/pipeline-overview">
    Chain split, schema, and tables on your paid extraction.
  </Card>

  <Card title="Error handling" icon="triangle-exclamation" href="/advanced/error-handling">
    Error codes and retry guidance for the whole API.
  </Card>
</CardGroup>
