{
  "openapi": "3.1.1",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "Stability Protocol Public Metal Prices API",
    "version": "1.0.0",
    "description": "Read-only public API of the existing metal-prices Cloudflare Worker. No API key, account, paid tier, or financial input is required. The site remains static and calculates Zakat locally. This API publishes prices and process health; it does not accept wealth, create calculations, or return a religious ruling. Browser CORS access is restricted to the existing first-party/localhost policy; command-line and server clients do not rely on CORS. Public errors use RFC 9457 application/problem+json with code and hint extensions plus the legacy error field. HEAD responses have the corresponding GET headers and no body, including errors. Other methods on these public paths return 405 with Allow: GET, HEAD, OPTIONS."
  },
  "servers": [
    {
      "url": "https://metal-prices.stachboo.workers.dev"
    }
  ],
  "security": [],
  "externalDocs": {
    "url": "https://stabilityprotocol.space/developers/",
    "description": "Developer quickstart, limitations, and local CLI usage"
  },
  "tags": [
    {
      "name": "Prices",
      "description": "Metal prices and derived nisab values"
    },
    {
      "name": "Health",
      "description": "Worker process health"
    }
  ],
  "paths": {
    "/prices": {
      "get": {
        "operationId": "getMetalPrices",
        "tags": [
          "Prices"
        ],
        "summary": "Get all available metal prices",
        "description": "Returns gold and silver prices per gram in the configured currencies plus derived 85g gold / 595g silver nisab values. No query parameters or currency filter are implemented; select a currency from prices locally. Upstream sources are gold-api.com, then a partly static metalpriceapi.com branch, then static estimates. FX uses Frankfurter v2, with a static fallback if fetching fails. A currency missing from an otherwise live FX response is omitted rather than filled from static rates. source containing fallback-static, or metalpriceapi.com (static silver), must not be treated as wholly live data. timestamp is response generation time, not an upstream quote timestamp. Responses are cached for CACHE_TTL seconds (currently 900). GET and HEAD each consume the approximate 60 requests / 60 seconds per-IP policy at the serving Cloudflare location, including cache hits. Successful responses omit remaining/reset quota headers because the binding exposes no such counters.",
        "responses": {
          "200": {
            "description": "All currently available configured currencies. Example values are illustrative. A successful HTTP status does not establish quote freshness or live provenance: inspect source and exchangeRateDates.",
            "headers": {
              "X-Cache": {
                "$ref": "#/components/headers/PriceCache"
              },
              "Cache-Control": {
                "description": "Public price cache lifetime in seconds, configured by CACHE_TTL (currently 900).",
                "schema": {
                  "type": "string"
                },
                "example": "public, max-age=900"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/PricesRateLimitPolicy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetalPricesResponse"
                },
                "examples": {
                  "liveSources": {
                    "summary": "Illustrative live-source response; missing currencies are omitted",
                    "value": {
                      "timestamp": "2026-10-25T00:00:00.000Z",
                      "source": "gold-api.com + frankfurter.dev (v2)",
                      "cached": false,
                      "exchangeRateDates": {
                        "EUR": "2026-10-23"
                      },
                      "prices": {
                        "USD": {
                          "goldPerGram": 100,
                          "silverPerGram": 1
                        },
                        "EUR": {
                          "goldPerGram": 92,
                          "silverPerGram": 0.92
                        }
                      },
                      "nisab": {
                        "USD": {
                          "gold85g": 8500,
                          "silver595g": 595
                        },
                        "EUR": {
                          "gold85g": 7820,
                          "silver595g": 547.4
                        }
                      }
                    }
                  },
                  "staticFallback": {
                    "summary": "Illustrative static fallback; do not treat as a live quote",
                    "value": {
                      "timestamp": "2026-10-25T00:00:00.000Z",
                      "source": "fallback-static",
                      "cached": false,
                      "exchangeRateDates": {},
                      "prices": {
                        "USD": {
                          "goldPerGram": 90,
                          "silverPerGram": 1
                        }
                      },
                      "nisab": {
                        "USD": {
                          "gold85g": 7650,
                          "silver595g": 595
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The approximate per-IP prices quota rejected this request. Wait at least Retry-After seconds; this is guidance, not a guaranteed reset time.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                },
                "description": "Errors must not be reused as price data."
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/PricesRateLimitPolicy"
              },
              "Retry-After": {
                "description": "Minimum suggested delay before retrying, in seconds.",
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                },
                "example": "60"
              },
              "RateLimit": {
                "description": "The limiter denied the request. No remaining count is advertised on successful responses because the binding only exposes a success boolean.",
                "schema": {
                  "type": "string"
                },
                "example": "\"prices\";r=0;t=60"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "about:blank",
                  "status": 429,
                  "title": "Too Many Requests",
                  "code": "RATE_LIMITED",
                  "detail": "Rate limit exceeded. Try again shortly.",
                  "hint": "Wait at least 60 seconds before retrying. Reuse the cached prices response when possible.",
                  "error": "Rate limit exceeded. Try again shortly."
                }
              }
            }
          },
          "500": {
            "description": "The prices request failed internally. No internal exception text is returned.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                },
                "description": "Errors must not be reused as price data."
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/PricesRateLimitPolicy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "example": {
                  "type": "about:blank",
                  "status": 500,
                  "title": "Internal Server Error",
                  "code": "INTERNAL_ERROR",
                  "detail": "The prices service could not complete the request.",
                  "hint": "Retry after a short delay. Check GET /status if the problem continues.",
                  "error": "The prices service could not complete the request."
                }
              }
            }
          }
        }
      },
      "head": {
        "operationId": "headMetalPrices",
        "tags": [
          "Prices"
        ],
        "summary": "Inspect metal price response headers",
        "description": "Runs the same cache and quota checks as GET /prices and returns no response body. A cold HEAD may populate the cache with a complete GET representation.",
        "responses": {
          "200": {
            "description": "All currently available configured currencies. Example values are illustrative. A successful HTTP status does not establish quote freshness or live provenance: inspect source and exchangeRateDates.",
            "headers": {
              "X-Cache": {
                "$ref": "#/components/headers/PriceCache"
              },
              "Cache-Control": {
                "description": "Public price cache lifetime in seconds, configured by CACHE_TTL (currently 900).",
                "schema": {
                  "type": "string"
                },
                "example": "public, max-age=900"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/PricesRateLimitPolicy"
              }
            }
          },
          "429": {
            "description": "The approximate per-IP prices quota rejected this request. Wait at least Retry-After seconds; this is guidance, not a guaranteed reset time.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                },
                "description": "Errors must not be reused as price data."
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/PricesRateLimitPolicy"
              },
              "Retry-After": {
                "description": "Minimum suggested delay before retrying, in seconds.",
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                },
                "example": "60"
              },
              "RateLimit": {
                "description": "The limiter denied the request. No remaining count is advertised on successful responses because the binding only exposes a success boolean.",
                "schema": {
                  "type": "string"
                },
                "example": "\"prices\";r=0;t=60"
              }
            }
          },
          "500": {
            "description": "The prices request failed internally. No internal exception text is returned.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                },
                "description": "Errors must not be reused as price data."
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/PricesRateLimitPolicy"
              }
            }
          }
        }
      },
      "options": {
        "operationId": "optionsMetalPrices",
        "tags": [
          "Prices"
        ],
        "summary": "Inspect prices CORS preflight policy",
        "description": "Read the existing first-party browser CORS policy without fetching prices or consuming the application price quota. Returns allowed methods and the price-rate policy with no response body.",
        "responses": {
          "204": {
            "description": "CORS preflight response, without a body. This does not consume a prices quota check.",
            "headers": {
              "Access-Control-Allow-Methods": {
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD, OPTIONS"
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/PricesRateLimitPolicy"
              }
            }
          }
        }
      }
    },
    "/status": {
      "get": {
        "operationId": "getServiceStatus",
        "tags": [
          "Health"
        ],
        "summary": "Get Worker process health",
        "description": "Public lightweight health response. It does not fetch prices, check upstream services, or check whether prices are live. No application quota limiter is invoked for this path.",
        "responses": {
          "200": {
            "description": "The Worker handler responds. This is not a metals quote freshness check or an upstream availability probe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceStatus"
                },
                "example": {
                  "ok": true,
                  "service": "metal-prices",
                  "timestamp": "2026-10-25T00:00:00.000Z",
                  "cacheTtl": 900
                }
              }
            }
          }
        }
      },
      "head": {
        "operationId": "headServiceStatus",
        "tags": [
          "Health"
        ],
        "summary": "Inspect health response headers",
        "description": "Same process health response headers as GET /status, without a body.",
        "responses": {
          "200": {
            "description": "The Worker handler responds. This is not a metals quote freshness check or an upstream availability probe."
          }
        }
      },
      "options": {
        "operationId": "optionsServiceStatus",
        "tags": [
          "Health"
        ],
        "summary": "Inspect health CORS preflight policy",
        "description": "Read the existing first-party browser CORS policy for the public service-health endpoint. Returns allowed methods and no response body; no application quota check or upstream fetch is performed.",
        "responses": {
          "204": {
            "description": "CORS preflight response, without a body. This does not consume a prices quota check.",
            "headers": {
              "Access-Control-Allow-Methods": {
                "schema": {
                  "type": "string",
                  "const": "GET, HEAD, OPTIONS"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "PricesRateLimitPolicy": {
        "description": "Configured approximate per-IP policy in each Cloudflare location. Uses draft-ietf-httpapi-ratelimit-headers-11 syntax (an Internet-Draft). The binding does not expose a remaining count or reset clock.",
        "schema": {
          "type": "string"
        },
        "example": "\"prices\";q=60;w=60"
      },
      "PriceCache": {
        "description": "Whether this response used the Worker Cache API. This header, rather than the legacy cached JSON field, reports a cache hit.",
        "schema": {
          "type": "string",
          "enum": [
            "HIT",
            "MISS"
          ]
        },
        "example": "HIT"
      }
    },
    "schemas": {
      "MetalPrice": {
        "type": "object",
        "required": [
          "goldPerGram",
          "silverPerGram"
        ],
        "additionalProperties": false,
        "properties": {
          "goldPerGram": {
            "type": "number",
            "description": "Gold price per gram in the dictionary currency. Source precision is retained until presentation."
          },
          "silverPerGram": {
            "type": "number",
            "description": "Silver price per gram in the dictionary currency. Source precision is retained until presentation."
          }
        }
      },
      "NisabValues": {
        "type": "object",
        "required": [
          "gold85g",
          "silver595g"
        ],
        "additionalProperties": false,
        "properties": {
          "gold85g": {
            "type": "number",
            "description": "Gold price per gram multiplied by 85, rounded to two decimal places."
          },
          "silver595g": {
            "type": "number",
            "description": "Silver price per gram multiplied by 595, rounded to two decimal places."
          }
        }
      },
      "MetalPricesResponse": {
        "type": "object",
        "required": [
          "timestamp",
          "source",
          "cached",
          "exchangeRateDates",
          "prices",
          "nisab"
        ],
        "additionalProperties": false,
        "properties": {
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When this response was generated, not the quote time; unchanged on cache hits."
          },
          "source": {
            "type": "string",
            "minLength": 1,
            "description": "Combined metal and FX source labels. Inspect for fallback-static or metalpriceapi.com before considering the data wholly live."
          },
          "cached": {
            "type": "boolean",
            "description": "Legacy payload field generated as false and preserved in the cached body. Read X-Cache to detect a cache hit."
          },
          "exchangeRateDates": {
            "type": "object",
            "description": "Currency code to upstream FX date. USD has no FX conversion date. Empty for static FX fallback.",
            "propertyNames": {
              "pattern": "^[A-Z]{3}$"
            },
            "additionalProperties": {
              "type": "string",
              "format": "date"
            }
          },
          "prices": {
            "type": "object",
            "description": "Only currencies with a usable FX rate are included; no implicit fallback to another currency.",
            "propertyNames": {
              "pattern": "^[A-Z]{3}$"
            },
            "additionalProperties": {
              "$ref": "#/components/schemas/MetalPrice"
            }
          },
          "nisab": {
            "type": "object",
            "description": "Derived thresholds for the same available currencies as prices.",
            "propertyNames": {
              "pattern": "^[A-Z]{3}$"
            },
            "additionalProperties": {
              "$ref": "#/components/schemas/NisabValues"
            }
          }
        }
      },
      "ServiceStatus": {
        "type": "object",
        "required": [
          "ok",
          "service",
          "timestamp",
          "cacheTtl"
        ],
        "additionalProperties": false,
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "service": {
            "type": "string",
            "const": "metal-prices"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "cacheTtl": {
            "type": "integer",
            "description": "Configured price cache lifetime in seconds.",
            "examples": [
              900
            ]
          }
        }
      },
      "Problem": {
        "type": "object",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "detail",
          "hint",
          "error"
        ],
        "description": "RFC 9457 problem details. Parse status/code for control flow; detail/hint/error are human-readable. Unknown extra fields may be added in future.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "const": "about:blank",
            "description": "HTTP status semantics; application-specific control flow uses code."
          },
          "title": {
            "type": "string",
            "minLength": 1,
            "description": "Standard HTTP status title."
          },
          "status": {
            "type": "integer",
            "enum": [
              404,
              405,
              429,
              500
            ],
            "description": "Matches the response HTTP status."
          },
          "code": {
            "type": "string",
            "enum": [
              "NOT_FOUND",
              "METHOD_NOT_ALLOWED",
              "RATE_LIMITED",
              "INTERNAL_ERROR"
            ]
          },
          "detail": {
            "type": "string",
            "minLength": 1
          },
          "hint": {
            "type": "string",
            "minLength": 1,
            "description": "Suggested recovery action; no internal exception or request body is echoed."
          },
          "error": {
            "type": "string",
            "minLength": 1,
            "description": "Legacy compatibility field with the same value as detail."
          }
        }
      }
    },
    "responses": {
      "NotFound": {
        "description": "No public route matches the requested path.",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string",
              "const": "no-store"
            },
            "description": "Errors must not be reused as price data."
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "about:blank",
              "status": 404,
              "title": "Not Found",
              "code": "NOT_FOUND",
              "detail": "The requested public endpoint was not found.",
              "hint": "Use GET /prices for metal prices or GET /status for service health.",
              "error": "The requested public endpoint was not found."
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "A method other than GET, HEAD, or OPTIONS was used for a public endpoint.",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string",
              "const": "no-store"
            },
            "description": "Errors must not be reused as price data."
          },
          "Allow": {
            "schema": {
              "type": "string",
              "const": "GET, HEAD, OPTIONS"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            },
            "example": {
              "type": "about:blank",
              "status": 405,
              "title": "Method Not Allowed",
              "code": "METHOD_NOT_ALLOWED",
              "detail": "This public endpoint only accepts GET, HEAD, and OPTIONS.",
              "hint": "Use GET for JSON or HEAD for response headers without a body.",
              "error": "This public endpoint only accepts GET, HEAD, and OPTIONS."
            }
          }
        }
      }
    }
  }
}
