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

# Payment Flow

> The full Key0 payment lifecycle: challenge issuance, on-chain settlement, token delivery, and the two endpoints that feed into the shared ChallengeEngine.

## Overview

Both Key0 entry points share a single `ChallengeEngine` that owns the payment lifecycle. The engine exposes two phases -- challenge issuance and settlement -- while each endpoint handles HTTP framing differently.

```
+-------------------------------------------------------------+
|                      ChallengeEngine                        |
|                                                             |
|   requestAccess() / requestHttpAccess()    Phase 1          |
|        -> settlePayment() (transport layer)                 |
|             -> processHttpPayment()        Phase 2          |
+-------------------------------------------------------------+
                    ^               ^
                    |               |
         +----------+---------+   +-+--------------------+
         |  POST /x402/access |   |  POST /x402/access   |
         |  (x402 HTTP flow)  |   |  + X-A2A-Extensions  |
         |                    |   |  -> A2A Executor      |
         +--------------------+   |  (Express only)       |
                                  +----------------------+
```

The two modes differ only in how the request arrives and how the response is formatted. Under the hood, every payment passes through the same state machine, the same Redis/Postgres stores, and the same settlement logic.

***

## Phase 1 -- Challenge

**Engine method:** `requestAccess()` (A2A) or `requestHttpAccess()` (HTTP transports)

When a client requests access to a resource, the engine creates a payment challenge:

<Steps>
  <Step title="Validate requestId">
    The `requestId` must be a valid UUID. Malformed values are rejected with `INVALID_REQUEST` (400).
  </Step>

  <Step title="Extract identifiers">
    `resourceId` defaults to `"default"` if not provided. `clientAgentId` defaults to `"anonymous"` (A2A) or `"x402-http"` (HTTP transports).
  </Step>

  <Step title="Look up plan">
    The `planId` is matched against `SellerConfig.plans`. If no matching plan exists, the engine throws `TIER_NOT_FOUND` (400).
  </Step>

  <Step title="Idempotency check">
    The engine calls `store.findActiveByRequestId(requestId)` and handles three cases:

    | Existing State               | Behavior                                                                |
    | ---------------------------- | ----------------------------------------------------------------------- |
    | **PENDING** (not expired)    | Return the existing challenge -- no new record created                  |
    | **DELIVERED** (with grant)   | Throw `PROOF_ALREADY_REDEEMED` (200) with the cached grant in `details` |
    | **EXPIRED** or **CANCELLED** | Fall through and create a new challenge                                 |
  </Step>

  <Step title="Generate challengeId">
    UUIDs for A2A transport, `http-{uuid}` prefix for HTTP transports.
  </Step>

  <Step title="Create PENDING record">
    A new `ChallengeRecord` is persisted via `store.create()` with state `PENDING`, the plan amount, chain configuration, and an expiration timestamp.
  </Step>

  <Step title="Return challenge">
    The engine returns an `X402Challenge` (A2A) or a challenge response object (HTTP) containing all the information the client needs to authorize payment.
  </Step>
</Steps>

***

## Phase 2 -- Settlement and Token Issuance

**Engine method:** `processHttpPayment(requestId, planId, resourceId, txHash, fromAddress?)`

After the client signs an EIP-3009 authorization and the transport layer settles it on-chain, the engine processes the payment:

