{
  "openapi": "3.1.0",
  "info": {
    "title": "Loop Gateway buyer API",
    "version": "2026-09-24",
    "summary": "Prepaid Bitcoin Lightning credit for OpenAI-compatible inference.",
    "description": "Four buyer endpoints: create a prepaid top-up, check the balance, call chat completions, and list models.\n\nPreferred first purchase is prepaid Lightning: `POST /v1/topup` with `{\"amount_sats\":5000}` (minimum 5000 sats), pay the returned BOLT11 invoice from a wallet you control, then send `Authorization: Bearer <token>`. Humans can top up at https://gateway.loopxxi.com/buy.\n\nRead the live rails before paying: https://api.loopxxi.com/.well-known/agent-payments.json. Prices change; read https://api.loopxxi.com/v1/pricing before any authorized purchase.\n\nPer-call L402: an unauthenticated `POST /v1/chat/completions` returns HTTP 402 with an L402 challenge. Paying that challenge with stock clients (lnget, BoltHub L402Client) is not yet tested by Loop XXI. Use prepaid top-up.\n\nEvidence: every response carries `x-evidence`. `live-ci` responses are checked against the live API by CI using unauthenticated requests and no payment. `live-manual` responses were observed once by hand on 2026-09-24. `source` responses are documented from the Gateway source because observing them needs a funded token or a failure.\n\nCredit pays for API usage only. Loop XXI does not take custody of customer wallets.",
    "contact": {
      "name": "Loop XXI LLC",
      "email": "business@loopxxi.com",
      "url": "https://loopxxi.com/businesses"
    }
  },
  "servers": [
    {
      "url": "https://api.loopxxi.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Agent guide",
    "url": "https://api.loopxxi.com/llms.txt"
  },
  "x-loop-discovery": {
    "payment_discovery": "https://api.loopxxi.com/.well-known/agent-payments.json",
    "l402_discovery": "https://api.loopxxi.com/.well-known/l402",
    "purchase": "https://gateway.loopxxi.com/buy",
    "pricing": "https://api.loopxxi.com/v1/pricing",
    "quickstart": "https://loopxxi.com/businesses#quickstart"
  },
  "tags": [
    {
      "name": "Prepaid",
      "description": "Prepaid Bitcoin Lightning credit. Preferred first purchase."
    },
    {
      "name": "Inference",
      "description": "OpenAI-compatible inference and the model catalog."
    }
  ],
  "paths": {
    "/v1/topup": {
      "post": {
        "tags": ["Prepaid"],
        "operationId": "createTopup",
        "summary": "Create a prepaid Lightning invoice and bearer token",
        "description": "Returns a new `loop_` bearer token and a BOLT11 invoice for `amount_sats`. The token authenticates only after the invoice settles; until then `GET /v1/balance` returns HTTP 401 `unknown_prepaid_token`. Pay only an invoice you have checked against your own budget. Validation failures return HTTP 400 before any invoice is created. Rate-limited per IP.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TopupRequest"
              },
              "examples": {
                "minimum": {
                  "summary": "Product minimum",
                  "value": {
                    "amount_sats": 5000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invoice created and not yet paid. The example is the observed body for a buyer that sent no external_id; secrets are replaced with placeholders.",
            "x-evidence": "live-manual",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopupCreated"
                },
                "example": {
                  "token": "loop_<64 hex characters>",
                  "invoice": "<BOLT11 invoice>",
                  "amount_sats": 5000,
                  "payment_hash": "0000000000000000000000000000000000000000000000000000000000000000",
                  "purpose": "",
                  "status": "invoice_pending",
                  "instructions": "Pay this Lightning invoice from your own wallet. The returned token is not funded until settlement. GET /v1/balance returns HTTP 401 code=unknown_prepaid_token until then. Do not treat a loop_ prefix as credit."
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. No invoice is created. Missing, malformed, or below-minimum amounts return the product minimum, a valid example body, and the human purchase page.",
            "x-evidence": "live-ci",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopupBadRequest"
                },
                "examples": {
                  "missing_amount_sats": {
                    "summary": "Empty body",
                    "value": {
                      "error": {
                        "code": "missing_amount_sats",
                        "message": "Missing required field amount_sats (integer, minimum 5000 sats), e.g. {\"amount_sats\":5000}. Buy credits: https://gateway.loopxxi.com/buy",
                        "param": "amount_sats",
                        "type": "invalid_request_error"
                      },
                      "example": {
                        "amount_sats": 5000
                      },
                      "min_amount_sats": 5000,
                      "purchase_url": "https://gateway.loopxxi.com/buy"
                    }
                  },
                  "amount_below_minimum": {
                    "summary": "{\"amount_sats\":1}",
                    "value": {
                      "error": {
                        "code": "amount_below_minimum",
                        "message": "amount_sats must be at least 5000 sats (minimum single top-up). For one-off requests, use the per-call L402 flow. Buy credits: https://gateway.loopxxi.com/buy",
                        "param": "amount_sats",
                        "type": "invalid_request_error"
                      },
                      "example": {
                        "amount_sats": 5000
                      },
                      "min_amount_sats": 5000,
                      "purchase_url": "https://gateway.loopxxi.com/buy"
                    }
                  },
                  "above_maximum": {
                    "summary": "{\"amount_sats\":1000001}",
                    "value": {
                      "error": {
                        "message": "maximum single top-up is 1000000 sats",
                        "type": "invalid_request_error"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/balance": {
      "get": {
        "tags": ["Prepaid"],
        "operationId": "getBalance",
        "summary": "Read the prepaid balance for a settled token",
        "description": "Requires `Authorization: Bearer <token>` from `POST /v1/topup`. Pending invoices and unknown `loop_` tokens return HTTP 401 and do not create an account. Rate-limited per IP.",
        "security": [
          {
            "prepaidBearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Available credit in sats. `spend_policy` reports the optional per-request cap set with `PUT /v1/spend-policy`.",
            "x-evidence": "source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                }
              }
            }
          },
          "401": {
            "description": "No prepaid bearer (`missing_prepaid_token`), or a bearer that never received a settled top-up (`unknown_prepaid_token`).",
            "x-evidence": "live-ci",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrepaidAuthError"
                },
                "examples": {
                  "missing_prepaid_token": {
                    "summary": "No Authorization header",
                    "value": {
                      "error": {
                        "message": "Provide your prepaid token as Authorization: Bearer loop_<token>.",
                        "type": "authentication_error",
                        "code": "missing_prepaid_token"
                      }
                    }
                  },
                  "unknown_prepaid_token": {
                    "summary": "Unpaid or unknown loop_ token",
                    "value": {
                      "error": {
                        "message": "Unknown prepaid token. A loop_ prefix is not authentication. POST https://api.loopxxi.com/v1/topup, pay the Lightning invoice from your own wallet, then GET https://api.loopxxi.com/v1/balance after settlement. Pending invoices do not authenticate and do not create an account.",
                        "type": "authentication_error",
                        "code": "unknown_prepaid_token"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/v1/chat/completions": {
      "post": {
        "tags": ["Inference"],
        "operationId": "createChatCompletion",
        "summary": "OpenAI-compatible chat completion",
        "description": "Send `Authorization: Bearer <token>` with a settled prepaid token. Prepaid calls are charged the estimated cost up front and refunded if the call fails. The estimate prices output at the larger of the token limit and twice the input tokens, so set `max_tokens` to bound the charge. When `max_tokens` and `max_completion_tokens` are omitted, the Gateway prices and forwards `max_tokens` 1024. `stream: true` returns server-sent events.\n\nWithout credentials the response is HTTP 402 with an L402 challenge bound to that exact request body. Paying that challenge with stock clients (lnget, BoltHub L402Client) is not yet tested by Loop XXI; use prepaid top-up. A body that names a model but cannot be normalized returns HTTP 400 before any invoice is created.",
        "security": [
          {
            "prepaidBearer": []
          },
          {
            "l402": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatCompletionRequest"
              },
              "examples": {
                "ping": {
                  "summary": "Ping from payment discovery",
                  "description": "Copy a current model id from GET /v1/models if this one is absent.",
                  "value": {
                    "model": "openai/gpt-4o-mini",
                    "messages": [
                      {
                        "role": "user",
                        "content": "ping"
                      }
                    ],
                    "max_tokens": 16
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Completion forwarded unchanged from the upstream model provider. With `stream: true`, `text/event-stream` lines of the form `data: {chunk}` ending with `data: [DONE]`.",
            "x-evidence": "source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatCompletion"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Request rejected before payment or any invoice. Examples: missing model, invalid JSON, duplicate keys, or a token limit outside 1 to 1000000.",
            "x-evidence": "live-ci",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "examples": {
                  "invalid_max_tokens": {
                    "summary": "max_tokens 0",
                    "value": {
                      "error": {
                        "message": "max_tokens must be an integer between 1 and 1000000",
                        "type": "invalid_request_error"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "A `loop_` bearer that never received a settled top-up (`unknown_prepaid_token`), or an L402 credential that does not authorize this exact request (`l402_request_not_authorized`).",
            "x-evidence": "live-ci",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatUnauthorized"
                },
                "examples": {
                  "unknown_prepaid_token": {
                    "summary": "Unpaid or unknown loop_ token",
                    "value": {
                      "error": {
                        "message": "Unknown prepaid token. A loop_ prefix is not authentication. POST https://api.loopxxi.com/v1/topup, pay the Lightning invoice from your own wallet, then GET https://api.loopxxi.com/v1/balance after settlement. Pending invoices do not authenticate and do not create an account.",
                        "type": "authentication_error",
                        "code": "unknown_prepaid_token"
                      }
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Without credentials: an L402 challenge (`payment_required` / `l402_challenge`) whose `payment.prepaid` block points at prepaid top-up. With a prepaid token: `insufficient_funds` (`no_balance`, `low_balance`) or `human_above` / `budget_exceeded` when the estimate is above your `auto_approve_sats` cap. No sats are deducted on a 402.",
            "x-evidence": "live-ci",
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatPaymentRequired"
                },
                "examples": {
                  "l402_challenge": {
                    "summary": "Unauthenticated ping (placeholders for invoice, macaroon, and hash)",
                    "value": {
                      "error": {
                        "code": "l402_challenge",
                        "message": "Payment required. Pay the Lightning invoice to access this endpoint. Prepaid Lightning top-up is cheaper for small calls when the L402 floor applies.",
                        "type": "payment_required"
                      },
                      "payment": {
                        "amount_sats": 1,
                        "estimated_sats": 1,
                        "floored": false,
                        "invoice": "<BOLT11 invoice>",
                        "macaroon": "<macaroon>",
                        "min_sats": 1,
                        "payment_hash": "0000000000000000000000000000000000000000000000000000000000000000",
                        "prepaid": {
                          "method": "POST",
                          "min_amount_sats": 5000,
                          "preferred": true,
                          "url": "https://api.loopxxi.com/v1/topup"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The upstream model provider failed. Prepaid charges are refunded.",
            "x-evidence": "source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unavailable. Includes `rail_unavailable` / `l402_disabled` when the per-call L402 rail is switched off (stay on prepaid top-up) and `upstream_misconfigured` when upstream inference is down.",
            "x-evidence": "source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/models": {
      "get": {
        "tags": ["Inference"],
        "operationId": "listModels",
        "summary": "List current models",
        "description": "OpenAI-compatible model list. Copy a current id before calling chat completions; do not rely on a cached catalog. Sats prices per million tokens are at `GET /v1/pricing`. Rate-limited per IP.",
        "security": [],
        "responses": {
          "200": {
            "description": "Current model catalog.",
            "x-evidence": "live-ci",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelList"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "The model catalog could not be fetched.",
            "x-evidence": "source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "prepaidBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "loop_ token",
        "description": "The token returned by POST /v1/topup. It authenticates only after its invoice settles. Keep it private."
      },
      "l402": {
        "type": "http",
        "scheme": "L402",
        "description": "Authorization: L402 <macaroon>:<preimage>, after paying the invoice from an HTTP 402 challenge. The challenge is bound to the exact request body. Paying it with stock clients (lnget, BoltHub L402Client) is not yet tested by Loop XXI."
      }
    },
    "headers": {
      "WWW-Authenticate": {
        "description": "L402 challenge carrying the same macaroon and invoice as the JSON body.",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^L402 macaroon=\"[^\"]+\", invoice=\"[^\"]+\"$"
        },
        "example": "L402 macaroon=\"<macaroon>\", invoice=\"<BOLT11 invoice>\""
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Too many requests from this IP or credential. Back off before retrying.",
        "x-evidence": "source",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitError"
            },
            "example": {
              "error": {
                "message": "Rate limit exceeded. Please slow down.",
                "type": "rate_limit_error",
                "code": "rate_limit_exceeded"
              }
            }
          }
        }
      },
      "ServerError": {
        "description": "Unexpected server error. Retry at most three times with backoff.",
        "x-evidence": "source",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      },
      "Unavailable": {
        "description": "A dependency is temporarily unavailable. Retry with backoff. Do not pay a new invoice to work around it.",
        "x-evidence": "source",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            }
          }
        }
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "description": "OpenAI-compatible error envelope. Check both the HTTP status and `error.type` / `error.code`.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "type": "string"
              },
              "code": {
                "type": "string"
              },
              "param": {
                "type": "string"
              }
            }
          }
        }
      },
      "RateLimitError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "const": "rate_limit_error"
              },
              "code": {
                "const": "rate_limit_exceeded"
              }
            }
          }
        }
      },
      "TopupRequest": {
        "type": "object",
        "required": ["amount_sats"],
        "properties": {
          "amount_sats": {
            "type": "integer",
            "minimum": 5000,
            "maximum": 1000000,
            "description": "Whole sats. Minimum 5000 sats, the value of payment_discovery.lightning.min_amount_sats."
          },
          "external_id": {
            "type": "string",
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9:._/@+-]+$",
            "description": "Optional opaque attribution stored for the caller's own bookkeeping. Not identity and not KYC. Token-, invoice-, or secret-shaped values are rejected with HTTP 400."
          }
        }
      },
      "TopupCreated": {
        "type": "object",
        "required": ["token", "invoice", "amount_sats", "payment_hash", "status", "instructions"],
        "properties": {
          "token": {
            "type": "string",
            "pattern": "^loop_",
            "description": "Prepaid bearer token. Keep it private. It authenticates only after the invoice settles."
          },
          "invoice": {
            "type": "string",
            "description": "BOLT11 Bitcoin Lightning invoice for amount_sats."
          },
          "amount_sats": {
            "type": "integer",
            "minimum": 5000
          },
          "payment_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$"
          },
          "purpose": {
            "type": "string",
            "description": "Empty for a standard top-up."
          },
          "status": {
            "const": "invoice_pending"
          },
          "instructions": {
            "type": "string"
          },
          "external_id": {
            "type": "string",
            "description": "Present when the request carried an accepted external_id."
          }
        }
      },
      "TopupAmountError": {
        "type": "object",
        "required": ["error", "min_amount_sats", "example", "purchase_url"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code", "param"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "const": "invalid_request_error"
              },
              "code": {
                "enum": ["invalid_json", "missing_amount_sats", "amount_below_minimum"]
              },
              "param": {
                "const": "amount_sats"
              }
            }
          },
          "min_amount_sats": {
            "const": 5000
          },
          "example": {
            "type": "object",
            "required": ["amount_sats"],
            "properties": {
              "amount_sats": {
                "const": 5000
              }
            }
          },
          "purchase_url": {
            "const": "https://gateway.loopxxi.com/buy"
          }
        }
      },
      "TopupPlainError": {
        "type": "object",
        "description": "Other top-up validation errors (above the maximum, invalid external_id) carry only a message and a type.",
        "required": ["error"],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type"],
            "additionalProperties": false,
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "const": "invalid_request_error"
              }
            }
          }
        }
      },
      "TopupBadRequest": {
        "description": "Missing, malformed, or below-minimum amounts use TopupAmountError. Other validation errors use TopupPlainError.",
        "anyOf": [
          {
            "$ref": "#/components/schemas/TopupAmountError"
          },
          {
            "$ref": "#/components/schemas/TopupPlainError"
          }
        ]
      },
      "PrepaidAuthError": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "const": "authentication_error"
              },
              "code": {
                "enum": ["missing_prepaid_token", "unknown_prepaid_token"]
              }
            }
          }
        }
      },
      "SpendPolicy": {
        "type": "object",
        "required": ["auto_approve_sats", "daily_budget_sats", "daily_budget_enforced"],
        "properties": {
          "auto_approve_sats": {
            "type": ["integer", "null"],
            "description": "Per-request cap in sats. Null means no cap; 0 refuses all paid inference."
          },
          "daily_budget_sats": {
            "type": "null",
            "description": "Reserved. Daily budgets are not enforced yet."
          },
          "daily_budget_enforced": {
            "const": false
          },
          "daily_budget_follow_up": {
            "type": "string"
          }
        }
      },
      "Balance": {
        "type": "object",
        "required": ["balance_sats", "token_hash"],
        "properties": {
          "balance_sats": {
            "type": "integer"
          },
          "token_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{16}\\.\\.\\.$",
            "description": "First 16 hex characters of the token's SHA-256 hash. Never the token itself."
          },
          "spend_policy": {
            "$ref": "#/components/schemas/SpendPolicy"
          }
        }
      },
      "ChatMessage": {
        "type": "object",
        "required": ["role"],
        "properties": {
          "role": {
            "type": "string"
          },
          "content": {
            "description": "Text or OpenAI-compatible content parts."
          }
        }
      },
      "ChatCompletionRequest": {
        "type": "object",
        "required": ["model", "messages"],
        "description": "OpenAI-compatible request. Other OpenAI fields (temperature, tools, response_format, and similar) are forwarded unchanged. JSON keys must not repeat, and the pricing fields (model, messages, max_tokens, max_completion_tokens, n) must be lowercase.",
        "properties": {
          "model": {
            "type": "string",
            "minLength": 1,
            "description": "A current id from GET /v1/models."
          },
          "messages": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/ChatMessage"
            }
          },
          "max_tokens": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000
          },
          "max_completion_tokens": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000
          },
          "n": {
            "type": "integer",
            "minimum": 1,
            "description": "The token limit multiplied by n must not exceed 1000000."
          },
          "stream": {
            "type": "boolean"
          }
        }
      },
      "ChatCompletion": {
        "type": "object",
        "description": "OpenAI chat.completion object.",
        "required": ["choices"],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string"
          },
          "created": {
            "type": "integer"
          },
          "model": {
            "type": "string"
          },
          "choices": {
            "type": "array"
          },
          "usage": {
            "type": "object"
          }
        }
      },
      "L402Challenge": {
        "type": "object",
        "required": ["error", "payment"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "const": "payment_required"
              },
              "code": {
                "const": "l402_challenge"
              }
            }
          },
          "payment": {
            "type": "object",
            "required": ["invoice", "amount_sats", "estimated_sats", "min_sats", "floored", "payment_hash", "macaroon", "prepaid"],
            "properties": {
              "invoice": {
                "type": "string",
                "description": "BOLT11 invoice for amount_sats. Same value as the WWW-Authenticate header."
              },
              "amount_sats": {
                "type": "integer",
                "minimum": 1,
                "description": "Estimated request cost, raised to min_sats when lower."
              },
              "estimated_sats": {
                "type": "integer",
                "minimum": 0
              },
              "min_sats": {
                "type": "integer",
                "minimum": 1,
                "description": "Current L402 floor. Equals payment_discovery.l402.min_amount_sats."
              },
              "floored": {
                "type": "boolean"
              },
              "payment_hash": {
                "type": "string",
                "pattern": "^[0-9a-f]{64}$"
              },
              "macaroon": {
                "type": "string"
              },
              "prepaid": {
                "type": "object",
                "required": ["method", "url", "min_amount_sats", "preferred"],
                "properties": {
                  "method": {
                    "const": "POST"
                  },
                  "url": {
                    "const": "https://api.loopxxi.com/v1/topup"
                  },
                  "min_amount_sats": {
                    "const": 5000
                  },
                  "preferred": {
                    "const": true
                  }
                }
              },
              "note": {
                "type": "string",
                "description": "Present when the body named no model; the challenge is then priced at the L402 floor."
              }
            }
          }
        }
      },
      "InsufficientFunds": {
        "type": "object",
        "required": ["error", "balance_sats", "refill_url"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "const": "insufficient_funds"
              },
              "code": {
                "enum": ["no_balance", "low_balance"]
              }
            }
          },
          "balance_sats": {
            "type": "integer"
          },
          "estimated_cost": {
            "type": "integer"
          },
          "refill_url": {
            "const": "https://api.loopxxi.com/v1/topup",
            "description": "POST endpoint, not a web page."
          }
        }
      },
      "BudgetExceeded": {
        "type": "object",
        "required": ["error", "balance_sats", "estimated_cost", "auto_approve_sats", "human_above"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "const": "human_above"
              },
              "code": {
                "const": "budget_exceeded"
              }
            }
          },
          "balance_sats": {
            "type": "integer"
          },
          "estimated_cost": {
            "type": "integer"
          },
          "auto_approve_sats": {
            "type": "integer"
          },
          "human_above": {
            "const": true
          }
        }
      },
      "ChatPaymentRequired": {
        "anyOf": [
          {
            "$ref": "#/components/schemas/L402Challenge"
          },
          {
            "$ref": "#/components/schemas/InsufficientFunds"
          },
          {
            "$ref": "#/components/schemas/BudgetExceeded"
          }
        ]
      },
      "ChatUnauthorized": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["message", "type", "code"],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "enum": ["authentication_error", "invalid_payment_credential"]
              },
              "code": {
                "enum": ["unknown_prepaid_token", "l402_request_not_authorized"]
              }
            }
          }
        }
      },
      "Model": {
        "type": "object",
        "required": ["id", "object", "name", "context_length", "pricing"],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          },
          "object": {
            "const": "model"
          },
          "name": {
            "type": "string"
          },
          "context_length": {
            "type": "integer",
            "minimum": 0
          },
          "pricing": {
            "type": "object",
            "required": ["prompt", "completion"],
            "description": "USD per token after the Gateway markup, as decimal strings. A negative value means the model has no fixed per-token price (dynamic routers); do not budget with it. Sats per million tokens are at GET /v1/pricing. Do not cache a price.",
            "properties": {
              "prompt": {
                "type": "string",
                "pattern": "^-?[0-9]+\\.[0-9]+$"
              },
              "completion": {
                "type": "string",
                "pattern": "^-?[0-9]+\\.[0-9]+$"
              }
            }
          }
        }
      },
      "ModelList": {
        "type": "object",
        "required": ["object", "data"],
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Model"
            }
          }
        }
      }
    }
  }
}