<Steps>
  <Step title="Look up plan">
    Verify `planId` exists in `SellerConfig.plans`. Throws `TIER_NOT_FOUND` (400) if missing.
  </Step>

  <Step title="Double-spend guard">
    Call `seenTxStore.get(txHash)`. If the transaction hash has already been claimed, throw `TX_ALREADY_REDEEMED` (409).
  </Step>

  <Step title="Find or create PENDING record">
    Look up the PENDING record by `requestId`. If no record exists (the challenge phase was skipped or the original expired), auto-create one.
  </Step>

  <Step title="Atomic PENDING to PAID transition">
    Transition the record from `PENDING` to `PAID` atomically via a Lua script (Redis) or a conditional UPDATE (Postgres). The transition writes `txHash`, `paidAt`, and `fromAddress` to the record.
  </Step>

  <Step title="Mark transaction hash as used">
    Call `seenTxStore.markUsed(txHash, challengeId)` which executes `SET NX`. If this returns `false` (another challenge claimed the same hash in a race), the engine **rolls back** PAID to PENDING and throws `TX_ALREADY_REDEEMED` (409).
  </Step>

  <Step title="Issue token">
    Call `config.fetchResourceCredentials()` with `{ requestId, challengeId, resourceId, planId, txHash }`. This call has a configurable timeout (`tokenIssueTimeoutMs`, default 15s) and retry policy (`tokenIssueRetries`, default 2 attempts with exponential backoff).
  </Step>

  <Step title="Build AccessGrant">
    Assemble the `AccessGrant` object containing the access token, expiration, resource endpoint, transaction hash, and explorer URL.
  </Step>

  <Step title="Persist grant (outbox pattern)">
    Write the full `AccessGrant` JSON to the record via a PAID to PAID update. This ensures the grant is durable before returning it to the client -- if the next step fails, the grant is not lost.
  </Step>

  <Step title="Mark DELIVERED (best-effort)">
    Transition PAID to DELIVERED with `deliveredAt`. If this fails, the record stays in PAID with `accessGrant` set. The refund cron skips records that already have an `accessGrant`.
  </Step>

  <Step title="Fire callback and return">
    Fire `onPaymentReceived` asynchronously (non-blocking, errors are logged). Return the `AccessGrant` to the client.
  </Step>
</Steps>

***

## Phase 3 -- Access Protected Resource

After receiving an `AccessGrant`, the client uses the `accessToken` as a Bearer token to access protected endpoints:

```http theme={null}
POST /api/photos/photo-123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```

The `validateAccessToken` middleware verifies the JWT and attaches decoded claims to the request:

| Framework | Claims location      |
| --------- | -------------------- |
| Express   | `req.key0Token`      |
| Hono      | `c.get("key0Token")` |
| Fastify   | `request.key0Token`  |

***

## Two Endpoints

<Tabs>
  <Tab title="x402 HTTP (/x402/access)">
    The Express integration mounts both `GET /discover` and `POST /x402/access`. The request determines which of four cases applies.

    ### Case 1a: Discovery (GET /discover)

    Call `GET /discover` to browse all available plans. No PENDING record is created — this is a pure pricing query.

    **Request:**

    ```http theme={null}
    GET /discover
    ```

    **Response:**

    ```http theme={null}
    HTTP/1.1 200 OK
    Content-Type: application/json
    ```

    ```json theme={null}
    {
      "agentName": "My Agent",
      "description": "Payment-gated API",
      "plans": [
        {
          "planId": "basic",
          "unitAmount": "$0.10",
          "description": "Basic plan - $0.10 USDC"
        }
      ],
      "routes": []
    }
    ```

    The `accepts` array contains one entry per plan configured in `SellerConfig.plans`. Use the `planId` from `extra.planId` in your subsequent `POST /x402/access` request.

    ### Case 1b: POST /x402/access without planId (HTTP 400)

    `POST /x402/access` with an empty body or no `planId` now returns an HTTP **400** error pointing to `GET /discover`. This is not the discovery endpoint — it is an error response.

    **Request:**

    ```http theme={null}
    POST /x402/access
    Content-Type: application/json

    {}
    ```

    **Response:**

    ```http theme={null}
    HTTP/1.1 400 Bad Request
    Content-Type: application/json
    ```

    ```json theme={null}
    {
      "error": "Please select a plan from the discovery API response to purchase access. Endpoint: GET /discover"
    }
    ```

    ### Case 2: Challenge (planId, no payment-signature)

    The client specifies a `planId` but does not include a `payment-signature` header. The engine creates a PENDING record and returns the challenge.

    `requestId` is auto-generated as `http-{uuid}` if not provided.

    **Request:**

    ```http theme={null}
    POST /x402/access
    Content-Type: application/json

    {
      "planId": "basic",
      "requestId": "550e8400-...",
      "resourceId": "photo-123"
    }
    ```

    **Response:**

    ```http theme={null}
    HTTP/1.1 402 Payment Required
    payment-required: eyJ4NDAyVm... (base64)
    www-authenticate: Payment realm="...", accept="exact", challenge="http-a1b2c3d4-..."
    ```

    ```json theme={null}
    {
      "x402Version": 2,
      "accepts": [ ... ],
      "extensions": {
        "key0": {
          "inputSchema": { ... },
          "outputSchema": { ... },
          "description": "..."
        }
      },
      "challengeId": "http-a1b2c3d4-...",
      "error": "Payment required"
    }
    ```

    ### Case 3: Settlement — Subscription (planId + payment-signature → AccessGrant)

    The client includes the `payment-signature` header containing a base64-encoded `X402PaymentPayload` with the signed EIP-3009 authorization.

    **Request:**

    ```http theme={null}
    POST /x402/access
    Content-Type: application/json
    payment-signature: eyJ4NDAyVm... (base64-encoded X402PaymentPayload)

    {
      "planId": "basic",
      "requestId": "550e8400-...",
      "resourceId": "photo-123"
    }
    ```

    **Response:**

    ```http theme={null}
    HTTP/1.1 200 OK
    payment-response: eyJzdWNjZXNz... (base64-encoded X402SettleResponse)
    ```

    ```json theme={null}
    {
      "type": "AccessGrant",
      "challengeId": "http-a1b2c3d4-...",
      "accessToken": "eyJhbGciOiJIUzI1NiIs...",
      "tokenType": "Bearer",
      "resourceEndpoint": "https://api.example.com/photos/photo-123",
      "resourceId": "photo-123",
      "planId": "basic",
      "txHash": "0xSettledTx...",
      "explorerUrl": "https://sepolia.basescan.org/tx/0xSettledTx..."
    }
    ```

    ### Case 4: Route-Based Standalone (routeId + resource + payment-signature → ResourceResponse)

    For top-level paid `routes` with `proxyTo` or `fetchResource` set on `SellerConfig`, the client includes a `resource` field and the engine proxies to the backend instead of issuing a JWT.

    **Challenge request:**

    ```http theme={null}
    POST /x402/access
    Content-Type: application/json

    {
      "routeId": "weather-query",
      "resource": { "method": "GET", "path": "/api/weather/london" }
    }
    ```

    **Response:** `402` (same payment requirements as subscription)

    **Settlement request:**

    ```http theme={null}
    POST /x402/access
    Content-Type: application/json
    payment-signature: eyJ4NDAyVm...

    {
      "routeId": "weather-query",
      "resource": { "method": "GET", "path": "/api/weather/london" }
    }
    ```

    **Response:** `200 ResourceResponse` (the backend's response, not a JWT)

    ```json theme={null}
    {
      "type": "ResourceResponse",
      "challengeId": "http-a1b2c3d4-...",
      "requestId": "550e8400-...",
      "routeId": "weather-query",
      "txHash": "0xSettledTx...",
      "explorerUrl": "https://sepolia.basescan.org/tx/0xSettledTx...",
      "resource": {
        "status": 200,
        "body": { "city": "london", "tempF": 65, "condition": "Cloudy" }
      }
    }
    ```

    If the backend returns a non-2xx status, the `ResourceResponse` still contains the backend's body and status code, and the challenge stays in `PAID` state so the refund cron can process it.
  </Tab>

  <Tab title="A2A Executor (Express only)">
    When a native A2A client sends the `X-A2A-Extensions` header to `POST /x402/access`, the Express integration bypasses the x402 HTTP flow and delegates to the A2A JSON-RPC handler, which routes to `Key0Executor`.

    ### Phase 1: AccessRequest to Task (input-required)

    The client sends an A2A `message/send` with an `AccessRequest` in message parts:

    ```json theme={null}
    {
      "method": "message/send",
      "params": {
        "message": {
          "parts": [
            {
              "kind": "data",
              "data": {
                "type": "AccessRequest",
                "planId": "basic",
                "requestId": "...",
                "resourceId": "photo-123",
                "clientAgentId": "did:web:buyer"
              }
            }
          ]
        }
      }
    }
    ```

    The executor calls `engine.requestAccess(req)` and publishes a Task with:

    * **State:** `input-required`
    * **Metadata:** `x402.payment.status: "payment-required"`, `x402.payment.required: <PaymentRequirements>`
    * **Parts:** challenge description (text) + X402Challenge (data)

    ### Phase 2: Payment to Task (completed)

    The client sends payment metadata in a follow-up `message/send`:

    ```json theme={null}
    {
      "method": "message/send",
      "params": {
        "message": {
          "metadata": {
            "x402.payment.status": "payment-submitted",
            "x402.payment.payload": {
              "x402Version": 2,
              "payload": { "signature": "0x..." },
              "accepted": {
                "extra": { "challengeId": "..." }
              }
            }
          },
          "parts": [
            { "kind": "text", "text": "Payment submitted" }
          ]
        }
      }
    }
    ```

    The executor processes through intermediate working states:

    1. Extract `challengeId` from `payload.accepted.extra.challengeId`
    2. Publish working Task: `x402.payment.status: "payment-submitted"`
    3. Call `settlePayment()` to verify and settle on-chain
    4. Publish working Task: `x402.payment.status: "payment-verified"`
    5. Call `engine.processHttpPayment()` (PENDING to PAID to DELIVERED)
    6. Publish final Task:
       * **State:** `completed`
       * **Metadata:** `x402.payment.status: "payment-completed"`, `x402.payment.receipts: [receipt]`
       * **Parts:** confirmation text + AccessGrant data
       * **Artifacts:** access-grant data part

    ### x402 Metadata Keys

    | Key                     | Value                                                                                                            | Direction        |
    | ----------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------- |
    | `x402.payment.status`   | `"payment-required"` / `"payment-submitted"` / `"payment-verified"` / `"payment-completed"` / `"payment-failed"` | Server to Client |
    | `x402.payment.required` | `PaymentRequirements` object                                                                                     | Server to Client |
    | `x402.payment.payload`  | `X402PaymentPayload` object                                                                                      | Client to Server |
    | `x402.payment.receipts` | Array of `X402SettleResponse`                                                                                    | Server to Client |
    | `x402.payment.error`    | Error code string                                                                                                | Server to Client |
  </Tab>
</Tabs>

***

<Note>
  For the complete HTTP headers reference, see [API Reference → Overview](/api-reference/overview#common-headers).
</Note>

***

## Message Types

### X402Challenge (Phase 1 response)

Returned by the engine after creating a PENDING record. Contains everything the client needs to authorize an on-chain payment.

```json theme={null}
{
  "type": "X402Challenge",
  "challengeId": "a1b2c3d4-...",
  "requestId": "550e8400-...",
  "planId": "basic",
  "amount": "$0.10",
  "asset": "USDC",
  "chainId": 84532,
  "destination": "0xSellerWallet...",
  "expiresAt": "2025-03-05T12:30:00.000Z",
  "description": "Send $0.10 USDC to 0xSeller... on chain 84532.",
  "resourceVerified": true
}
```

### ResourceResponse (Phase 2 response — route-based standalone)

Returned after successful settlement for standalone route-based purchases. Contains the backend API response directly — no token issued.

```json theme={null}
{
  "type": "ResourceResponse",
  "challengeId": "http-a1b2c3d4-...",
  "requestId": "550e8400-...",
  "planId": "weather-query",
  "txHash": "0xSettledTx...",
  "explorerUrl": "https://sepolia.basescan.org/tx/0xSettledTx...",
  "resource": {
    "status": 200,
    "body": { "city": "london", "tempF": 65 }
  }
}
```

### AccessGrant (Phase 2 response — subscription)

Returned after successful settlement and token issuance. The `accessToken` is used as a Bearer token to access protected endpoints.

```json theme={null}
{
  "type": "AccessGrant",
  "challengeId": "a1b2c3d4-...",
  "requestId": "550e8400-...",
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "tokenType": "Bearer",
  "resourceEndpoint": "https://api.example.com/photos/photo-123",
  "resourceId": "photo-123",
  "planId": "basic",
  "txHash": "0xabcdef1234567890...",
  "explorerUrl": "https://sepolia.basescan.org/tx/0xabcdef..."
}
```

### payment-signature Header (decoded)

The `payment-signature` header carries a base64-encoded `X402PaymentPayload`:

```json theme={null}
{
  "x402Version": 2,
  "network": "eip155:84532",
  "scheme": "exact",
  "payload": {
    "signature": "0xSignedEIP3009...",
    "authorization": {
      "from": "0xBuyer...",
      "to": "0xSeller...",
      "value": "100000",
      "validAfter": "0",
      "validBefore": "1741180560",
      "nonce": "0xRandomNonce..."
    }
  },
  "accepted": {
    "scheme": "exact",
    "network": "eip155:84532",
    "asset": "0x036CbD...",
    "amount": "100000",
    "payTo": "0xSeller...",
    "maxTimeoutSeconds": 900,
    "extra": { "name": "USDC", "version": "2" }
  }
}
```
