diff --git a/Cargo.lock b/Cargo.lock index 25b66a377..9e5cf663d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -282,6 +293,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bech32" version = "0.11.1" @@ -629,6 +646,21 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "brk_mcp" +version = "0.3.6" +dependencies = [ + "axum", + "base64 0.23.1", + "brk_logger", + "rmcp", + "serde", + "serde_json", + "tokio", + "tracing", + "ureq", +] + [[package]] name = "brk_mempool" version = "0.3.6" @@ -931,6 +963,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -1676,6 +1709,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.33" @@ -1683,6 +1731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1691,6 +1740,34 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "futures-sink" version = "0.3.33" @@ -1709,8 +1786,13 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -2596,6 +2678,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pathfinder_geometry" version = "0.5.1" @@ -3050,6 +3138,36 @@ dependencies = [ "libc", ] +[[package]] +name = "rmcp" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad26b216c966e987e80e86daf784a455c039c43d98575ceed57b8faa259e5695" +dependencies = [ + "async-trait", + "base64 0.23.1", + "bytes", + "chrono", + "futures", + "http", + "http-body", + "http-body-util", + "pastey", + "pin-project-lite", + "rand 0.10.2", + "schemars", + "serde", + "serde_json", + "sse-stream", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", + "tracing", + "uuid", +] + [[package]] name = "rustc-demangle" version = "0.1.28" @@ -3146,6 +3264,7 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ + "chrono", "dyn-clone", "indexmap", "ref-cast", @@ -3439,6 +3558,19 @@ dependencies = [ "lock_api", ] +[[package]] +name = "sse-stream" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c123f296ade4ec4b8b0f6162116e6629f5146922ca5ab40ca9d3c2e73ab4761e" +dependencies = [ + "bytes", + "futures-util", + "http-body", + "http-body-util", + "pin-project-lite", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3675,6 +3807,7 @@ version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", @@ -3694,6 +3827,17 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.19" @@ -3703,6 +3847,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] @@ -3971,6 +4116,7 @@ version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] diff --git a/crates/brk_bindgen/src/generators/llm/manifest.rs b/crates/brk_bindgen/src/generators/llm/manifest.rs new file mode 100644 index 000000000..c5f335d95 --- /dev/null +++ b/crates/brk_bindgen/src/generators/llm/manifest.rs @@ -0,0 +1,624 @@ +use std::{ + collections::{BTreeMap, BTreeSet, VecDeque}, + io, + path::Path, +}; + +use serde::Serialize; +use serde_json::{Map, Value, json}; + +use crate::{Endpoint, Parameter, TypeSchemas, generators::write_if_changed}; + +const MANIFEST_SCHEMA_VERSION: u32 = 1; +const JSON_SCHEMA_2020_12: &str = "https://json-schema.org/draft/2020-12/schema"; +const COMPONENT_REF_PREFIX: &str = "#/components/schemas/"; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Manifest { + schema_version: u32, + operations: Vec, +} + +#[derive(Serialize)] +struct ManifestOperation { + tool: ManifestTool, + http: ManifestHttpOperation, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ManifestTool { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + input_schema: Value, + #[serde(skip_serializing_if = "Option::is_none")] + output_schema: Option, + annotations: ManifestToolAnnotations, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ManifestToolAnnotations { + read_only_hint: bool, + destructive_hint: bool, + idempotent_hint: bool, + open_world_hint: bool, +} + +#[derive(Serialize)] +struct ManifestHttpOperation { + method: &'static str, + path: String, + parameters: Vec, +} + +#[derive(Serialize)] +struct ManifestParameter { + name: String, + location: ParameterLocation, +} + +#[derive(Clone, Copy, Serialize)] +#[serde(rename_all = "snake_case")] +enum ParameterLocation { + Path, + Query, +} + +/// Generate the immutable machine-readable tool catalog in the LLM bundle. +/// Every non-deprecated GET operation is included; all other methods are +/// excluded structurally because the current consumer is a read-only cache +/// bridge. +pub(super) fn generate_tool_manifest( + endpoints: &[Endpoint], + schemas: &TypeSchemas, + path: &Path, +) -> io::Result<()> { + let content = render_tool_manifest(endpoints, schemas)?; + write_if_changed(path, &content) +} + +fn render_tool_manifest(endpoints: &[Endpoint], schemas: &TypeSchemas) -> io::Result { + let mut names = BTreeSet::new(); + let mut operations = endpoints + .iter() + .filter(|endpoint| endpoint.method == "GET" && !endpoint.deprecated) + .map(|endpoint| operation_from_endpoint(endpoint, schemas, &mut names)) + .collect::>>()?; + + operations.sort_by(|a, b| a.tool.name.cmp(&b.tool.name)); + + let manifest = Manifest { + schema_version: MANIFEST_SCHEMA_VERSION, + operations, + }; + let mut content = serde_json::to_string_pretty(&manifest) + .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + content.push('\n'); + Ok(content) +} + +fn operation_from_endpoint( + endpoint: &Endpoint, + schemas: &TypeSchemas, + names: &mut BTreeSet, +) -> io::Result { + let name = endpoint.operation_id.clone().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "MCP generation requires a stable operationId for GET {}", + endpoint.path + ), + ) + })?; + validate_tool_name(&name)?; + if !names.insert(name.clone()) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("duplicate MCP tool name: {name}"), + )); + } + + let input_schema = build_input_schema(endpoint, schemas)?; + let output_schema = build_output_schema(endpoint, schemas)?; + let parameters = endpoint + .path_params + .iter() + .map(|parameter| manifest_parameter(parameter, ParameterLocation::Path)) + .chain( + endpoint + .query_params + .iter() + .map(|parameter| manifest_parameter(parameter, ParameterLocation::Query)), + ) + .collect(); + + Ok(ManifestOperation { + tool: ManifestTool { + name, + title: endpoint.summary.clone(), + description: tool_description(endpoint), + input_schema, + output_schema, + annotations: ManifestToolAnnotations { + read_only_hint: true, + destructive_hint: false, + idempotent_hint: true, + open_world_hint: true, + }, + }, + http: ManifestHttpOperation { + method: "GET", + path: endpoint.path.clone(), + parameters, + }, + }) +} + +fn tool_description(endpoint: &Endpoint) -> Option { + let description = endpoint + .description + .as_deref() + .or(endpoint.summary.as_deref())?; + Some(format!( + "{description}\n\nREST operation: `{} {}`.", + endpoint.method, endpoint.path + )) +} + +fn build_output_schema(endpoint: &Endpoint, schemas: &TypeSchemas) -> io::Result> { + let Some(json_schema) = endpoint.json_response_schema.clone() else { + return Ok(None); + }; + + // Series endpoints can return either JSON or CSV depending on `format`. + // Both successful representations must satisfy the advertised contract. + let root = if endpoint.supports_csv { + json!({ + "anyOf": [ + json_schema, + { + "type": "string", + "contentMediaType": "text/csv" + } + ] + }) + } else { + json_schema + }; + + let mut schema = standalone_schema(root, schemas)?; + strip_output_annotations(&mut schema); + Ok(Some(schema)) +} + +/// Output schemas are validation contracts, not a second documentation +/// bundle. Remove annotation-only keywords while preserving every structural +/// and validation keyword. Traversal is schema-aware so an output property +/// literally named `description`, `title`, or `default` is never removed. +fn strip_output_annotations(schema: &mut Value) { + let Some(object) = schema.as_object_mut() else { + return; + }; + for key in [ + "title", + "description", + "default", + "examples", + "deprecated", + "readOnly", + "writeOnly", + "$comment", + ] { + object.remove(key); + } + + for key in [ + "$defs", + "definitions", + "properties", + "patternProperties", + "dependentSchemas", + ] { + if let Some(schemas) = object.get_mut(key).and_then(Value::as_object_mut) { + for schema in schemas.values_mut() { + strip_output_annotations(schema); + } + } + } + for key in [ + "additionalProperties", + "contains", + "contentSchema", + "else", + "if", + "items", + "not", + "propertyNames", + "then", + "unevaluatedItems", + "unevaluatedProperties", + ] { + if let Some(schema) = object.get_mut(key) { + strip_output_annotations(schema); + } + } + for key in ["allOf", "anyOf", "oneOf", "prefixItems"] { + if let Some(schemas) = object.get_mut(key).and_then(Value::as_array_mut) { + for schema in schemas { + strip_output_annotations(schema); + } + } + } +} + +fn manifest_parameter(parameter: &Parameter, location: ParameterLocation) -> ManifestParameter { + ManifestParameter { + name: parameter.name.clone(), + location, + } +} + +fn build_input_schema(endpoint: &Endpoint, schemas: &TypeSchemas) -> io::Result { + let mut properties = Map::new(); + let mut required = Vec::new(); + + for parameter in endpoint + .path_params + .iter() + .chain(endpoint.query_params.iter()) + { + let mut schema = if parameter.schema.as_object().is_some_and(Map::is_empty) { + schema_from_type_name(¶meter.param_type) + } else { + parameter.schema.clone() + }; + if let Some(description) = ¶meter.description + && let Some(object) = schema.as_object_mut() + { + object + .entry("description") + .or_insert_with(|| Value::String(description.clone())); + } + properties.insert(parameter.name.clone(), schema); + if parameter.required { + required.push(Value::String(parameter.name.clone())); + } + } + + let mut root = Map::from_iter([ + ("type".to_string(), Value::String("object".to_string())), + ("properties".to_string(), Value::Object(properties)), + ("additionalProperties".to_string(), Value::Bool(false)), + ]); + if !required.is_empty() { + root.insert("required".to_string(), Value::Array(required)); + } + + standalone_schema(Value::Object(root), schemas) +} + +fn schema_from_type_name(name: &str) -> Value { + if let Some(inner) = name.strip_suffix("[]") { + return json!({ + "type": "array", + "items": schema_from_type_name(inner) + }); + } + + match name { + "*" | "Object" => json!({}), + "string" => json!({ "type": "string" }), + "number" => json!({ "type": "number" }), + "integer" => json!({ "type": "integer" }), + "boolean" => json!({ "type": "boolean" }), + "null" => json!({ "type": "null" }), + other => json!({ "$ref": format!("{COMPONENT_REF_PREFIX}{other}") }), + } +} + +/// Turn an OpenAPI schema into a self-contained JSON Schema. MCP clients must +/// not fetch external `$ref` targets, so component references are rewritten to +/// local `$defs` and only the transitively used definitions are copied. +fn standalone_schema(mut root: Value, schemas: &TypeSchemas) -> io::Result { + if root.is_boolean() { + root = if root == Value::Bool(true) { + json!({}) + } else { + json!({ "not": {} }) + }; + } + if !root.is_object() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "MCP schemas must be JSON objects", + )); + } + + let mut pending = VecDeque::new(); + let mut seen = BTreeSet::new(); + collect_component_refs(&root, &mut pending)?; + + let mut definitions = BTreeMap::new(); + while let Some(name) = pending.pop_front() { + if !seen.insert(name.clone()) { + continue; + } + let schema = schemas.get(&name).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("OpenAPI schema references missing component {name}"), + ) + })?; + collect_component_refs(schema, &mut pending)?; + definitions.insert(name, schema.clone()); + } + + rewrite_component_refs(&mut root)?; + for schema in definitions.values_mut() { + rewrite_component_refs(schema)?; + } + + let object = root.as_object_mut().expect("root checked above"); + object.insert( + "$schema".to_string(), + Value::String(JSON_SCHEMA_2020_12.to_string()), + ); + if !definitions.is_empty() { + object.insert( + "$defs".to_string(), + Value::Object(definitions.into_iter().collect()), + ); + } + Ok(root) +} + +fn collect_component_refs(value: &Value, pending: &mut VecDeque) -> io::Result<()> { + match value { + Value::Object(object) => { + if let Some(reference) = object.get("$ref").and_then(Value::as_str) { + if let Some(name) = reference.strip_prefix(COMPONENT_REF_PREFIX) { + pending.push_back(unescape_json_pointer(name)); + } else if !reference.starts_with("#/$defs/") && !reference.starts_with("#/") { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("external JSON Schema reference is not supported: {reference}"), + )); + } + } + for nested in object.values() { + collect_component_refs(nested, pending)?; + } + } + Value::Array(array) => { + for nested in array { + collect_component_refs(nested, pending)?; + } + } + _ => {} + } + Ok(()) +} + +fn rewrite_component_refs(value: &mut Value) -> io::Result<()> { + match value { + Value::Object(object) => { + if let Some(reference) = object.get_mut("$ref") { + let replacement = reference + .as_str() + .and_then(|reference| reference.strip_prefix(COMPONENT_REF_PREFIX)) + .map(|name| { + format!( + "#/$defs/{}", + escape_json_pointer(&unescape_json_pointer(name)) + ) + }); + if let Some(replacement) = replacement { + *reference = Value::String(replacement); + } + } + for nested in object.values_mut() { + rewrite_component_refs(nested)?; + } + } + Value::Array(array) => { + for nested in array { + rewrite_component_refs(nested)?; + } + } + _ => {} + } + Ok(()) +} + +fn validate_tool_name(name: &str) -> io::Result<()> { + let valid = (1..=128).contains(&name.len()) + && name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')); + if valid { + Ok(()) + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid MCP tool name: {name}"), + )) + } +} + +fn escape_json_pointer(value: &str) -> String { + value.replace('~', "~0").replace('/', "~1") +} + +fn unescape_json_pointer(value: &str) -> String { + value.replace("~1", "/").replace("~0", "~") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Parameter, ResponseKind, TextSchema}; + + fn endpoint(name: &str, method: &str) -> Endpoint { + Endpoint { + method: method.to_string(), + path: "/api/items/{id}".to_string(), + operation_id: Some(name.to_string()), + summary: Some("Read item".to_string()), + description: Some("Returns one item.".to_string()), + path_params: vec![Parameter { + name: "id".to_string(), + required: true, + param_type: "ItemId".to_string(), + description: Some("Item identifier".to_string()), + schema: json!({ "$ref": "#/components/schemas/ItemId" }), + }], + query_params: Vec::new(), + request_body: None, + response_kind: ResponseKind::Text(Some(TextSchema { + name: "ItemId".to_string(), + is_numeric: true, + })), + json_response_schema: Some(json!({ + "$ref": "#/components/schemas/Item" + })), + deprecated: false, + supports_csv: false, + } + } + + #[test] + fn emits_only_active_gets_in_name_order() { + let mut post = endpoint("post_item", "POST"); + post.path = "/api/items".to_string(); + let mut deprecated = endpoint("old_item", "GET"); + deprecated.deprecated = true; + let get = endpoint("get_item", "GET"); + let schemas = BTreeMap::from([ + ("ItemId".to_string(), json!({ "type": "integer" })), + ( + "Item".to_string(), + json!({ + "description": "Item response documentation", + "type": "object", + "properties": { + "id": { "$ref": "#/components/schemas/ItemId" }, + "description": { + "description": "Human-readable item description", + "type": "string", + "examples": ["example"], + "default": "example" + } + }, + "required": ["id"] + }), + ), + ]); + + let manifest = render_tool_manifest(&[post, deprecated, get], &schemas).unwrap(); + let value: Value = serde_json::from_str(&manifest).unwrap(); + let operations = value["operations"].as_array().unwrap(); + + assert_eq!(operations.len(), 1); + assert_eq!(operations[0]["tool"]["name"], "get_item"); + assert_eq!(operations[0]["http"]["method"], "GET"); + assert_eq!( + operations[0]["tool"]["description"], + "Returns one item.\n\nREST operation: `GET /api/items/{id}`." + ); + assert_eq!( + operations[0]["tool"]["inputSchema"]["properties"]["id"]["$ref"], + "#/$defs/ItemId" + ); + assert_eq!( + operations[0]["tool"]["inputSchema"]["$defs"]["ItemId"]["type"], + "integer" + ); + assert_eq!( + operations[0]["tool"]["outputSchema"]["$ref"], + "#/$defs/Item" + ); + assert_eq!( + operations[0]["tool"]["outputSchema"]["$defs"]["Item"]["properties"]["id"]["$ref"], + "#/$defs/ItemId" + ); + assert_eq!( + operations[0]["tool"]["outputSchema"]["$defs"]["ItemId"]["type"], + "integer" + ); + assert!( + operations[0]["tool"]["outputSchema"]["$defs"]["Item"] + .get("description") + .is_none() + ); + let description_property = + &operations[0]["tool"]["outputSchema"]["$defs"]["Item"]["properties"]["description"]; + assert_eq!(description_property["type"], "string"); + assert!(description_property.get("description").is_none()); + assert!(description_property.get("examples").is_none()); + assert!(description_property.get("default").is_none()); + } + + #[test] + fn csv_output_schema_accepts_json_or_csv_text() { + let mut endpoint = endpoint("get_item", "GET"); + endpoint.supports_csv = true; + let schemas = BTreeMap::from([ + ("ItemId".to_string(), json!({ "type": "integer" })), + ( + "Item".to_string(), + json!({ + "type": "object", + "properties": { + "id": { "$ref": "#/components/schemas/ItemId" } + } + }), + ), + ]); + + let manifest = render_tool_manifest(&[endpoint], &schemas).unwrap(); + let value: Value = serde_json::from_str(&manifest).unwrap(); + let output = &value["operations"][0]["tool"]["outputSchema"]; + + assert_eq!(output["anyOf"][0]["$ref"], "#/$defs/Item"); + assert_eq!(output["anyOf"][1]["type"], "string"); + assert_eq!(output["anyOf"][1]["contentMediaType"], "text/csv"); + } + + #[test] + fn omits_output_schema_without_a_json_response_schema() { + let mut endpoint = endpoint("get_item", "GET"); + endpoint.json_response_schema = None; + let schemas = BTreeMap::from([("ItemId".to_string(), json!({ "type": "integer" }))]); + + let manifest = render_tool_manifest(&[endpoint], &schemas).unwrap(); + let value: Value = serde_json::from_str(&manifest).unwrap(); + + assert!(value["operations"][0]["tool"].get("outputSchema").is_none()); + } + + #[test] + fn rejects_missing_operation_id() { + let mut endpoint = endpoint("get_item", "GET"); + endpoint.operation_id = None; + let error = render_tool_manifest(&[endpoint], &BTreeMap::new()).unwrap_err(); + assert!(error.to_string().contains("requires a stable operationId")); + } + + #[test] + fn rejects_duplicate_operation_ids() { + let first = endpoint("get_item", "GET"); + let second = endpoint("get_item", "GET"); + let schemas = BTreeMap::from([ + ("ItemId".to_string(), json!({ "type": "integer" })), + ("Item".to_string(), json!({ "type": "object" })), + ]); + let error = render_tool_manifest(&[first, second], &schemas).unwrap_err(); + assert!(error.to_string().contains("duplicate MCP tool name")); + } +} diff --git a/crates/brk_bindgen/src/generators/llm/mod.rs b/crates/brk_bindgen/src/generators/llm/mod.rs index 76b8e2b96..5a064286a 100644 --- a/crates/brk_bindgen/src/generators/llm/mod.rs +++ b/crates/brk_bindgen/src/generators/llm/mod.rs @@ -14,6 +14,8 @@ use crate::{ClientMetadata, Endpoint, ResponseKind, TypeSchemas}; use super::write_if_changed; +mod manifest; + const BASE_URL: &str = "https://bitview.space"; pub fn generate_llm_clients( @@ -22,6 +24,7 @@ pub fn generate_llm_clients( endpoints: &[Endpoint], schemas: &TypeSchemas, roots: &[PathBuf], + manifest_path: Option<&Path>, ) -> io::Result<()> { let metric_count = count_metrics(&metadata.catalog); let generated = endpoints @@ -47,6 +50,12 @@ pub fn generate_llm_clients( write_output(&root.join("llms.txt"), &llms)?; write_output(&root.join("llms-full.txt"), &llms_full)?; } + if let Some(path) = manifest_path { + if let Some(parent) = path.parent() { + create_dir_all(parent)?; + } + manifest::generate_tool_manifest(endpoints, schemas, path)?; + } Ok(()) } @@ -440,10 +449,14 @@ mod tests { required: true, param_type: "string".to_owned(), description: Some("Thing identifier".to_owned()), + schema: serde_json::json!({ "type": "string" }), }], query_params: Vec::new(), request_body: None, response_kind: ResponseKind::Json("Thing".to_owned()), + json_response_schema: Some(serde_json::json!({ + "$ref": "#/components/schemas/Thing" + })), deprecated: false, supports_csv: false, } diff --git a/crates/brk_bindgen/src/lib.rs b/crates/brk_bindgen/src/lib.rs index b8bdddc58..7bd9f11f8 100644 --- a/crates/brk_bindgen/src/lib.rs +++ b/crates/brk_bindgen/src/lib.rs @@ -16,6 +16,7 @@ use brk_query::Vecs; /// .rust("crates/brk_client/src/lib.rs") /// .javascript("modules/brk-client/index.js") /// .python("packages/brk_client/__init__.py") +/// .llm_manifest("crates/brk_mcp/generated/manifest.json") /// .llm("website") /// .llm("website_next"); /// ``` @@ -29,6 +30,8 @@ pub struct ClientOutputPaths { pub python: Option, /// Root directories for generated LLM client bundles. pub llm: Vec, + /// Full path to the machine-readable tool manifest in the LLM bundle. + pub llm_manifest: Option, } impl ClientOutputPaths { @@ -55,6 +58,11 @@ impl ClientOutputPaths { self.llm.push(root.into()); self } + + pub fn llm_manifest(mut self, path: impl Into) -> Self { + self.llm_manifest = Some(path.into()); + self + } } mod analysis; @@ -86,6 +94,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// .rust("crates/brk_client/src/lib.rs") /// .javascript("modules/brk-client/index.js") /// .python("packages/brk_client/__init__.py") +/// .llm_manifest("crates/brk_mcp/generated/manifest.json") /// .llm("website") /// .llm("website_next"); /// @@ -137,7 +146,14 @@ pub fn generate_clients( generate_python_client(&metadata, &endpoints, &schemas, python_path)?; } - generate_llm_clients(&metadata, &spec, &endpoints, &schemas, &output_paths.llm)?; + generate_llm_clients( + &metadata, + &spec, + &endpoints, + &schemas, + &output_paths.llm, + output_paths.llm_manifest.as_deref(), + )?; Ok(()) } diff --git a/crates/brk_bindgen/src/openapi/endpoint.rs b/crates/brk_bindgen/src/openapi/endpoint.rs index bc14ac728..deb8b256f 100644 --- a/crates/brk_bindgen/src/openapi/endpoint.rs +++ b/crates/brk_bindgen/src/openapi/endpoint.rs @@ -1,4 +1,5 @@ use crate::openapi::{Parameter, ResponseKind}; +use serde_json::Value; /// Request body shape for POST/PUT/PATCH endpoints. #[derive(Debug, Clone)] @@ -30,6 +31,8 @@ pub struct Endpoint { pub request_body: Option, /// Body kind for the 200 response. pub response_kind: ResponseKind, + /// Raw JSON schema for the application/json 200 response, when present. + pub json_response_schema: Option, /// Whether this endpoint is deprecated pub deprecated: bool, /// Whether this endpoint supports CSV format (text/csv content type) diff --git a/crates/brk_bindgen/src/openapi/mod.rs b/crates/brk_bindgen/src/openapi/mod.rs index d5b1a0f63..fde41b392 100644 --- a/crates/brk_bindgen/src/openapi/mod.rs +++ b/crates/brk_bindgen/src/openapi/mod.rs @@ -129,6 +129,7 @@ fn extract_endpoint( let query_params = extract_parameters(operation, ParameterIn::Query); let response_kind = extract_response_kind(operation, spec); + let json_response_schema = extract_json_response_schema(operation); let request_body = extract_request_body(operation); let supports_csv = check_csv_support(operation); @@ -142,11 +143,29 @@ fn extract_endpoint( query_params, request_body, response_kind, + json_response_schema, deprecated: operation.deprecated.unwrap_or(false), supports_csv, }) } +/// Preserve the complete JSON response schema for MCP output schema +/// generation. The regular clients only need [`ResponseKind`], while MCP +/// clients benefit from the exact self-contained response shape. +fn extract_json_response_schema(operation: &Operation) -> Option { + let response = + operation + .responses + .as_ref()? + .get("200") + .and_then(|response| match response { + ObjectOrReference::Object(response) => Some(response), + ObjectOrReference::Ref { .. } => None, + })?; + let schema = response.content.get("application/json")?.schema.as_ref()?; + serde_json::to_value(schema).ok() +} + /// Extract the request body shape, if any. /// Prefers `text/plain` (string) over `application/json` (typed). fn extract_request_body(operation: &Operation) -> Option { @@ -225,6 +244,11 @@ fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec None, diff --git a/crates/brk_bindgen/src/openapi/parameter.rs b/crates/brk_bindgen/src/openapi/parameter.rs index e2718b821..95a0443f6 100644 --- a/crates/brk_bindgen/src/openapi/parameter.rs +++ b/crates/brk_bindgen/src/openapi/parameter.rs @@ -1,3 +1,5 @@ +use serde_json::Value; + /// Parameter information. #[derive(Debug, Clone)] pub struct Parameter { @@ -5,4 +7,6 @@ pub struct Parameter { pub required: bool, pub param_type: String, pub description: Option, + /// Original OpenAPI/JSON Schema for schema-driven generators. + pub schema: Value, } diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index cf27c4ceb..b8a34611e 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -9842,7 +9842,7 @@ impl BrkClient { /// Health check /// - /// Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`. + /// Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, request `GET /api/server/sync`. /// /// Endpoint: `GET /health` pub fn get_health(&self) -> Result { @@ -9962,7 +9962,7 @@ impl BrkClient { /// Get raw series data /// - /// Returns just the data array without the SeriesData wrapper. Supports the same range and format parameters as the standard endpoint. + /// Returns just the data array without the SeriesData wrapper. Supports the same range and format parameters as `GET /api/series/{series}/{index}`. /// /// Endpoint: `GET /api/series/{series}/{index}/data` pub fn get_series_data(&self, series: SeriesName, index: Index, start: Option, end: Option, limit: Option, format: Option) -> Result>> { @@ -10053,9 +10053,7 @@ impl BrkClient { /// Latest URPD /// - /// URPD for the most recent available date in the cohort. The response's `date` field echoes which date was served. - /// - /// See the URPD tag description for the response shape, `agg`, and `weight` options. + /// URPD for the most recent available date in the cohort. The response's `date` field echoes which date was served. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }`. `close` and each bucket's `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and bucket `supply` are BTC. `unrealized_pnl` can be negative. /// /// Endpoint: `GET /api/urpd/{cohort}` pub fn get_urpd(&self, cohort: Cohort, agg: Option, weight: Option) -> Result { @@ -10069,9 +10067,7 @@ impl BrkClient { /// URPD at date /// - /// URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. - /// - /// See the URPD tag description for unit conventions, `agg`, and `weight` options. + /// URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. `close`, `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and `supply` are BTC. `unrealized_pnl` can be negative. /// /// Endpoint: `GET /api/urpd/{cohort}/{date}` pub fn get_urpd_at(&self, cohort: Cohort, date: &str, agg: Option, weight: Option) -> Result { @@ -10122,7 +10118,7 @@ impl BrkClient { /// Address hash-prefix matches /// - /// Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`. + /// Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata with `GET /api/address/{address}`. /// /// Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}` pub fn get_address_hash_prefix_matches(&self, addr_type: OutputType, prefix: &str) -> Result { @@ -10143,7 +10139,7 @@ impl BrkClient { /// Address transactions /// - /// Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, use `/address/{address}/txs/chain/{last_seen_txid}`. + /// Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* /// @@ -10154,7 +10150,7 @@ impl BrkClient { /// Address confirmed transactions /// - /// Get the first 25 confirmed transactions for an address. For pagination, use the path-style form `/txs/chain/{last_seen_txid}`. + /// Get the first 25 confirmed transactions for an address. For pagination, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)* /// @@ -10669,7 +10665,7 @@ impl BrkClient { /// Recent full-RBF replacements /// - /// Like `/api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). + /// Same response shape as `GET /api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)* /// @@ -10680,7 +10676,7 @@ impl BrkClient { /// Projected next block template /// - /// Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. + /// Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. /// /// Endpoint: `GET /api/v1/mempool/block-template` pub fn get_block_template(&self) -> Result { @@ -10689,7 +10685,7 @@ impl BrkClient { /// Block template diff since hash /// - /// Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`. + /// Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `GET /api/v1/mempool/block-template`. /// /// Endpoint: `GET /api/v1/mempool/block-template/diff/{hash}` pub fn get_block_template_diff(&self, hash: NextBlockHash) -> Result { @@ -10707,7 +10703,7 @@ impl BrkClient { /// Live BTC/USD price /// - /// Current BTC/USD price in dollars. Same value as `/api/mempool/price`. Confirmed per-height history is available at `/api/vecs/height-to-price`. + /// Current BTC/USD price in dollars. Same value as `GET /api/mempool/price`. Confirmed per-height history is available at `GET /api/series/price/height`. /// /// Endpoint: `GET /api/oracle/price` pub fn get_oracle_price(&self) -> Result { @@ -10906,7 +10902,7 @@ impl BrkClient { /// Compact OpenAPI specification /// - /// Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`. + /// Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. The full specification is available at `GET /openapi.json`. /// /// Endpoint: `GET /api.json` pub fn get_api(&self) -> Result { diff --git a/crates/brk_computer/examples/address_duplicate_audit.rs b/crates/brk_computer/examples/address_duplicate_audit.rs deleted file mode 100644 index 11ed82d59..000000000 --- a/crates/brk_computer/examples/address_duplicate_audit.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::{collections::HashSet, env, path::Path, time::Instant}; - -use brk_indexer::Indexer; -use brk_types::{Height, OutputType, TypeIndex}; -use vecdb::{AnyVec, ReadableVec}; - -#[derive(Default)] -struct Counts { - events: u64, - new: u64, - cached: u64, - disk_reads: u64, - unique_disk_reads: u64, -} - -fn first_index(indexer: &Indexer, output_type: OutputType, height: Height) -> TypeIndex { - match output_type { - OutputType::P2PK65 => indexer.vecs.addrs.p2pk65.first_index.collect_one(height).into(), - OutputType::P2PK33 => indexer.vecs.addrs.p2pk33.first_index.collect_one(height).into(), - OutputType::P2PKH => indexer.vecs.addrs.p2pkh.first_index.collect_one(height).into(), - OutputType::P2SH => indexer.vecs.addrs.p2sh.first_index.collect_one(height).into(), - OutputType::P2WPKH => indexer.vecs.addrs.p2wpkh.first_index.collect_one(height).into(), - OutputType::P2WSH => indexer.vecs.addrs.p2wsh.first_index.collect_one(height).into(), - OutputType::P2TR => indexer.vecs.addrs.p2tr.first_index.collect_one(height).into(), - OutputType::P2A => indexer.vecs.addrs.p2a.first_index.collect_one(height).into(), - _ => unreachable!(), - } -} - -fn main() { - let outputs_dir = Path::new(&env::var("HOME").unwrap()).join(".brk"); - let indexer = Indexer::forced_import(&outputs_dir).unwrap(); - - let from = 950_001usize; - let to = 960_001usize; - let start = Instant::now(); - - let output_starts = indexer - .vecs - .outputs - .first_txout_index - .collect_range_at(from, to + 1); - let input_starts = indexer - .vecs - .inputs - .first_txin_index - .collect_range_at(from, to + 1); - - let output_from = output_starts[0].to_usize(); - let output_types = indexer - .vecs - .outputs - .output_type - .collect_range_at(output_from, output_starts.last().unwrap().to_usize()); - let output_indexes = indexer - .vecs - .outputs - .type_index - .collect_range_at(output_from, output_starts.last().unwrap().to_usize()); - - let input_from = input_starts[0].to_usize(); - let input_types = indexer - .vecs - .inputs - .output_type - .collect_range_at(input_from, input_starts.last().unwrap().to_usize()); - let input_indexes = indexer - .vecs - .inputs - .type_index - .collect_range_at(input_from, input_starts.last().unwrap().to_usize()); - - let mut cache = HashSet::new(); - let mut counts = Counts::default(); - - for block_offset in 0..to - from { - let height = Height::from(from + block_offset); - let first_indexes = OutputType::ADDR_TYPES.map(|output_type| { - ( - output_type, - first_index(&indexer, output_type, height), - ) - }); - let mut block_misses = HashSet::new(); - let mut block_addresses = HashSet::new(); - - let output_start = output_starts[block_offset].to_usize() - output_from; - let output_end = output_starts[block_offset + 1].to_usize() - output_from; - let output_items = output_types[output_start..output_end] - .iter() - .copied() - .zip(output_indexes[output_start..output_end].iter().copied()); - - let input_start = input_starts[block_offset].to_usize() - input_from + 1; - let input_end = input_starts[block_offset + 1].to_usize() - input_from; - let input_items = input_types[input_start..input_end] - .iter() - .copied() - .zip(input_indexes[input_start..input_end].iter().copied()); - - for (output_type, type_index) in output_items.chain(input_items) { - if !output_type.is_addr() { - continue; - } - counts.events += 1; - let key = (output_type, type_index); - block_addresses.insert(key); - - let first = first_indexes - .iter() - .find_map(|(kind, first)| (*kind == output_type).then_some(*first)) - .unwrap(); - if type_index >= first { - counts.new += 1; - } else if cache.contains(&key) { - counts.cached += 1; - } else { - counts.disk_reads += 1; - block_misses.insert(key); - } - } - - counts.unique_disk_reads += block_misses.len() as u64; - cache.extend(block_addresses); - } - - println!("elapsed={:?}", start.elapsed()); - println!("events={}", counts.events); - println!("new={}", counts.new); - println!("cached={}", counts.cached); - println!("disk_reads={}", counts.disk_reads); - println!("unique_disk_reads={}", counts.unique_disk_reads); - println!( - "redundant_disk_reads={} ({:.2}%)", - counts.disk_reads - counts.unique_disk_reads, - 100.0 * (counts.disk_reads - counts.unique_disk_reads) as f64 / counts.disk_reads as f64 - ); -} diff --git a/crates/brk_computer/src/distribution/addr/type_map/index_map.rs b/crates/brk_computer/src/distribution/addr/type_map/index_map.rs index 91c901688..ae823a610 100644 --- a/crates/brk_computer/src/distribution/addr/type_map/index_map.rs +++ b/crates/brk_computer/src/distribution/addr/type_map/index_map.rs @@ -40,25 +40,6 @@ impl AddrTypeToTypeIndexMap { }) } - fn merge_single(own: &mut FxHashMap, other: &mut FxHashMap) { - if own.len() < other.len() { - mem::swap(own, other); - } - own.extend(other.drain()); - } - - /// Merge another map into self, consuming other. - pub(crate) fn merge_mut(&mut self, mut other: Self) { - Self::merge_single(&mut self.p2a, &mut other.p2a); - Self::merge_single(&mut self.p2pk33, &mut other.p2pk33); - Self::merge_single(&mut self.p2pk65, &mut other.p2pk65); - Self::merge_single(&mut self.p2pkh, &mut other.p2pkh); - Self::merge_single(&mut self.p2sh, &mut other.p2sh); - Self::merge_single(&mut self.p2tr, &mut other.p2tr); - Self::merge_single(&mut self.p2wpkh, &mut other.p2wpkh); - Self::merge_single(&mut self.p2wsh, &mut other.p2wsh); - } - /// Insert a value for a specific address type and type_index. pub(crate) fn insert_for_type( &mut self, diff --git a/crates/brk_computer/src/distribution/block/cache/addr.rs b/crates/brk_computer/src/distribution/block/cache/addr.rs index 605aa767c..de362f424 100644 --- a/crates/brk_computer/src/distribution/block/cache/addr.rs +++ b/crates/brk_computer/src/distribution/block/cache/addr.rs @@ -12,12 +12,49 @@ use crate::distribution::{ use super::super::cohort::{WithAddrDataSource, update_tx_counts}; use super::lookup::AddrLookup; +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +struct BlockAddress(u64); + +impl BlockAddress { + const TYPE_SHIFT: u32 = u32::BITS; + + #[inline(always)] + fn new(addr_type: OutputType, type_index: TypeIndex) -> Self { + debug_assert!(addr_type.is_addr()); + + Self((u64::from(addr_type as u8) << Self::TYPE_SHIFT) | u64::from(u32::from(type_index))) + } + + #[inline(always)] + fn addr_type(self) -> OutputType { + match (self.0 >> Self::TYPE_SHIFT) as u8 { + value if value == OutputType::P2PK65 as u8 => OutputType::P2PK65, + value if value == OutputType::P2PK33 as u8 => OutputType::P2PK33, + value if value == OutputType::P2PKH as u8 => OutputType::P2PKH, + value if value == OutputType::P2SH as u8 => OutputType::P2SH, + value if value == OutputType::P2WPKH as u8 => OutputType::P2WPKH, + value if value == OutputType::P2WSH as u8 => OutputType::P2WSH, + value if value == OutputType::P2TR as u8 => OutputType::P2TR, + value if value == OutputType::P2A as u8 => OutputType::P2A, + _ => unreachable!("BlockAddress only stores address output types"), + } + } + + #[inline(always)] + fn type_index(self) -> TypeIndex { + TypeIndex::from(self.0 as u32) + } +} + /// Cache for address data within a flush interval. pub struct AddrCache { /// Addrs with non-zero balance funded: AddrTypeToTypeIndexMap>, /// Addrs that became empty (zero balance) empty: AddrTypeToTypeIndexMap>, + /// Reusable scratch space for the unique addresses touched by one block. + block_addresses: Vec, } impl Default for AddrCache { @@ -31,6 +68,7 @@ impl AddrCache { Self { funded: AddrTypeToTypeIndexMap::default(), empty: AddrTypeToTypeIndexMap::default(), + block_addresses: Vec::new(), } } @@ -46,13 +84,50 @@ impl AddrCache { .is_some_and(|m| m.contains_key(&type_index)) } - /// Merge address data into funded cache. - #[inline] - pub(crate) fn merge_funded( + /// Load each address touched by the block once. + pub(crate) fn load_block_addresses( &mut self, - data: AddrTypeToTypeIndexMap>, + addresses: impl Iterator, + first_addr_indexes: &ByAddrType, + vr: &VecsReaders, + any_addr_indexes: &AnyAddrIndexesVecs, + addrs_data: &AddrsDataVecs, ) { - self.funded.merge_mut(data); + self.block_addresses.clear(); + for (addr_type, type_index) in addresses { + if addr_type.is_addr() && !self.contains(addr_type, type_index) { + self.block_addresses + .push(BlockAddress::new(addr_type, type_index)); + } + } + self.block_addresses.sort_unstable(); + self.block_addresses.dedup(); + + for index in 0..self.block_addresses.len() { + let address = self.block_addresses[index]; + let addr_type = address.addr_type(); + let type_index = address.type_index(); + let first = *first_addr_indexes.get(addr_type).unwrap(); + + let source = if first <= type_index { + WithAddrDataSource::New(FundedAddrData::default()) + } else { + let any_addr_index = vr.any_addr_index(any_addr_indexes, addr_type, type_index); + + match any_addr_index.to_enum() { + AnyAddrDataIndexEnum::Funded(funded_index) => { + let funded_data = vr.funded_data(addrs_data, funded_index); + WithAddrDataSource::FromFunded(funded_index, funded_data) + } + AnyAddrDataIndexEnum::Empty(empty_index) => { + let empty_data = vr.empty_data(addrs_data, empty_index); + WithAddrDataSource::FromEmpty(empty_index, empty_data.into()) + } + } + }; + + self.funded.insert_for_type(addr_type, type_index, source); + } } /// Create an AddrLookup view into this cache. @@ -86,41 +161,19 @@ impl AddrCache { } } -/// Load address data from storage or create new. -/// -/// Returns None if address is already in cache (funded or empty). -#[allow(clippy::too_many_arguments)] -pub(crate) fn load_uncached_addr_data( - addr_type: OutputType, - type_index: TypeIndex, - first_addr_indexes: &ByAddrType, - cache: &AddrCache, - vr: &VecsReaders, - any_addr_indexes: &AnyAddrIndexesVecs, - addrs_data: &AddrsDataVecs, -) -> Option> { - // Check if this is a new address (type_index >= first for this height) - let first = *first_addr_indexes.get(addr_type).unwrap(); - if first <= type_index { - return Some(WithAddrDataSource::New(FundedAddrData::default())); - } +#[cfg(test)] +mod tests { + use super::*; - // Skip if already in cache - if cache.contains(addr_type, type_index) { - return None; - } + #[test] + fn block_address_round_trips_every_address_type() { + for addr_type in OutputType::ADDR_TYPES { + for type_index in [TypeIndex::from(0_u32), TypeIndex::from(u32::MAX)] { + let address = BlockAddress::new(addr_type, type_index); - // Read from storage - let any_addr_index = vr.any_addr_index(any_addr_indexes, addr_type, type_index); - - Some(match any_addr_index.to_enum() { - AnyAddrDataIndexEnum::Funded(funded_index) => { - let funded_data = vr.funded_data(addrs_data, funded_index); - WithAddrDataSource::FromFunded(funded_index, funded_data) + assert_eq!(address.addr_type(), addr_type); + assert_eq!(address.type_index(), type_index); + } } - AnyAddrDataIndexEnum::Empty(empty_index) => { - let empty_data = vr.empty_data(addrs_data, empty_index); - WithAddrDataSource::FromEmpty(empty_index, empty_data.into()) - } - }) + } } diff --git a/crates/brk_computer/src/distribution/block/utxo/inputs.rs b/crates/brk_computer/src/distribution/block/utxo/inputs.rs index 2f0243dc9..dde1a566c 100644 --- a/crates/brk_computer/src/distribution/block/utxo/inputs.rs +++ b/crates/brk_computer/src/distribution/block/utxo/inputs.rs @@ -1,30 +1,18 @@ -use brk_cohort::ByAddrType; -use brk_types::{FundedAddrData, Height, OutputType, Sats, TxIndex, TypeIndex}; -use rayon::prelude::*; +use brk_types::{Height, OutputType, Sats, TxIndex, TypeIndex}; use rustc_hash::FxHashMap; use smallvec::SmallVec; use crate::distribution::{ - addr::{AddrTypeToTypeIndexMap, AddrsDataVecs, AnyAddrIndexesVecs}, - compute::VecsReaders, + addr::{AddrTypeToTypeIndexMap, HeightToAddrTypeToVec}, state::Transacted, }; -use crate::distribution::addr::HeightToAddrTypeToVec; - -use super::super::{ - cache::{AddrCache, load_uncached_addr_data}, - cohort::WithAddrDataSource, -}; - /// Result of processing inputs for a block. pub struct InputsResult { /// Map from UTXO creation height -> aggregated sent supply. pub height_to_sent: FxHashMap, /// Per-height, per-address-type sent data: (type_index, value) for each address. pub sent_data: HeightToAddrTypeToVec<(TypeIndex, Sats)>, - /// Address data looked up during processing, keyed by (addr_type, type_index). - pub addr_data: AddrTypeToTypeIndexMap>, /// Transaction indexes per address for tx_count tracking. pub tx_index_vecs: AddrTypeToTypeIndexMap>, } @@ -36,65 +24,21 @@ pub struct InputsResult { /// 2. Resolve outpoint to txout_index /// 3. Get the creation height from txout_index_to_height map /// 4. Read value and type from the referenced output (random access via mmap) -/// 5. Look up address data if input references an address type -/// 6. Accumulate into height_to_sent map -/// 7. Track address-specific data for address cohort processing -/// -/// Uses parallel reads followed by sequential accumulation to avoid -/// expensive merge overhead from rayon's fold/reduce pattern. -#[allow(clippy::too_many_arguments)] +/// 5. Accumulate into height_to_sent map +/// 6. Track address-specific data for address cohort processing pub(crate) fn process_inputs( - input_count: usize, txin_index_to_tx_index: &[TxIndex], txin_index_to_value: &[Sats], txin_index_to_output_type: &[OutputType], txin_index_to_type_index: &[TypeIndex], txin_index_to_prev_height: &[Height], - first_addr_indexes: &ByAddrType, - cache: &AddrCache, - vr: &VecsReaders, - any_addr_indexes: &AnyAddrIndexesVecs, - addrs_data: &AddrsDataVecs, ) -> InputsResult { - let map_fn = |local_idx: usize| { - let tx_index = txin_index_to_tx_index[local_idx]; + let input_count = txin_index_to_value.len(); + debug_assert_eq!(txin_index_to_tx_index.len(), input_count); + debug_assert_eq!(txin_index_to_output_type.len(), input_count); + debug_assert_eq!(txin_index_to_type_index.len(), input_count); + debug_assert_eq!(txin_index_to_prev_height.len(), input_count); - let prev_height = txin_index_to_prev_height[local_idx]; - let value = txin_index_to_value[local_idx]; - let input_type = txin_index_to_output_type[local_idx]; - - if input_type.is_not_addr() { - return (prev_height, value, input_type, None); - } - - let type_index = txin_index_to_type_index[local_idx]; - - // Look up address data - let addr_data_opt = load_uncached_addr_data( - input_type, - type_index, - first_addr_indexes, - cache, - vr, - any_addr_indexes, - addrs_data, - ); - - ( - prev_height, - value, - input_type, - Some((type_index, tx_index, value, addr_data_opt)), - ) - }; - - let items: Vec<_> = if input_count < 128 { - (0..input_count).map(map_fn).collect() - } else { - (0..input_count).into_par_iter().map(map_fn).collect() - }; - - // Phase 2: Sequential accumulation - no merge overhead // Estimate: unique heights bounded by block depth, addresses spread across ~8 types let estimated_unique_heights = (input_count / 4).max(16); let estimated_per_type = (input_count / 8).max(8); @@ -103,43 +47,41 @@ pub(crate) fn process_inputs( Default::default(), ); let mut sent_data = HeightToAddrTypeToVec::with_capacity(estimated_unique_heights); - let mut addr_data = AddrTypeToTypeIndexMap::>::with_capacity( - estimated_per_type, - ); let mut tx_index_vecs = AddrTypeToTypeIndexMap::>::with_capacity(estimated_per_type); - for (prev_height, value, output_type, addr_info) in items { + for local_idx in 0..input_count { + let prev_height = txin_index_to_prev_height[local_idx]; + let value = txin_index_to_value[local_idx]; + let output_type = txin_index_to_output_type[local_idx]; + height_to_sent .entry(prev_height) .or_default() .iterate(value, output_type); - if let Some((type_index, tx_index, value, addr_data_opt)) = addr_info { - sent_data - .entry(prev_height) - .or_default() - .get_mut(output_type) - .unwrap() - .push((type_index, value)); - - if let Some(source) = addr_data_opt { - addr_data.insert_for_type(output_type, type_index, source); - } - - tx_index_vecs - .get_mut(output_type) - .unwrap() - .entry(type_index) - .or_default() - .push(tx_index); + if output_type.is_not_addr() { + continue; } + + let type_index = txin_index_to_type_index[local_idx]; + sent_data + .entry(prev_height) + .or_default() + .get_mut(output_type) + .unwrap() + .push((type_index, value)); + tx_index_vecs + .get_mut(output_type) + .unwrap() + .entry(type_index) + .or_default() + .push(txin_index_to_tx_index[local_idx]); } InputsResult { height_to_sent, sent_data, - addr_data, tx_index_vecs, } } diff --git a/crates/brk_computer/src/distribution/block/utxo/outputs.rs b/crates/brk_computer/src/distribution/block/utxo/outputs.rs index 600d638ec..71dc05ea0 100644 --- a/crates/brk_computer/src/distribution/block/utxo/outputs.rs +++ b/crates/brk_computer/src/distribution/block/utxo/outputs.rs @@ -1,27 +1,18 @@ -use brk_cohort::ByAddrType; -use brk_types::{FundedAddrData, Sats, TxIndex, TypeIndex}; -use rayon::prelude::*; +use brk_types::{Sats, TxIndex, TypeIndex}; use smallvec::SmallVec; use crate::distribution::{ - addr::{AddrTypeToTypeIndexMap, AddrTypeToVec, AddrsDataVecs, AnyAddrIndexesVecs}, - compute::{TxOutData, VecsReaders}, + addr::{AddrTypeToTypeIndexMap, AddrTypeToVec}, + compute::TxOutData, state::Transacted, }; -use super::super::{ - cache::{AddrCache, load_uncached_addr_data}, - cohort::WithAddrDataSource, -}; - /// Result of processing outputs for a block. pub struct OutputsResult { /// Aggregated supply transacted in this block. pub transacted: Transacted, /// Per-address-type received data: (type_index, value) for each address. pub received_data: AddrTypeToVec<(TypeIndex, Sats)>, - /// Address data looked up during processing, keyed by (addr_type, type_index). - pub addr_data: AddrTypeToTypeIndexMap>, /// Transaction indexes per address for tx_count tracking. pub tx_index_vecs: AddrTypeToTypeIndexMap>, } @@ -31,92 +22,45 @@ pub struct OutputsResult { /// For each output: /// 1. Read pre-collected value, output type, and type_index /// 2. Accumulate into Transacted by type and amount -/// 3. Look up address data if output is an address type -/// 4. Track address-specific data for address cohort processing -#[allow(clippy::too_many_arguments)] +/// 3. Track address-specific data for address cohort processing pub(crate) fn process_outputs( txout_index_to_tx_index: &[TxIndex], txout_data_vec: &[TxOutData], - first_addr_indexes: &ByAddrType, - cache: &AddrCache, - vr: &VecsReaders, - any_addr_indexes: &AnyAddrIndexesVecs, - addrs_data: &AddrsDataVecs, ) -> OutputsResult { let output_count = txout_data_vec.len(); + debug_assert_eq!(txout_index_to_tx_index.len(), output_count); - // Phase 1: Addr lookups (mmap reads) — parallel for large blocks, sequential for small - let map_fn = |local_idx: usize| { - let txout_data = &txout_data_vec[local_idx]; - let value = txout_data.value; - let output_type = txout_data.output_type; - - if output_type.is_not_addr() { - return (value, output_type, None); - } - - let type_index = txout_data.type_index; - let tx_index = txout_index_to_tx_index[local_idx]; - - let addr_data_opt = load_uncached_addr_data( - output_type, - type_index, - first_addr_indexes, - cache, - vr, - any_addr_indexes, - addrs_data, - ); - - ( - value, - output_type, - Some((type_index, tx_index, value, addr_data_opt)), - ) - }; - - let items: Vec<_> = if output_count < 128 { - (0..output_count).map(map_fn).collect() - } else { - (0..output_count).into_par_iter().map(map_fn).collect() - }; - - // Phase 2: Sequential accumulation let estimated_per_type = (output_count / 8).max(8); let mut transacted = Transacted::default(); let mut received_data = AddrTypeToVec::with_capacity(estimated_per_type); - let mut addr_data = AddrTypeToTypeIndexMap::>::with_capacity( - estimated_per_type, - ); let mut tx_index_vecs = AddrTypeToTypeIndexMap::>::with_capacity(estimated_per_type); - for (value, output_type, addr_info) in items { + for (local_idx, txout_data) in txout_data_vec.iter().enumerate() { + let value = txout_data.value; + let output_type = txout_data.output_type; transacted.iterate(value, output_type); - if let Some((type_index, tx_index, value, addr_data_opt)) = addr_info { - received_data - .get_mut(output_type) - .unwrap() - .push((type_index, value)); - - if let Some(source) = addr_data_opt { - addr_data.insert_for_type(output_type, type_index, source); - } - - tx_index_vecs - .get_mut(output_type) - .unwrap() - .entry(type_index) - .or_default() - .push(tx_index); + if output_type.is_not_addr() { + continue; } + + let type_index = txout_data.type_index; + received_data + .get_mut(output_type) + .unwrap() + .push((type_index, value)); + tx_index_vecs + .get_mut(output_type) + .unwrap() + .entry(type_index) + .or_default() + .push(txout_index_to_tx_index[local_idx]); } OutputsResult { transacted, received_data, - addr_data, tx_index_vecs, } } diff --git a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs index 7aca75875..b9f9c5ada 100644 --- a/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs +++ b/crates/brk_computer/src/distribution/cohorts/utxo/percentiles.rs @@ -39,6 +39,28 @@ impl UTXOCohorts { Ok(()) } + /// Iterate over the current in-memory age-cohort URPD entries. + /// + /// Prices use the same rounding as the persisted daily distributions, so + /// consumers can avoid writing and immediately rereading the current day. + pub(crate) fn age_range_urpd_entries( + &self, + ) -> impl Iterator + '_ { + let sth_filter = &self.sth.metrics.filter; + self.age_range + .iter() + .enumerate() + .flat_map(move |(age, cohort)| { + let is_sth = sth_filter.includes(cohort.filter()); + cohort.state.iter().flat_map(move |state| { + state + .cost_basis_map() + .iter() + .map(move |(&price, &sats)| (age, is_sth, rounded_urpd_price(price), sats)) + }) + }) + } + /// Push all Fenwick-derived per-block results: percentiles, density, profitability. fn push_fenwick_results(&mut self, spot_price: Cents) { let (all_d, sth_d, lth_d) = self.caches.fenwick.density(spot_price); @@ -71,7 +93,7 @@ impl UTXOCohorts { }; let mut merged: Vec<(CentsCompact, Sats)> = Vec::new(); for (&price, &sats) in state.cost_basis_map().iter() { - let rounded = price.round_to_dollar(COST_BASIS_PRICE_DIGITS); + let rounded = rounded_urpd_price(price); if let Some(last) = merged.last_mut() && last.0 == rounded { @@ -125,6 +147,11 @@ impl UTXOCohorts { } } +#[inline] +fn rounded_urpd_price(price: CentsCompact) -> CentsCompact { + price.round_to_dollar(COST_BASIS_PRICE_DIGITS) +} + /// Push percentiles + density to cost basis vecs. #[inline(always)] fn push_cost_basis( @@ -198,9 +225,9 @@ impl MergeTarget { self.price_sats += amount; } - fn finalize_price(&mut self, price: Cents) { + fn finalize_price(&mut self, price: CentsCompact) { if self.price_sats > 0 { - let rounded: CentsCompact = price.round_to_dollar(COST_BASIS_PRICE_DIGITS).into(); + let rounded = rounded_urpd_price(price); if let Some((lp, ls)) = self.merged.last_mut() && *lp == rounded { @@ -242,7 +269,7 @@ fn merge_k_way( if let Some(prev) = current_price && prev != price { - targets.for_each_mut(|t| t.finalize_price(prev.into())); + targets.for_each_mut(|t| t.finalize_price(prev)); } current_price = Some(price); @@ -255,6 +282,6 @@ fn merge_k_way( } if let Some(price) = current_price { - targets.for_each_mut(|t| t.finalize_price(price.into())); + targets.for_each_mut(|t| t.finalize_price(price)); } } diff --git a/crates/brk_computer/src/distribution/compute/block_loop.rs b/crates/brk_computer/src/distribution/compute/block_loop.rs index d8c88d173..1e29f020f 100644 --- a/crates/brk_computer/src/distribution/compute/block_loop.rs +++ b/crates/brk_computer/src/distribution/compute/block_loop.rs @@ -12,8 +12,8 @@ use crate::{ distribution::{ addr::AddrMetricsState, block::{ - AddrCache, InputsResult, TransferAddressCache, process_inputs, process_outputs, - process_received, process_sent, + AddrCache, TransferAddressCache, process_inputs, process_outputs, process_received, + process_sent, }, compute::write::{process_addr_updates, write}, state::{BlockState, Transacted}, @@ -259,17 +259,23 @@ pub(crate) fn process_blocks( state.reset_per_block(); - // Process outputs, inputs, and tick-tock in parallel via rayon::join. - // Collection (build tx_index mappings + bulk mmap reads) is merged into the - // processing closures so outputs and inputs collection overlap each other - // and tick-tock, instead of running sequentially before the join. + debug_assert!(input_count > 0); + + // Keep tick-tock concurrent with the block reads and address processing. let (matured, (outputs_result, inputs_result)) = rayon::join( || { vecs.utxo_cohorts .tick_tock_next_block(chain_state, timestamp) }, || { - rayon::join( + // Collect both sides concurrently, then load their shared addresses once. + let ( + (txout_index_to_tx_index, txout_data_vec), + ( + txin_index_to_tx_index, + (input_values, input_prev_heights, input_output_types, input_type_indexes), + ), + ) = rayon::join( || { let txout_index_to_tx_index = txout_to_tx_index_buf.build( first_tx_index, @@ -278,63 +284,54 @@ pub(crate) fn process_blocks( ); let txout_data_vec = txout_iters.collect_block_outputs(first_txout_index, output_count); - process_outputs( - txout_index_to_tx_index, - txout_data_vec, - &first_addr_indexes, - &cache, - &vr, - &vecs.any_addr_indexes, - &vecs.addrs_data, - ) + (txout_index_to_tx_index, txout_data_vec) }, || { - if input_count > 1 { - let txin_index_to_tx_index = txin_to_tx_index_buf.build( - first_tx_index, - tx_count, - tx_index_to_input_count, - ); - let ( - input_values, - input_prev_heights, - input_output_types, - input_type_indexes, - ) = txin_iters.collect_block_inputs( - first_txin_index + 1, - input_count - 1, - height, - ); - process_inputs( - input_count - 1, - &txin_index_to_tx_index[1..], - input_values, - input_output_types, - input_type_indexes, - input_prev_heights, - &first_addr_indexes, - &cache, - &vr, - &vecs.any_addr_indexes, - &vecs.addrs_data, - ) - } else { - InputsResult { - height_to_sent: Default::default(), - sent_data: Default::default(), - addr_data: Default::default(), - tx_index_vecs: Default::default(), - } - } + let txin_index_to_tx_index = txin_to_tx_index_buf.build( + first_tx_index, + tx_count, + tx_index_to_input_count, + ); + let input_data = txin_iters.collect_block_inputs( + first_txin_index + 1, + input_count - 1, + height, + ); + (txin_index_to_tx_index, input_data) + }, + ); + + cache.load_block_addresses( + txout_data_vec + .iter() + .map(|data| (data.output_type, data.type_index)) + .chain( + input_output_types + .iter() + .copied() + .zip(input_type_indexes.iter().copied()), + ), + &first_addr_indexes, + &vr, + &vecs.any_addr_indexes, + &vecs.addrs_data, + ); + + rayon::join( + || process_outputs(txout_index_to_tx_index, txout_data_vec), + || { + process_inputs( + &txin_index_to_tx_index[1..], + input_values, + input_output_types, + input_type_indexes, + input_prev_heights, + ) }, ) }, ); - // Merge new address data into current cache - cache.merge_funded(outputs_result.addr_data); - cache.merge_funded(inputs_result.addr_data); - // Combine tx_index_vecs from outputs and inputs, then update tx_count let combined_tx_index_vecs = outputs_result .tx_index_vecs diff --git a/crates/brk_computer/src/internal/algo/mod.rs b/crates/brk_computer/src/internal/algo/mod.rs index 12b1cdace..634669bd3 100644 --- a/crates/brk_computer/src/internal/algo/mod.rs +++ b/crates/brk_computer/src/internal/algo/mod.rs @@ -1,10 +1,12 @@ mod drawdown; mod fenwick; +mod order_statistics; mod sliding_distribution; mod sliding_median; mod sliding_window; pub(crate) use drawdown::*; pub(crate) use fenwick::*; +pub(crate) use order_statistics::*; pub(crate) use sliding_distribution::*; pub(crate) use sliding_median::*; diff --git a/crates/brk_computer/src/internal/algo/order_statistics.rs b/crates/brk_computer/src/internal/algo/order_statistics.rs new file mode 100644 index 000000000..e4b967797 --- /dev/null +++ b/crates/brk_computer/src/internal/algo/order_statistics.rs @@ -0,0 +1,223 @@ +/// Exact order-statistics multiset backed by sqrt-decomposed sorted blocks. +/// +/// Insert, remove, and rank lookup are O(sqrt(n)). Bulk construction sorts +/// once, then partitions the values into sorted blocks. +#[derive(Clone)] +pub(crate) struct ExactOrderStats { + blocks: Vec>, + len: usize, + block_size: usize, +} + +impl ExactOrderStats { + pub(crate) fn new(capacity: usize) -> Self { + let block_size = ((capacity as f64).sqrt() as usize).max(64); + Self { + blocks: Vec::new(), + len: 0, + block_size, + } + } + + pub(crate) fn from_unsorted(mut values: Vec) -> Self { + values.sort_unstable_by(f64::total_cmp); + Self::from_sorted(values) + } + + pub(crate) fn from_sorted(values: Vec) -> Self { + debug_assert!( + values + .windows(2) + .all(|pair| !pair[0].total_cmp(&pair[1]).is_gt()), + "order-statistics input must be sorted" + ); + let mut stats = Self::new(values.len()); + stats.len = values.len(); + let mut values = values.into_iter(); + loop { + let block: Vec<_> = values.by_ref().take(stats.block_size).collect(); + if block.is_empty() { + break; + } + stats.blocks.push(block); + } + stats + } + + pub(crate) fn len(&self) -> usize { + self.len + } + + pub(crate) fn is_empty(&self) -> bool { + self.len == 0 + } + + pub(crate) fn insert(&mut self, value: f64) { + self.len += 1; + + if self.blocks.is_empty() { + self.blocks.push(vec![value]); + return; + } + + let block_index = self + .blocks + .partition_point(|block| { + block + .last() + .is_some_and(|candidate| candidate.total_cmp(&value).is_lt()) + }) + .min(self.blocks.len() - 1); + let block = &mut self.blocks[block_index]; + let position = block.partition_point(|candidate| candidate.total_cmp(&value).is_lt()); + block.insert(position, value); + + if block.len() > 2 * self.block_size { + let right = block.split_off(block.len() / 2); + self.blocks.insert(block_index + 1, right); + } + } + + pub(crate) fn remove(&mut self, value: f64) -> bool { + let block_index = self.blocks.partition_point(|block| { + block + .last() + .is_some_and(|candidate| candidate.total_cmp(&value).is_lt()) + }); + let Some(block) = self.blocks.get_mut(block_index) else { + return false; + }; + + let position = block.partition_point(|candidate| candidate.total_cmp(&value).is_lt()); + if block + .get(position) + .is_none_or(|candidate| !candidate.total_cmp(&value).is_eq()) + { + return false; + } + + block.remove(position); + self.len -= 1; + if block.is_empty() { + self.blocks.remove(block_index); + } + true + } + + pub(crate) fn kth(&self, mut index: usize) -> f64 { + for block in &self.blocks { + if index < block.len() { + return block[index]; + } + index -= block.len(); + } + unreachable!("order-statistics index out of bounds") + } + + /// Resolve sorted, unique ranks in one pass through the underlying blocks. + pub(crate) fn values_at(&self, ranks: &[usize], out: &mut [f64]) { + debug_assert_eq!(ranks.len(), out.len()); + debug_assert!(ranks.windows(2).all(|pair| pair[0] < pair[1])); + debug_assert!(ranks.last().is_none_or(|&rank| rank < self.len)); + let mut rank_index = 0; + let mut block_start = 0; + for block in &self.blocks { + let block_end = block_start + block.len(); + while rank_index < ranks.len() && ranks[rank_index] < block_end { + out[rank_index] = block[ranks[rank_index] - block_start]; + rank_index += 1; + } + block_start += block.len(); + if rank_index == ranks.len() { + return; + } + } + debug_assert_eq!(rank_index, ranks.len()); + } + + pub(crate) fn count_lt(&self, value: f64) -> usize { + let mut count = 0; + for block in &self.blocks { + if block + .last() + .is_some_and(|candidate| candidate.total_cmp(&value).is_lt()) + { + count += block.len(); + continue; + } + count += block.partition_point(|candidate| candidate.total_cmp(&value).is_lt()); + break; + } + count + } + + pub(crate) fn count_le(&self, value: f64) -> usize { + let mut count = 0; + for block in &self.blocks { + if block + .last() + .is_some_and(|candidate| !candidate.total_cmp(&value).is_gt()) + { + count += block.len(); + continue; + } + count += block.partition_point(|candidate| !candidate.total_cmp(&value).is_gt()); + break; + } + count + } + + pub(crate) fn first(&self) -> f64 { + self.blocks.first().unwrap().first().copied().unwrap() + } + + pub(crate) fn last(&self) -> f64 { + self.blocks.last().unwrap().last().copied().unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bulk_build_and_rank_queries_preserve_duplicates() { + let stats = ExactOrderStats::from_unsorted(vec![3.0, 1.0, 2.0, 2.0]); + + assert_eq!(stats.len(), 4); + assert_eq!(stats.kth(0), 1.0); + assert_eq!(stats.kth(1), 2.0); + assert_eq!(stats.kth(2), 2.0); + assert_eq!(stats.kth(3), 3.0); + assert_eq!(stats.count_lt(2.0), 1); + assert_eq!(stats.count_le(2.0), 3); + let mut values = [0.0; 3]; + stats.values_at(&[0, 2, 3], &mut values); + assert_eq!(values, [1.0, 2.0, 3.0]); + } + + #[test] + fn insert_and_remove_update_order_statistics() { + let mut stats = ExactOrderStats::new(4); + for value in [3.0, 1.0, 2.0, 2.0] { + stats.insert(value); + } + + assert!(stats.remove(2.0)); + assert!(!stats.remove(4.0)); + assert_eq!(stats.len(), 3); + assert_eq!(stats.kth(0), 1.0); + assert_eq!(stats.kth(1), 2.0); + assert_eq!(stats.kth(2), 3.0); + } + + #[test] + fn total_order_distinguishes_signed_zero() { + let stats = ExactOrderStats::from_unsorted(vec![0.0, -0.0]); + + assert_eq!(stats.kth(0).to_bits(), (-0.0_f64).to_bits()); + assert_eq!(stats.kth(1).to_bits(), 0.0_f64.to_bits()); + assert_eq!(stats.count_lt(0.0), 1); + assert_eq!(stats.count_le(-0.0), 1); + } +} diff --git a/crates/brk_computer/src/internal/algo/sliding_window.rs b/crates/brk_computer/src/internal/algo/sliding_window.rs index e654dc751..f6c5cc569 100644 --- a/crates/brk_computer/src/internal/algo/sliding_window.rs +++ b/crates/brk_computer/src/internal/algo/sliding_window.rs @@ -1,133 +1,18 @@ -use std::cmp::Ordering; - -/// Sqrt-decomposed sorted structure for O(sqrt(n)) insert/remove/kth. -/// -/// Maintains `blocks` sorted sub-arrays where each block is sorted and -/// the blocks are ordered (max of block[i] <= min of block[i+1]). -/// Total element count is tracked via `total_len`. -struct SortedBlocks { - blocks: Vec>, - total_len: usize, - block_size: usize, -} - -impl SortedBlocks { - fn new(capacity: usize) -> Self { - let block_size = ((capacity as f64).sqrt() as usize).max(64); - Self { - blocks: Vec::new(), - total_len: 0, - block_size, - } - } - - /// Build from a pre-sorted slice in O(n) by chunking directly into blocks. - fn from_sorted(sorted: &[f64], block_size: usize) -> Self { - let total_len = sorted.len(); - let blocks: Vec> = sorted.chunks(block_size).map(|c| c.to_vec()).collect(); - Self { - blocks, - total_len, - block_size, - } - } - - fn len(&self) -> usize { - self.total_len - } - - fn is_empty(&self) -> bool { - self.total_len == 0 - } - - /// Insert a value in sorted order. O(sqrt(n)). - fn insert(&mut self, value: f64) { - self.total_len += 1; - - if self.blocks.is_empty() { - self.blocks.push(vec![value]); - return; - } - - // Find the block where value belongs: first block whose max >= value - let block_idx = self - .blocks - .partition_point(|b| *b.last().unwrap() < value) - .min(self.blocks.len() - 1); - - let block = &mut self.blocks[block_idx]; - let pos = block.partition_point(|a| *a < value); - block.insert(pos, value); - - // Split if block too large - if block.len() > 2 * self.block_size { - let mid = block.len() / 2; - let right = block[mid..].to_vec(); - block.truncate(mid); - self.blocks.insert(block_idx + 1, right); - } - } - - /// Remove one occurrence of value. O(sqrt(n)). - fn remove(&mut self, value: f64) -> bool { - if self.blocks.is_empty() { - return false; - } - - // Binary search for first block whose max >= value - let bi = self - .blocks - .partition_point(|b| b.last().is_some_and(|&last| last < value)); - if bi >= self.blocks.len() { - return false; - } - - let block = &mut self.blocks[bi]; - let pos = block.partition_point(|a| *a < value); - if pos < block.len() && block[pos] == value { - block.remove(pos); - self.total_len -= 1; - if block.is_empty() { - self.blocks.remove(bi); - } - return true; - } - false - } - - /// Get the k-th smallest element (0-indexed). O(sqrt(n)). - fn kth(&self, mut k: usize) -> f64 { - for block in &self.blocks { - if k < block.len() { - return block[k]; - } - k -= block.len(); - } - unreachable!("kth out of bounds") - } - - fn first(&self) -> f64 { - self.blocks.first().unwrap().first().copied().unwrap() - } - - fn last(&self) -> f64 { - self.blocks.last().unwrap().last().copied().unwrap() - } -} +use super::order_statistics::ExactOrderStats; /// Sorted sliding window for rolling distribution/median computations. /// /// Uses sqrt-decomposition for O(sqrt(n)) insert/remove/kth instead of /// O(n) memmoves with a flat sorted Vec. pub(crate) struct SlidingWindowSorted { - sorted: SortedBlocks, + sorted: ExactOrderStats, prev_start: usize, } impl SlidingWindowSorted { pub fn with_capacity(cap: usize) -> Self { Self { - sorted: SortedBlocks::new(cap), + sorted: ExactOrderStats::new(cap), prev_start: 0, } } @@ -140,9 +25,7 @@ impl SlidingWindowSorted { if slice.is_empty() { return; } - let mut sorted_copy: Vec = slice.to_vec(); - sorted_copy.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); - self.sorted = SortedBlocks::from_sorted(&sorted_copy, self.sorted.block_size); + self.sorted = ExactOrderStats::from_unsorted(slice.to_vec()); } /// Add a new value and remove all expired values up to `new_start`. @@ -251,18 +134,7 @@ impl SlidingWindowSorted { // Single pass through blocks to get all values let ranks = &rank_set[..rank_count]; let mut values = [0.0f64; 10]; - let mut ri = 0; - let mut cumulative = 0; - for block in &self.sorted.blocks { - while ri < rank_count && ranks[ri] - cumulative < block.len() { - values[ri] = block[ranks[ri] - cumulative]; - ri += 1; - } - cumulative += block.len(); - if ri >= rank_count { - break; - } - } + self.sorted.values_at(ranks, &mut values[..rank_count]); // Interpolate results let mut out = [0.0; N]; diff --git a/crates/brk_computer/src/models/bedrock/compute.rs b/crates/brk_computer/src/models/bedrock/compute.rs index d6abe4417..334299661 100644 --- a/crates/brk_computer/src/models/bedrock/compute.rs +++ b/crates/brk_computer/src/models/bedrock/compute.rs @@ -336,7 +336,6 @@ impl Vecs { let needs_rebuild = !weighted_urpd_is_current || day_index >= recompute_from; if let Some(date) = indexes.day1.date.collect_one(day) && (needs_rebuild || needs_evaluation) - && UrpdRaw::path(&distribution.states_path, UTXO_ALL_NAME.id, date).try_exists()? { let weights = mode_weights( day, @@ -346,17 +345,31 @@ impl Vecs { &coinflow_spending_rate, &bounds, ); - let urpds = build_day_urpds(&distribution.states_path, date, &weights)?; - if needs_rebuild { - write_weighted_day_urpds( - &self.states_path, - &weighted_urpd_names, - date, - &urpds, - )?; - } - if needs_evaluation { - evaluate_day(&urpds, &thresholds, &mut result); + let urpds = if day_index + 1 == source_end { + Some(build_current_day_urpds( + &distribution.utxo_cohorts, + &weights, + )) + } else if UrpdRaw::path(&distribution.states_path, UTXO_ALL_NAME.id, date) + .try_exists()? + { + Some(read_day_urpds(&distribution.states_path, date, &weights)?) + } else { + None + }; + + if let Some(urpds) = urpds { + if needs_rebuild { + write_weighted_day_urpds( + &self.states_path, + &weighted_urpd_names, + date, + &urpds, + )?; + } + if needs_evaluation { + evaluate_day(&urpds, &thresholds, &mut result); + } } } calibration.observe(loss_shares); @@ -553,7 +566,7 @@ fn remove_urpd_dir(states_path: &Path, name: &str) -> Result<()> { } } -fn build_day_urpds( +fn read_day_urpds( distribution_states_path: &Path, date: Date, weights: &ModeWeights, @@ -575,23 +588,54 @@ fn build_day_urpds( }; for (price, sats) in source.map { - let mass = u64::from(sats) as f64; - let bucket = weighted.entry(price).or_default(); - for (mode, (all, weights)) in bucket.all.iter_mut().zip(&weights[1..]).enumerate() { - if let Some(weights) = weights { - let weighted_mass = mass * weights[age]; - *all += weighted_mass; - if mode < STORED_WEIGHT_COUNT { - bucket.terms[term][mode] += weighted_mass; - } - } - } + add_weighted_entry(&mut weighted, price, sats, age, term, weights); } } Ok(finalize_day_urpds(raw, weighted)) } +fn build_current_day_urpds(utxos: &distribution::UTXOCohorts, weights: &ModeWeights) -> DayUrpds { + build_urpds_from_age_entries(utxos.age_range_urpd_entries(), weights) +} + +fn build_urpds_from_age_entries( + entries: impl IntoIterator, + weights: &ModeWeights, +) -> DayUrpds { + let mut raw = UrpdRaw::default(); + let mut weighted = WeightedUrpd::new(); + + for (age, is_sth, price, sats) in entries { + *raw.map.entry(price).or_default() += sats; + let term = if is_sth { STH_TERM } else { LTH_TERM }; + add_weighted_entry(&mut weighted, price, sats, age, term, weights); + } + + finalize_day_urpds(raw, weighted) +} + +fn add_weighted_entry( + weighted: &mut WeightedUrpd, + price: CentsCompact, + sats: Sats, + age: usize, + term: usize, + weights: &ModeWeights, +) { + let mass = u64::from(sats) as f64; + let bucket = weighted.entry(price).or_default(); + for (mode, (all, weights)) in bucket.all.iter_mut().zip(&weights[1..]).enumerate() { + if let Some(weights) = weights { + let weighted_mass = mass * weights[age]; + *all += weighted_mass; + if mode < STORED_WEIGHT_COUNT { + bucket.terms[term][mode] += weighted_mass; + } + } + } +} + fn finalize_day_urpds(raw: UrpdRaw, weighted: WeightedUrpd) -> DayUrpds { let mut all: AllWeightedUrpds = std::array::from_fn(|_| UrpdRaw::default()); let mut terms: TermWeightedUrpds = @@ -886,6 +930,30 @@ mod tests { ); } + #[test] + fn current_day_entries_build_raw_and_weighted_urpds() { + let weights: ModeWeights = std::array::from_fn(|_| Some([0.5; AGE_COHORT_COUNT])); + let price = CentsCompact::new(100); + let urpds = build_urpds_from_age_entries( + [ + (0, true, price, Sats::from(3_u64)), + (1, false, price, Sats::from(5_u64)), + ], + &weights, + ); + + assert_eq!(urpds.raw.map[&price], Sats::from(8_u64)); + assert_eq!(urpds.all[COINTIME_MODE - 1].map[&price], Sats::from(4_u64)); + assert_eq!( + urpds.terms[STH_TERM][COINTIME_MODE - 1].map[&price], + Sats::from(1_u64) + ); + assert_eq!( + urpds.terms[LTH_TERM][COINTIME_MODE - 1].map[&price], + Sats::from(2_u64) + ); + } + #[test] fn stores_only_cointime_and_coinflow_for_all_sth_and_lth() { let root = diff --git a/crates/brk_computer/src/models/rarity_meter/extremes.rs b/crates/brk_computer/src/models/rarity_meter/extremes.rs index a53b96d80..3137cec6d 100644 --- a/crates/brk_computer/src/models/rarity_meter/extremes.rs +++ b/crates/brk_computer/src/models/rarity_meter/extremes.rs @@ -12,7 +12,8 @@ use vecdb::{ use crate::{ indexes, internal::{ - NumericValue, PerBlock, PercentPerBlock, algo::FenwickTree, + NumericValue, PerBlock, PercentPerBlock, + algo::{ExactOrderStats, FenwickTree}, db_utils::validate_any_computed_version_or_reset, }, }; @@ -20,30 +21,39 @@ use crate::{ const VERSION: Version = Version::new(6); const MIN_HISTORY_BLOCKS: usize = 210_000; const WRITE_INTERVAL: usize = 10_000; +/// Above this many missing outputs, coordinate compression is faster than +/// applying exact live updates one at a time. +const BULK_BACKFILL_THRESHOLD: usize = 10_000; const BANDS: [(f64, u8); 3] = [(0.00025, 3), (0.0005, 2), (0.001, 1)]; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum WindowKind { + Full, + Rolling(usize), +} + #[derive(Clone, Copy)] struct Config { upper_tail: bool, - rolling: bool, + window: WindowKind, positive_only: bool, } const REALIZED: Config = Config { upper_tail: true, - rolling: false, + window: WindowKind::Full, positive_only: false, }; const COINS_IN_LOSS: Config = Config { upper_tail: true, - rolling: false, + window: WindowKind::Full, positive_only: true, }; const SELLER_EXHAUSTION: Config = Config { upper_tail: false, - rolling: true, + window: WindowKind::Rolling(MIN_HISTORY_BLOCKS), positive_only: true, }; @@ -61,6 +71,9 @@ where pub threshold_pct0_025: PerBlock, pub tail: PercentPerBlock, pub rank: PerBlock, + + #[traversable(skip)] + history: LiveHistory, } impl Extreme @@ -94,6 +107,7 @@ where )?, tail: PercentPerBlock::forced_import(db, &format!("{name}_tail"), version, indexes)?, rank: PerBlock::forced_import(db, &format!("{name}_rank"), version, indexes)?, + history: LiveHistory::new(), }) } @@ -141,59 +155,138 @@ where self.tail.ppm.height.any_truncate_if_needed_at(start)?; self.rank.height.any_truncate_if_needed_at(start)?; + if source_end.saturating_sub(start) > BULK_BACKFILL_THRESHOLD { + self.compute_bulk(source, start, source_end, config, exit) + } else { + self.compute_incremental(source, start, source_end, config, exit) + } + } + + fn compute_incremental( + &mut self, + source: &impl ReadableVec, + start: usize, + source_end: usize, + config: Config, + exit: &Exit, + ) -> Result<()> { + let pending = if self.history.is_current(start, config.window) { + source + .collect_range_at(start, source_end) + .into_iter() + .map(Into::into) + .collect() + } else { + let mut values: Vec = source + .collect_range_at(0, source_end) + .into_iter() + .map(Into::into) + .collect(); + let pending = values.split_off(start); + self.history.rebuild(values, config); + pending + }; + + for (offset, value) in pending.into_iter().enumerate() { + let height_index = start + offset; + let state = if is_valid(value, config) && self.history.len() >= MIN_HISTORY_BLOCKS { + event_state(value, &self.history, config) + } else { + EventState::missing() + }; + + self.push_state(state); + self.history.observe(value, config); + self.write_if_needed(height_index, source_end, exit)?; + } + + Ok(()) + } + + /// Preserve the coordinate-compressed Fenwick path for large backfills. + fn compute_bulk( + &mut self, + source: &impl ReadableVec, + start: usize, + source_end: usize, + config: Config, + exit: &Exit, + ) -> Result<()> { let values: Vec = source .collect_range_at(0, source_end) .into_iter() .map(Into::into) .collect(); - let is_valid = |value: f64| value.is_finite() && (!config.positive_only || value > 0.0); - let mut coordinates: Vec = values.iter().copied().filter(|&v| is_valid(v)).collect(); - coordinates.sort_unstable_by(f64::total_cmp); + let mut sorted_values: Vec = values + .iter() + .copied() + .filter(|&value| is_valid(value, config)) + .collect(); + sorted_values.sort_unstable_by(f64::total_cmp); + let mut coordinates = sorted_values.clone(); coordinates.dedup_by(|a, b| a.total_cmp(b).is_eq()); - let mut history = History::new(coordinates.len().max(1), config.rolling); + let mut history = CoordinateHistory::new(&coordinates, config.window); for &value in &values[..start] { - if is_valid(value) { - history.add(bucket(&coordinates, value)); + if is_valid(value, config) { + history.add(value); } } for (height_index, &value) in values.iter().enumerate().skip(start) { - let state = if is_valid(value) && history.len >= MIN_HISTORY_BLOCKS { - event_state(value, &coordinates, &history, config) + let state = if is_valid(value, config) && history.len() >= MIN_HISTORY_BLOCKS { + event_state(value, &history, config) } else { EventState::missing() }; - self.threshold_pct0_1 - .height - .push(T::from(state.thresholds.pct0_1)); - self.threshold_pct0_05 - .height - .push(T::from(state.thresholds.pct0_05)); - self.threshold_pct0_025 - .height - .push(T::from(state.thresholds.pct0_025)); - self.tail - .ppm - .height - .push(PartsPerMillion32::from(state.tail)); - self.rank.height.push(StoredU8::new(state.rank)); - - if is_valid(value) { - history.add(bucket(&coordinates, value)); - } - - if (height_index + 1).is_multiple_of(WRITE_INTERVAL) || height_index + 1 == source_end { - let _lock = exit.lock(); - self.threshold_pct0_1.height.write()?; - self.threshold_pct0_05.height.write()?; - self.threshold_pct0_025.height.write()?; - self.tail.ppm.height.write()?; - self.rank.height.write()?; + self.push_state(state); + if is_valid(value, config) { + history.add(value); } + self.write_if_needed(height_index, source_end, exit)?; } + let (sorted_values, window) = match config.window { + WindowKind::Full => (sorted_values, HistoryWindow::Full), + WindowKind::Rolling(_) => history.rolling_snapshot(), + }; + self.history + .replace_from_sorted(source_end, sorted_values, window); + Ok(()) + } + + fn push_state(&mut self, state: EventState) { + self.threshold_pct0_1 + .height + .push(T::from(state.thresholds.pct0_1)); + self.threshold_pct0_05 + .height + .push(T::from(state.thresholds.pct0_05)); + self.threshold_pct0_025 + .height + .push(T::from(state.thresholds.pct0_025)); + self.tail + .ppm + .height + .push(PartsPerMillion32::from(state.tail)); + self.rank.height.push(StoredU8::new(state.rank)); + } + + fn write_if_needed( + &mut self, + height_index: usize, + source_end: usize, + exit: &Exit, + ) -> Result<()> { + if (height_index + 1).is_multiple_of(WRITE_INTERVAL) || height_index + 1 == source_end { + let _lock = exit.lock(); + self.threshold_pct0_1.height.write()?; + self.threshold_pct0_05.height.write()?; + self.threshold_pct0_025.height.write()?; + self.tail.ppm.height.write()?; + self.rank.height.write()?; + } Ok(()) } } @@ -268,43 +361,178 @@ impl Extremes { } } -struct History { - tree: FenwickTree, - len: usize, - rolling: Option>, +#[derive(Clone)] +enum HistoryWindow { + Full, + Rolling { values: VecDeque, limit: usize }, } -impl History { - fn new(size: usize, rolling: bool) -> Self { - Self { - tree: FenwickTree::new(size), - len: 0, - rolling: rolling.then(VecDeque::new), +impl HistoryWindow { + fn new(kind: WindowKind) -> Self { + match kind { + WindowKind::Full => Self::Full, + WindowKind::Rolling(limit) => Self::Rolling { + values: VecDeque::with_capacity(limit), + limit, + }, } } - fn add(&mut self, bucket: usize) { + fn kind(&self) -> WindowKind { + match self { + Self::Full => WindowKind::Full, + Self::Rolling { limit, .. } => WindowKind::Rolling(*limit), + } + } + + fn push(&mut self, value: T) -> Option { + let Self::Rolling { values, limit } = self else { + return None; + }; + values.push_back(value); + (values.len() > *limit).then(|| values.pop_front().unwrap()) + } +} + +struct CoordinateHistory<'a> { + coordinates: &'a [f64], + tree: FenwickTree, + len: usize, + window: HistoryWindow, +} + +impl<'a> CoordinateHistory<'a> { + fn new(coordinates: &'a [f64], window: WindowKind) -> Self { + Self { + coordinates, + tree: FenwickTree::new(coordinates.len().max(1)), + len: 0, + window: HistoryWindow::new(window), + } + } + + fn add(&mut self, value: f64) { + let bucket = bucket(self.coordinates, value); self.tree.add(bucket, &1.0); self.len += 1; - if let Some(rolling) = &mut self.rolling { - rolling.push_back(bucket); - if rolling.len() > MIN_HISTORY_BLOCKS { - let expired = rolling.pop_front().unwrap(); - self.tree.add(expired, &-1.0); - self.len -= 1; - } + if let Some(expired) = self.window.push(bucket) { + self.tree.add(expired, &-1.0); + self.len -= 1; } } - fn quantile(&self, coordinates: &[f64], percentile: f64) -> f64 { + fn rolling_snapshot(&self) -> (Vec, HistoryWindow) { + let HistoryWindow::Rolling { values, limit } = &self.window else { + unreachable!("rolling snapshot requested for full history") + }; + + let mut counts = vec![0_usize; self.coordinates.len()]; + let chronological: VecDeque<_> = values + .iter() + .map(|&bucket| { + counts[bucket] += 1; + self.coordinates[bucket] + }) + .collect(); + let sorted = self + .coordinates + .iter() + .zip(counts) + .flat_map(|(&value, count)| std::iter::repeat_n(value, count)) + .collect(); + + ( + sorted, + HistoryWindow::Rolling { + values: chronological, + limit: *limit, + }, + ) + } +} + +#[derive(Clone)] +struct LiveHistory { + stats: ExactOrderStats, + processed: usize, + window: HistoryWindow, +} + +impl LiveHistory { + fn new() -> Self { + Self { + stats: ExactOrderStats::new(0), + processed: 0, + window: HistoryWindow::Full, + } + } + + fn is_current(&self, processed: usize, window: WindowKind) -> bool { + self.processed == processed && self.window.kind() == window + } + + fn rebuild(&mut self, mut historical: Vec, config: Config) { + self.processed = historical.len(); + historical.retain(|&value| is_valid(value, config)); + + self.window = HistoryWindow::new(config.window); + if let HistoryWindow::Rolling { values, limit } = &mut self.window { + let keep_from = historical.len().saturating_sub(*limit); + historical.drain(..keep_from); + values.extend(historical.iter().copied()); + } + + self.stats = ExactOrderStats::from_unsorted(historical); + } + + fn replace_from_sorted( + &mut self, + processed: usize, + sorted_values: Vec, + window: HistoryWindow, + ) { + if let HistoryWindow::Rolling { values, .. } = &window { + debug_assert_eq!(sorted_values.len(), values.len()); + } + self.processed = processed; + self.stats = ExactOrderStats::from_sorted(sorted_values); + self.window = window; + } + + fn observe(&mut self, value: f64, config: Config) { + self.processed += 1; + if !is_valid(value, config) { + return; + } + + self.stats.insert(value); + if let Some(expired) = self.window.push(value) { + assert!(self.stats.remove(expired)); + } + } +} + +trait HistoryStats { + fn len(&self) -> usize; + fn quantile(&self, percentile: f64) -> f64; + fn tail(&self, value: f64, upper: bool) -> f64; +} + +impl HistoryStats for CoordinateHistory<'_> { + fn len(&self) -> usize { + self.len + } + + fn quantile(&self, percentile: f64) -> f64 { let target = ((self.len - 1) as f64 * percentile).floor(); let mut index = [0]; self.tree.kth(&[target], &|count: &f64| *count, &mut index); - coordinates[index[0]] + self.coordinates[index[0]] } - fn tail(&self, bucket: usize, upper: bool) -> f64 { + fn tail(&self, value: f64, upper: bool) -> f64 { + let bucket = bucket(self.coordinates, value); let less = if bucket == 0 { 0.0 } else { @@ -320,6 +548,26 @@ impl History { } } +impl HistoryStats for LiveHistory { + fn len(&self) -> usize { + self.stats.len() + } + + fn quantile(&self, percentile: f64) -> f64 { + let index = ((self.stats.len() - 1) as f64 * percentile).floor() as usize; + self.stats.kth(index) + } + + fn tail(&self, value: f64, upper: bool) -> f64 { + let count = if upper { + self.stats.len() - self.stats.count_lt(value) + } else { + self.stats.count_le(value) + }; + (count as f64 + 1.0) / (self.stats.len() as f64 + 1.0) + } +} + struct EventState { thresholds: EventThresholds, tail: f64, @@ -346,11 +594,11 @@ impl EventState { } } -fn event_state(value: f64, coordinates: &[f64], history: &History, config: Config) -> EventState { +fn event_state(value: f64, history: &impl HistoryStats, config: Config) -> EventState { let percentile = |tail: f64| { if config.upper_tail { 1.0 - tail } else { tail } }; - let boundary = |tail: f64| history.quantile(coordinates, percentile(tail)); + let boundary = |tail: f64| history.quantile(percentile(tail)); let thresholds = EventThresholds { pct0_1: boundary(BANDS[2].0), pct0_05: boundary(BANDS[1].0), @@ -374,11 +622,15 @@ fn event_state(value: f64, coordinates: &[f64], history: &History, config: Confi EventState { thresholds, - tail: history.tail(bucket(coordinates, value), config.upper_tail), + tail: history.tail(value, config.upper_tail), rank, } } +fn is_valid(value: f64, config: Config) -> bool { + value.is_finite() && (!config.positive_only || value > 0.0) +} + fn bucket(coordinates: &[f64], value: f64) -> usize { coordinates .binary_search_by(|candidate| candidate.total_cmp(&value)) @@ -389,30 +641,25 @@ fn bucket(coordinates: &[f64], value: f64) -> usize { mod tests { use super::*; - fn history(values: &[f64]) -> (Vec, History) { - let mut coordinates = values.to_vec(); - coordinates.sort_unstable_by(f64::total_cmp); - coordinates.dedup_by(|a, b| a.total_cmp(b).is_eq()); - let mut history = History::new(coordinates.len(), false); - for &value in values { - history.add(bucket(&coordinates, value)); - } - (coordinates, history) + fn history(values: &[f64], config: Config) -> LiveHistory { + let mut history = LiveHistory::new(); + history.rebuild(values.to_vec(), config); + history + } + + fn assert_same_state(left: EventState, right: EventState) { + assert_eq!(left.thresholds.pct0_1, right.thresholds.pct0_1); + assert_eq!(left.thresholds.pct0_05, right.thresholds.pct0_05); + assert_eq!(left.thresholds.pct0_025, right.thresholds.pct0_025); + assert_eq!(left.tail, right.tail); + assert_eq!(left.rank, right.rank); } #[test] fn upper_tail_includes_current_observation() { - let (mut coordinates, mut history) = history(&(1..=100).map(f64::from).collect::>()); - coordinates.push(101.0); - history.tree = { - let mut tree = FenwickTree::new(coordinates.len()); - for value in 1..=100 { - tree.add(bucket(&coordinates, f64::from(value)), &1.0); - } - tree - }; + let history = history(&(1..=100).map(f64::from).collect::>(), REALIZED); - let state = event_state(101.0, &coordinates, &history, REALIZED); + let state = event_state(101.0, &history, REALIZED); assert!((state.tail - 1.0 / 101.0).abs() < f64::EPSILON); assert_eq!(state.rank, 3); assert_eq!(state.thresholds.pct0_025, 99.0); @@ -422,12 +669,11 @@ mod tests { #[test] fn lower_tail_includes_current_observation() { - let coordinates: Vec<_> = (0..=100).map(f64::from).collect(); - let mut history = History::new(coordinates.len(), false); - for value in 1..=100 { - history.add(bucket(&coordinates, f64::from(value))); - } - let state = event_state(0.0, &coordinates, &history, SELLER_EXHAUSTION); + let history = history( + &(1..=100).map(f64::from).collect::>(), + SELLER_EXHAUSTION, + ); + let state = event_state(0.0, &history, SELLER_EXHAUSTION); assert!((state.tail - 1.0 / 101.0).abs() < f64::EPSILON); assert_eq!(state.rank, 3); @@ -435,4 +681,56 @@ mod tests { assert!(state.thresholds.pct0_1 >= state.thresholds.pct0_05); assert!(state.thresholds.pct0_05 >= state.thresholds.pct0_025); } + + #[test] + fn incremental_history_matches_coordinate_history() { + let values = [3.0, 1.0, 2.0, 2.0, -1.0, 5.0, 4.0, 5.0]; + let mut coordinates = values.to_vec(); + coordinates.sort_unstable_by(f64::total_cmp); + coordinates.dedup_by(|a, b| a.total_cmp(b).is_eq()); + + let mut coordinate = CoordinateHistory::new(&coordinates, WindowKind::Full); + let mut incremental = LiveHistory::new(); + + for value in values { + if coordinate.len() > 0 { + assert_same_state( + event_state(value, &coordinate, REALIZED), + event_state(value, &incremental, REALIZED), + ); + } + coordinate.add(value); + incremental.observe(value, REALIZED); + } + } + + #[test] + fn rolling_history_evicts_the_oldest_observation() { + let values: Vec<_> = (1..=MIN_HISTORY_BLOCKS + 1) + .map(|value| value as f64) + .collect(); + let mut history = history(&values, SELLER_EXHAUSTION); + + assert_eq!(history.len(), MIN_HISTORY_BLOCKS); + assert_eq!(history.stats.kth(0), 2.0); + + history.observe((MIN_HISTORY_BLOCKS + 2) as f64, SELLER_EXHAUSTION); + + assert_eq!(history.len(), MIN_HISTORY_BLOCKS); + assert_eq!(history.stats.kth(0), 3.0); + } + + #[test] + fn rollback_rebuild_replaces_incremental_state() { + let mut history = history(&[1.0, 2.0, 3.0], REALIZED); + history.observe(4.0, REALIZED); + assert!(history.is_current(4, WindowKind::Full)); + + history.rebuild(vec![1.0, 2.0], REALIZED); + + assert!(history.is_current(2, WindowKind::Full)); + assert_eq!(history.len(), 2); + assert_eq!(history.stats.kth(0), 1.0); + assert_eq!(history.stats.kth(1), 2.0); + } } diff --git a/crates/brk_logger/src/lib.rs b/crates/brk_logger/src/lib.rs index 722f6151d..582159b2e 100644 --- a/crates/brk_logger/src/lib.rs +++ b/crates/brk_logger/src/lib.rs @@ -22,15 +22,23 @@ const MAX_LOG_AGE_DAYS: u64 = 7; /// tracing level. The directory is created if it does not exist, and any /// `*.txt` file older than 7 days is pruned on startup. pub fn init(dir: Option<&Path>) -> io::Result<()> { - tracing_log::LogTracer::init().ok(); - install_panic_hook(); - #[cfg(debug_assertions)] const DEFAULT_LEVEL: &str = "debug"; #[cfg(not(debug_assertions))] const DEFAULT_LEVEL: &str = "info"; - let level = std::env::var("LOG").unwrap_or_else(|_| DEFAULT_LEVEL.to_string()); + init_with_default_level(dir, DEFAULT_LEVEL) +} + +/// Initialize the logger with a caller-selected fallback level. +/// +/// `LOG` and `RUST_LOG` still take precedence. This is useful for services +/// whose normal debug traffic is too verbose for their default execution mode. +pub fn init_with_default_level(dir: Option<&Path>, default_level: &str) -> io::Result<()> { + tracing_log::LogTracer::init().ok(); + install_panic_hook(); + + let level = std::env::var("LOG").unwrap_or_else(|_| default_level.to_string()); let directives = std::env::var("RUST_LOG").unwrap_or_else(|_| { format!( diff --git a/crates/brk_mcp/Cargo.toml b/crates/brk_mcp/Cargo.toml new file mode 100644 index 000000000..2758f4ebc --- /dev/null +++ b/crates/brk_mcp/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "brk_mcp" +description = "A stateless MCP bridge to BRK's Cloudflare-cached REST API" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +axum = { workspace = true } +base64 = "0.23.1" +brk_logger = { workspace = true } +rmcp = { version = "3.1.0", default-features = false, features = ["server", "transport-streamable-http-server"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros", "net", "sync", "time"] } +tracing = { workspace = true } +ureq = { workspace = true } + +[[bin]] +name = "brk_mcp" +path = "src/main.rs" diff --git a/crates/brk_mcp/README.md b/crates/brk_mcp/README.md new file mode 100644 index 000000000..6f16cd01b --- /dev/null +++ b/crates/brk_mcp/README.md @@ -0,0 +1,54 @@ +# brk_mcp + +`brk_mcp` is a thin, stateless, read-only MCP adapter for the BRK REST API. It +exposes the generated OpenAPI operations as MCP tools and forwards every tool +call to the configured public REST origin as a `GET` request. + +## Caching model + +`brk_mcp` does not cache API responses or retain MCP sessions. Point it at the +public Cloudflare-fronted REST origin so its upstream `GET` requests use the +existing Cloudflare cache: + +```text +MCP client -> brk_mcp -> Cloudflare-cached REST API -> BRK server +``` + +Cloudflare does not need to cache the `/mcp` endpoint. The MCP catalog TTL is +only a standard client-side cache hint for the static discovery and tool-list +metadata. + +## Run + +From the workspace: + +```sh +cargo run --bin brk_mcp -- bitview.space +``` + +Or run an installed binary: + +```sh +brk_mcp bitview.space +``` + +A bare host tries HTTPS first and retries over HTTP only if the HTTPS request +fails at the transport layer. An explicit origin uses only that origin: + +```sh +brk_mcp http://127.0.0.1:3110 +``` + +The Streamable HTTP endpoint is `http://127.0.0.1:3111/mcp` by default. If that +port is unavailable, the server tries each port through `3211`. The server +supports MCP protocol version `2026-07-28`. + +The REST URL or host is the only configuration. It must be an HTTP(S) origin +without a path, query, or credentials. + +## Generated catalog + +The tool catalog is generated from the canonical OpenAPI document by +`brk_bindgen` and embedded in the binary at compile time from +`generated/manifest.json`. Do not edit the generated manifest by hand; rerun +the BRK bindgen target after changing the API. diff --git a/crates/brk_mcp/generated/manifest.json b/crates/brk_mcp/generated/manifest.json new file mode 100644 index 000000000..68a943bc5 --- /dev/null +++ b/crates/brk_mcp/generated/manifest.json @@ -0,0 +1,16022 @@ +{ + "schemaVersion": 1, + "operations": [ + { + "tool": { + "name": "get_address", + "title": "Address information", + "description": "Retrieve address information including current balance and transaction counts. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address)*\n\nREST operation: `GET /api/address/{address}`.", + "inputSchema": { + "type": "object", + "properties": { + "address": { + "$ref": "#/$defs/Addr" + } + }, + "additionalProperties": false, + "required": [ + "address" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Addr": { + "description": "Bitcoin address string", + "type": "string", + "examples": [ + "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/AddrStats", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Addr": { + "type": "string" + }, + "AddrChainStats": { + "required": [ + "balance", + "funded_txo_count", + "funded_txo_sum", + "spent_txo_count", + "spent_txo_sum", + "tx_count", + "type_index", + "realized_price" + ], + "type": "object", + "properties": { + "balance": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "funded_txo_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "funded_txo_sum": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "spent_txo_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "spent_txo_sum": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "type_index": { + "allOf": [ + { + "$ref": "#/$defs/TypeIndex" + } + ] + }, + "realized_price": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "AddrMempoolStats": { + "required": [ + "balance_delta", + "funded_txo_count", + "funded_txo_sum", + "spent_txo_count", + "spent_txo_sum", + "tx_count" + ], + "type": "object", + "properties": { + "balance_delta": { + "allOf": [ + { + "$ref": "#/$defs/SatsSigned" + } + ] + }, + "funded_txo_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "funded_txo_sum": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "spent_txo_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "spent_txo_sum": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "AddrStats": { + "required": [ + "address", + "addr_type", + "chain_stats", + "mempool_stats", + "balance" + ], + "type": "object", + "properties": { + "address": { + "allOf": [ + { + "$ref": "#/$defs/Addr" + } + ] + }, + "addr_type": { + "allOf": [ + { + "$ref": "#/$defs/OutputType" + } + ] + }, + "chain_stats": { + "allOf": [ + { + "$ref": "#/$defs/AddrChainStats" + } + ] + }, + "mempool_stats": { + "allOf": [ + { + "$ref": "#/$defs/AddrMempoolStats" + } + ] + }, + "balance": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "OutputType": { + "enum": [ + "p2pk", + "p2pk", + "p2pkh", + "multisig", + "p2sh", + "op_return", + "v0_p2wpkh", + "v0_p2wsh", + "v1_p2tr", + "p2a", + "empty", + "unknown" + ], + "type": "string" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SatsSigned": { + "format": "int64", + "type": "integer" + }, + "TypeIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/address/{address}", + "parameters": [ + { + "name": "address", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_address_confirmed_txs", + "title": "Address confirmed transactions", + "description": "Get the first 25 confirmed transactions for an address. For pagination, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*\n\nREST operation: `GET /api/address/{address}/txs/chain`.", + "inputSchema": { + "type": "object", + "properties": { + "address": { + "$ref": "#/$defs/Addr" + } + }, + "additionalProperties": false, + "required": [ + "address" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Addr": { + "description": "Bitcoin address string", + "type": "string", + "examples": [ + "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Transaction" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/address/{address}/txs/chain", + "parameters": [ + { + "name": "address", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_address_confirmed_txs_after", + "title": "Address confirmed transactions (paginated)", + "description": "Get the next 25 confirmed transactions strictly older than `after_txid` (Esplora-canonical pagination form, matches mempool.space).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*\n\nREST operation: `GET /api/address/{address}/txs/chain/{after_txid}`.", + "inputSchema": { + "type": "object", + "properties": { + "address": { + "$ref": "#/$defs/Addr" + }, + "after_txid": { + "$ref": "#/$defs/Txid", + "description": "Last txid from the previous page (return transactions strictly older than this)" + } + }, + "additionalProperties": false, + "required": [ + "address", + "after_txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Addr": { + "description": "Bitcoin address string", + "type": "string", + "examples": [ + "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + ] + }, + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Transaction" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/address/{address}/txs/chain/{after_txid}", + "parameters": [ + { + "name": "address", + "location": "path" + }, + { + "name": "after_txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_address_hash_prefix_matches", + "title": "Address hash-prefix matches", + "description": "Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata with `GET /api/address/{address}`.\n\nREST operation: `GET /api/address/hash-prefix/{addr_type}/{prefix}`.", + "inputSchema": { + "type": "object", + "properties": { + "addr_type": { + "$ref": "#/$defs/OutputType" + }, + "prefix": { + "type": "string", + "description": "First 1–16 hexadecimal nibbles of the RapidHash v3 hash over the raw\naddress payload bytes." + } + }, + "additionalProperties": false, + "required": [ + "addr_type", + "prefix" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "OutputType": { + "description": "Type (P2PKH, P2WPKH, P2SH, P2TR, etc.)", + "type": "string", + "enum": [ + "p2pk", + "p2pk", + "p2pkh", + "multisig", + "p2sh", + "op_return", + "v0_p2wpkh", + "v0_p2wsh", + "v1_p2tr", + "p2a", + "empty", + "unknown" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/AddrHashPrefixMatches", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Addr": { + "type": "string" + }, + "AddrHashPrefixMatches": { + "type": "object", + "properties": { + "addr_type": { + "$ref": "#/$defs/OutputType" + }, + "prefix": { + "type": "string" + }, + "truncated": { + "type": "boolean" + }, + "addresses": { + "type": "array", + "items": { + "$ref": "#/$defs/Addr" + } + } + }, + "required": [ + "addr_type", + "prefix", + "truncated", + "addresses" + ] + }, + "OutputType": { + "enum": [ + "p2pk", + "p2pk", + "p2pkh", + "multisig", + "p2sh", + "op_return", + "v0_p2wpkh", + "v0_p2wsh", + "v1_p2tr", + "p2a", + "empty", + "unknown" + ], + "type": "string" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/address/hash-prefix/{addr_type}/{prefix}", + "parameters": [ + { + "name": "addr_type", + "location": "path" + }, + { + "name": "prefix", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_address_mempool_txs", + "title": "Address mempool transactions", + "description": "Get unconfirmed transactions for an address from the mempool, newest first (up to 50).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-mempool)*\n\nREST operation: `GET /api/address/{address}/txs/mempool`.", + "inputSchema": { + "type": "object", + "properties": { + "address": { + "$ref": "#/$defs/Addr" + } + }, + "additionalProperties": false, + "required": [ + "address" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Addr": { + "description": "Bitcoin address string", + "type": "string", + "examples": [ + "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Transaction" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/address/{address}/txs/mempool", + "parameters": [ + { + "name": "address", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_address_txs", + "title": "Address transactions", + "description": "Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*\n\nREST operation: `GET /api/address/{address}/txs`.", + "inputSchema": { + "type": "object", + "properties": { + "address": { + "$ref": "#/$defs/Addr" + } + }, + "additionalProperties": false, + "required": [ + "address" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Addr": { + "description": "Bitcoin address string", + "type": "string", + "examples": [ + "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Transaction" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/address/{address}/txs", + "parameters": [ + { + "name": "address", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_address_utxos", + "title": "Address UTXOs", + "description": "Get unspent transaction outputs (UTXOs) for an address. Returns txid, vout, value, and confirmation status for each UTXO.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-utxo)*\n\nREST operation: `GET /api/address/{address}/utxo`.", + "inputSchema": { + "type": "object", + "properties": { + "address": { + "$ref": "#/$defs/Addr" + } + }, + "additionalProperties": false, + "required": [ + "address" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Addr": { + "description": "Bitcoin address string", + "type": "string", + "examples": [ + "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Utxo" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "Txid": { + "type": "string" + }, + "Utxo": { + "required": [ + "txid", + "vout", + "status", + "value" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/address/{address}/utxo", + "parameters": [ + { + "name": "address", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_api", + "title": "Compact OpenAPI specification", + "description": "Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. The full specification is available at `GET /openapi.json`.\n\nREST operation: `GET /api.json`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api.json", + "parameters": [] + } + }, + { + "tool": { + "name": "get_block", + "title": "Block information", + "description": "Retrieve block information by block hash. Returns block metadata including height, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block)*\n\nREST operation: `GET /api/block/{hash}`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash" + } + }, + "additionalProperties": false, + "required": [ + "hash" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/BlockInfo", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "BlockInfo": { + "required": [ + "id", + "height", + "version", + "timestamp", + "bits", + "nonce", + "difficulty", + "merkle_root", + "tx_count", + "size", + "weight", + "previousblockhash", + "mediantime" + ], + "type": "object", + "properties": { + "id": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "bits": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "nonce": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "merkle_root": { + "type": "string" + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "previousblockhash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "mediantime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block/{hash}", + "parameters": [ + { + "name": "hash", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_by_height", + "title": "Block hash by height", + "description": "Retrieve the block hash at a given height. Returns the hash as plain text.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-height)*\n\nREST operation: `GET /api/block-height/{height}`.", + "inputSchema": { + "type": "object", + "properties": { + "height": { + "$ref": "#/$defs/Height" + } + }, + "additionalProperties": false, + "required": [ + "height" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Height": { + "description": "Block height", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 0, + 210000, + 420000, + 840000 + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block-height/{height}", + "parameters": [ + { + "name": "height", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_by_timestamp", + "title": "Block by timestamp", + "description": "Find the block closest to a given UNIX timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-timestamp)*\n\nREST operation: `GET /api/v1/mining/blocks/timestamp/{timestamp}`.", + "inputSchema": { + "type": "object", + "properties": { + "timestamp": { + "$ref": "#/$defs/Timestamp" + } + }, + "additionalProperties": false, + "required": [ + "timestamp" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Timestamp": { + "description": "UNIX timestamp in seconds", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 1231006505, + 1672531200, + 1713571200, + 1743631892, + 1759000868 + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/BlockTimestamp", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "BlockTimestamp": { + "required": [ + "height", + "hash", + "timestamp" + ], + "type": "object", + "properties": { + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "hash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "timestamp": { + "type": "string" + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/blocks/timestamp/{timestamp}", + "parameters": [ + { + "name": "timestamp", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_fee_rates", + "title": "Block fee rates", + "description": "Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*\n\nREST operation: `GET /api/v1/mining/blocks/fee-rates/{time_period}`.", + "inputSchema": { + "type": "object", + "properties": { + "time_period": { + "$ref": "#/$defs/TimePeriod" + } + }, + "additionalProperties": false, + "required": [ + "time_period" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TimePeriod": { + "description": "Time period for mining statistics.\n\nUsed to specify the lookback window for pool statistics, hashrate calculations,\nand other time-based mining series.", + "type": "string", + "enum": [ + "24h", + "3d", + "1w", + "1m", + "3m", + "6m", + "1y", + "2y", + "3y", + "all" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockFeeRatesEntry" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockFeeRatesEntry": { + "required": [ + "avgHeight", + "timestamp", + "avgFee_0", + "avgFee_10", + "avgFee_25", + "avgFee_50", + "avgFee_75", + "avgFee_90", + "avgFee_100" + ], + "type": "object", + "properties": { + "avgHeight": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgFee_0": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "avgFee_10": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "avgFee_25": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "avgFee_50": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "avgFee_75": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "avgFee_90": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "avgFee_100": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + } + } + }, + "FeeRate": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/blocks/fee-rates/{time_period}", + "parameters": [ + { + "name": "time_period", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_fees", + "title": "Block fees", + "description": "Get average total fees per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)*\n\nREST operation: `GET /api/v1/mining/blocks/fees/{time_period}`.", + "inputSchema": { + "type": "object", + "properties": { + "time_period": { + "$ref": "#/$defs/TimePeriod" + } + }, + "additionalProperties": false, + "required": [ + "time_period" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TimePeriod": { + "description": "Time period for mining statistics.\n\nUsed to specify the lookback window for pool statistics, hashrate calculations,\nand other time-based mining series.", + "type": "string", + "enum": [ + "24h", + "3d", + "1w", + "1m", + "3m", + "6m", + "1y", + "2y", + "3y", + "all" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockFeesEntry" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockFeesEntry": { + "required": [ + "avgHeight", + "timestamp", + "avgFees", + "USD" + ], + "type": "object", + "properties": { + "avgHeight": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgFees": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "USD": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/blocks/fees/{time_period}", + "parameters": [ + { + "name": "time_period", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_header", + "title": "Block header", + "description": "Returns the hex-encoded 80-byte block header.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-header)*\n\nREST operation: `GET /api/block/{hash}/header`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash" + } + }, + "additionalProperties": false, + "required": [ + "hash" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block/{hash}/header", + "parameters": [ + { + "name": "hash", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_raw", + "title": "Raw block", + "description": "Returns the raw block data in binary format.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-raw)*\n\nREST operation: `GET /api/block/{hash}/raw`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash" + } + }, + "additionalProperties": false, + "required": [ + "hash" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block/{hash}/raw", + "parameters": [ + { + "name": "hash", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_rewards", + "title": "Block rewards", + "description": "Get average coinbase reward (subsidy + fees) per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)*\n\nREST operation: `GET /api/v1/mining/blocks/rewards/{time_period}`.", + "inputSchema": { + "type": "object", + "properties": { + "time_period": { + "$ref": "#/$defs/TimePeriod" + } + }, + "additionalProperties": false, + "required": [ + "time_period" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TimePeriod": { + "description": "Time period for mining statistics.\n\nUsed to specify the lookback window for pool statistics, hashrate calculations,\nand other time-based mining series.", + "type": "string", + "enum": [ + "24h", + "3d", + "1w", + "1m", + "3m", + "6m", + "1y", + "2y", + "3y", + "all" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockRewardsEntry" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockRewardsEntry": { + "required": [ + "avgHeight", + "timestamp", + "avgRewards", + "USD" + ], + "type": "object", + "properties": { + "avgHeight": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgRewards": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "USD": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/blocks/rewards/{time_period}", + "parameters": [ + { + "name": "time_period", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_sizes_weights", + "title": "Block sizes and weights", + "description": "Get average block sizes and weights for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)*\n\nREST operation: `GET /api/v1/mining/blocks/sizes-weights/{time_period}`.", + "inputSchema": { + "type": "object", + "properties": { + "time_period": { + "$ref": "#/$defs/TimePeriod" + } + }, + "additionalProperties": false, + "required": [ + "time_period" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TimePeriod": { + "description": "Time period for mining statistics.\n\nUsed to specify the lookback window for pool statistics, hashrate calculations,\nand other time-based mining series.", + "type": "string", + "enum": [ + "24h", + "3d", + "1w", + "1m", + "3m", + "6m", + "1y", + "2y", + "3y", + "all" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/BlockSizesWeights", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockSizeEntry": { + "required": [ + "avgHeight", + "timestamp", + "avgSize" + ], + "type": "object", + "properties": { + "avgHeight": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "BlockSizesWeights": { + "required": [ + "sizes", + "weights" + ], + "type": "object", + "properties": { + "sizes": { + "items": { + "$ref": "#/$defs/BlockSizeEntry" + }, + "type": "array" + }, + "weights": { + "items": { + "$ref": "#/$defs/BlockWeightEntry" + }, + "type": "array" + } + } + }, + "BlockWeightEntry": { + "required": [ + "avgHeight", + "timestamp", + "avgWeight" + ], + "type": "object", + "properties": { + "avgHeight": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgWeight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/blocks/sizes-weights/{time_period}", + "parameters": [ + { + "name": "time_period", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_status", + "title": "Block status", + "description": "Retrieve the status of a block. Returns whether the block is in the best chain and, if so, its height and the hash of the next block.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-status)*\n\nREST operation: `GET /api/block/{hash}/status`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash" + } + }, + "additionalProperties": false, + "required": [ + "hash" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/BlockStatus", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "BlockStatus": { + "required": [ + "in_best_chain" + ], + "type": "object", + "properties": { + "in_best_chain": { + "type": "boolean" + }, + "height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "next_best": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block/{hash}/status", + "parameters": [ + { + "name": "hash", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_template", + "title": "Projected next block template", + "description": "Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template.\n\nREST operation: `GET /api/v1/mempool/block-template`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/BlockTemplate", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "BlockTemplate": { + "required": [ + "hash", + "stats", + "transactions" + ], + "type": "object", + "properties": { + "hash": { + "allOf": [ + { + "$ref": "#/$defs/NextBlockHash" + } + ] + }, + "stats": { + "allOf": [ + { + "$ref": "#/$defs/MempoolBlock" + } + ] + }, + "transactions": { + "items": { + "$ref": "#/$defs/Transaction" + }, + "type": "array" + } + } + }, + "FeeRate": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "MempoolBlock": { + "required": [ + "blockSize", + "blockVSize", + "nTx", + "totalFees", + "medianFee", + "feeRange" + ], + "type": "object", + "properties": { + "blockSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "blockVSize": { + "format": "double", + "type": "number" + }, + "nTx": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "totalFees": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "feeRange": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/FeeRate" + }, + "minItems": 7 + } + } + }, + "NextBlockHash": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mempool/block-template", + "parameters": [] + } + }, + { + "tool": { + "name": "get_block_template_diff", + "title": "Block template diff since hash", + "description": "Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `GET /api/v1/mempool/block-template`.\n\nREST operation: `GET /api/v1/mempool/block-template/diff/{hash}`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/NextBlockHash" + } + }, + "additionalProperties": false, + "required": [ + "hash" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "NextBlockHash": { + "description": "Content hash of the projected next block (block 0 of the mempool\nsnapshot). Same value as the mempool ETag. Opaque token: pass back\nto `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + }, + "outputSchema": { + "$ref": "#/$defs/BlockTemplateDiff", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "BlockTemplateDiff": { + "required": [ + "hash", + "since", + "order", + "removed" + ], + "type": "object", + "properties": { + "hash": { + "allOf": [ + { + "$ref": "#/$defs/NextBlockHash" + } + ] + }, + "since": { + "allOf": [ + { + "$ref": "#/$defs/NextBlockHash" + } + ] + }, + "order": { + "items": { + "$ref": "#/$defs/BlockTemplateDiffEntry" + }, + "type": "array" + }, + "removed": { + "items": { + "$ref": "#/$defs/Txid" + }, + "type": "array" + } + } + }, + "BlockTemplateDiffEntry": { + "anyOf": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + { + "$ref": "#/$defs/Transaction" + } + ] + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "NextBlockHash": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mempool/block-template/diff/{hash}", + "parameters": [ + { + "name": "hash", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_tip_hash", + "title": "Block tip hash", + "description": "Returns the hash of the last block.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-tip-hash)*\n\nREST operation: `GET /api/blocks/tip/hash`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/blocks/tip/hash", + "parameters": [] + } + }, + { + "tool": { + "name": "get_block_tip_height", + "title": "Block tip height", + "description": "Returns the height of the last block.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-tip-height)*\n\nREST operation: `GET /api/blocks/tip/height`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/blocks/tip/height", + "parameters": [] + } + }, + { + "tool": { + "name": "get_block_txid", + "title": "Transaction ID at index", + "description": "Retrieve a single transaction ID at a specific index within a block. Returns plain text txid.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transaction-id)*\n\nREST operation: `GET /api/block/{hash}/txid/{index}`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash", + "description": "Bitcoin block hash" + }, + "index": { + "$ref": "#/$defs/BlockTxIndex", + "description": "Transaction index within the block (0-based)" + } + }, + "additionalProperties": false, + "required": [ + "hash", + "index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + }, + "BlockTxIndex": { + "description": "Position of a transaction within a single block (0 = coinbase).\nDistinct from `TxIndex`, which is the chain-wide global tx index.", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 0 + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block/{hash}/txid/{index}", + "parameters": [ + { + "name": "hash", + "location": "path" + }, + { + "name": "index", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_txids", + "title": "Block transaction IDs", + "description": "Retrieve all transaction IDs in a block. Returns an array of txids in block order.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transaction-ids)*\n\nREST operation: `GET /api/block/{hash}/txids`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash" + } + }, + "additionalProperties": false, + "required": [ + "hash" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Txid" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "type": "string" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block/{hash}/txids", + "parameters": [ + { + "name": "hash", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_txs", + "title": "Block transactions", + "description": "Retrieve transactions in a block by block hash. Returns up to 25 transactions starting from index 0.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transactions)*\n\nREST operation: `GET /api/block/{hash}/txs`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash" + } + }, + "additionalProperties": false, + "required": [ + "hash" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Transaction" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block/{hash}/txs", + "parameters": [ + { + "name": "hash", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_txs_from_index", + "title": "Block transactions (paginated)", + "description": "Retrieve transactions in a block by block hash, starting from the specified index. Returns up to 25 transactions at a time.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-transactions)*\n\nREST operation: `GET /api/block/{hash}/txs/{start_index}`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash", + "description": "Bitcoin block hash" + }, + "start_index": { + "$ref": "#/$defs/BlockTxIndex", + "description": "Starting transaction index within the block (0-based)" + } + }, + "additionalProperties": false, + "required": [ + "hash", + "start_index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + }, + "BlockTxIndex": { + "description": "Position of a transaction within a single block (0 = coinbase).\nDistinct from `TxIndex`, which is the chain-wide global tx index.", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 0 + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Transaction" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/block/{hash}/txs/{start_index}", + "parameters": [ + { + "name": "hash", + "location": "path" + }, + { + "name": "start_index", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_block_v1", + "title": "Block (v1)", + "description": "Returns block details with extras by hash.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-v1)*\n\nREST operation: `GET /api/v1/block/{hash}`.", + "inputSchema": { + "type": "object", + "properties": { + "hash": { + "$ref": "#/$defs/BlockHash" + } + }, + "additionalProperties": false, + "required": [ + "hash" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "description": "Block hash", + "type": "string", + "examples": [ + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/BlockInfoV1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockExtras": { + "required": [ + "totalFees", + "medianFee", + "feeRange", + "reward", + "pool", + "avgFee", + "avgFeeRate", + "coinbaseRaw", + "coinbaseAddresses", + "coinbaseSignature", + "coinbaseSignatureAscii", + "avgTxSize", + "totalInputs", + "totalOutputs", + "totalOutputAmt", + "medianFeeAmt", + "feePercentiles", + "segwitTotalTxs", + "segwitTotalSize", + "segwitTotalWeight", + "header", + "utxoSetChange", + "utxoSetSize", + "totalInputAmt", + "virtualSize", + "orphans", + "price" + ], + "type": "object", + "properties": { + "totalFees": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "feeRange": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/FeeRate" + }, + "minItems": 7 + }, + "reward": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "pool": { + "allOf": [ + { + "$ref": "#/$defs/BlockPool" + } + ] + }, + "avgFee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "avgFeeRate": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "coinbaseRaw": { + "type": "string" + }, + "coinbaseAddress": { + "type": [ + "string", + "null" + ] + }, + "coinbaseAddresses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "coinbaseSignature": { + "type": "string" + }, + "coinbaseSignatureAscii": { + "type": "string" + }, + "avgTxSize": { + "format": "double", + "type": "number" + }, + "totalInputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFeeAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "feePercentiles": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/Sats" + }, + "minItems": 7 + }, + "segwitTotalTxs": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "segwitTotalSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "segwitTotalWeight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "header": { + "type": "string" + }, + "utxoSetChange": { + "format": "int64", + "type": "integer" + }, + "utxoSetSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalInputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "virtualSize": { + "format": "double", + "type": "number" + }, + "firstSeen": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint64" + }, + "orphans": { + "items": { + "type": "string" + }, + "type": "array" + }, + "price": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "BlockHash": { + "type": "string" + }, + "BlockInfoV1": { + "required": [ + "id", + "height", + "version", + "timestamp", + "bits", + "nonce", + "difficulty", + "merkle_root", + "tx_count", + "size", + "weight", + "previousblockhash", + "mediantime", + "extras" + ], + "type": "object", + "properties": { + "id": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "bits": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "nonce": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "merkle_root": { + "type": "string" + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "previousblockhash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "mediantime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "stale": { + "type": "boolean" + }, + "extras": { + "allOf": [ + { + "$ref": "#/$defs/BlockExtras" + } + ] + } + } + }, + "BlockPool": { + "required": [ + "id", + "name", + "slug", + "blockNumber" + ], + "type": "object", + "properties": { + "id": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "slug": { + "allOf": [ + { + "$ref": "#/$defs/PoolSlug" + } + ] + }, + "blockNumber": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "minerNames": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + } + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "FeeRate": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "PoolSlug": { + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ], + "type": "string" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/block/{hash}", + "parameters": [ + { + "name": "hash", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_blocks", + "title": "Recent blocks", + "description": "Retrieve the last 10 blocks. Returns block metadata for each block.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*\n\nREST operation: `GET /api/blocks`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockInfo" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "BlockInfo": { + "required": [ + "id", + "height", + "version", + "timestamp", + "bits", + "nonce", + "difficulty", + "merkle_root", + "tx_count", + "size", + "weight", + "previousblockhash", + "mediantime" + ], + "type": "object", + "properties": { + "id": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "bits": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "nonce": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "merkle_root": { + "type": "string" + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "previousblockhash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "mediantime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/blocks", + "parameters": [] + } + }, + { + "tool": { + "name": "get_blocks_from_height", + "title": "Blocks from height", + "description": "Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*\n\nREST operation: `GET /api/blocks/{height}`.", + "inputSchema": { + "type": "object", + "properties": { + "height": { + "$ref": "#/$defs/Height" + } + }, + "additionalProperties": false, + "required": [ + "height" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Height": { + "description": "Block height", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 0, + 210000, + 420000, + 840000 + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockInfo" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "BlockInfo": { + "required": [ + "id", + "height", + "version", + "timestamp", + "bits", + "nonce", + "difficulty", + "merkle_root", + "tx_count", + "size", + "weight", + "previousblockhash", + "mediantime" + ], + "type": "object", + "properties": { + "id": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "bits": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "nonce": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "merkle_root": { + "type": "string" + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "previousblockhash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "mediantime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/blocks/{height}", + "parameters": [ + { + "name": "height", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_blocks_v1", + "title": "Recent blocks with extras", + "description": "Retrieve the last 15 blocks with extended data including pool identification and fee statistics.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks-v1)*\n\nREST operation: `GET /api/v1/blocks`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockInfoV1" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockExtras": { + "required": [ + "totalFees", + "medianFee", + "feeRange", + "reward", + "pool", + "avgFee", + "avgFeeRate", + "coinbaseRaw", + "coinbaseAddresses", + "coinbaseSignature", + "coinbaseSignatureAscii", + "avgTxSize", + "totalInputs", + "totalOutputs", + "totalOutputAmt", + "medianFeeAmt", + "feePercentiles", + "segwitTotalTxs", + "segwitTotalSize", + "segwitTotalWeight", + "header", + "utxoSetChange", + "utxoSetSize", + "totalInputAmt", + "virtualSize", + "orphans", + "price" + ], + "type": "object", + "properties": { + "totalFees": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "feeRange": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/FeeRate" + }, + "minItems": 7 + }, + "reward": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "pool": { + "allOf": [ + { + "$ref": "#/$defs/BlockPool" + } + ] + }, + "avgFee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "avgFeeRate": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "coinbaseRaw": { + "type": "string" + }, + "coinbaseAddress": { + "type": [ + "string", + "null" + ] + }, + "coinbaseAddresses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "coinbaseSignature": { + "type": "string" + }, + "coinbaseSignatureAscii": { + "type": "string" + }, + "avgTxSize": { + "format": "double", + "type": "number" + }, + "totalInputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFeeAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "feePercentiles": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/Sats" + }, + "minItems": 7 + }, + "segwitTotalTxs": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "segwitTotalSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "segwitTotalWeight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "header": { + "type": "string" + }, + "utxoSetChange": { + "format": "int64", + "type": "integer" + }, + "utxoSetSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalInputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "virtualSize": { + "format": "double", + "type": "number" + }, + "firstSeen": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint64" + }, + "orphans": { + "items": { + "type": "string" + }, + "type": "array" + }, + "price": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "BlockHash": { + "type": "string" + }, + "BlockInfoV1": { + "required": [ + "id", + "height", + "version", + "timestamp", + "bits", + "nonce", + "difficulty", + "merkle_root", + "tx_count", + "size", + "weight", + "previousblockhash", + "mediantime", + "extras" + ], + "type": "object", + "properties": { + "id": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "bits": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "nonce": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "merkle_root": { + "type": "string" + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "previousblockhash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "mediantime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "stale": { + "type": "boolean" + }, + "extras": { + "allOf": [ + { + "$ref": "#/$defs/BlockExtras" + } + ] + } + } + }, + "BlockPool": { + "required": [ + "id", + "name", + "slug", + "blockNumber" + ], + "type": "object", + "properties": { + "id": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "slug": { + "allOf": [ + { + "$ref": "#/$defs/PoolSlug" + } + ] + }, + "blockNumber": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "minerNames": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + } + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "FeeRate": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "PoolSlug": { + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ], + "type": "string" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/blocks", + "parameters": [] + } + }, + { + "tool": { + "name": "get_blocks_v1_from_height", + "title": "Blocks from height with extras", + "description": "Retrieve up to 15 blocks with extended data going backwards from the given height.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks-v1)*\n\nREST operation: `GET /api/v1/blocks/{height}`.", + "inputSchema": { + "type": "object", + "properties": { + "height": { + "$ref": "#/$defs/Height" + } + }, + "additionalProperties": false, + "required": [ + "height" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Height": { + "description": "Block height", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 0, + 210000, + 420000, + 840000 + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockInfoV1" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockExtras": { + "required": [ + "totalFees", + "medianFee", + "feeRange", + "reward", + "pool", + "avgFee", + "avgFeeRate", + "coinbaseRaw", + "coinbaseAddresses", + "coinbaseSignature", + "coinbaseSignatureAscii", + "avgTxSize", + "totalInputs", + "totalOutputs", + "totalOutputAmt", + "medianFeeAmt", + "feePercentiles", + "segwitTotalTxs", + "segwitTotalSize", + "segwitTotalWeight", + "header", + "utxoSetChange", + "utxoSetSize", + "totalInputAmt", + "virtualSize", + "orphans", + "price" + ], + "type": "object", + "properties": { + "totalFees": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "feeRange": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/FeeRate" + }, + "minItems": 7 + }, + "reward": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "pool": { + "allOf": [ + { + "$ref": "#/$defs/BlockPool" + } + ] + }, + "avgFee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "avgFeeRate": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "coinbaseRaw": { + "type": "string" + }, + "coinbaseAddress": { + "type": [ + "string", + "null" + ] + }, + "coinbaseAddresses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "coinbaseSignature": { + "type": "string" + }, + "coinbaseSignatureAscii": { + "type": "string" + }, + "avgTxSize": { + "format": "double", + "type": "number" + }, + "totalInputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFeeAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "feePercentiles": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/Sats" + }, + "minItems": 7 + }, + "segwitTotalTxs": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "segwitTotalSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "segwitTotalWeight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "header": { + "type": "string" + }, + "utxoSetChange": { + "format": "int64", + "type": "integer" + }, + "utxoSetSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalInputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "virtualSize": { + "format": "double", + "type": "number" + }, + "firstSeen": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint64" + }, + "orphans": { + "items": { + "type": "string" + }, + "type": "array" + }, + "price": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "BlockHash": { + "type": "string" + }, + "BlockInfoV1": { + "required": [ + "id", + "height", + "version", + "timestamp", + "bits", + "nonce", + "difficulty", + "merkle_root", + "tx_count", + "size", + "weight", + "previousblockhash", + "mediantime", + "extras" + ], + "type": "object", + "properties": { + "id": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "bits": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "nonce": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "merkle_root": { + "type": "string" + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "previousblockhash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "mediantime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "stale": { + "type": "boolean" + }, + "extras": { + "allOf": [ + { + "$ref": "#/$defs/BlockExtras" + } + ] + } + } + }, + "BlockPool": { + "required": [ + "id", + "name", + "slug", + "blockNumber" + ], + "type": "object", + "properties": { + "id": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "slug": { + "allOf": [ + { + "$ref": "#/$defs/PoolSlug" + } + ] + }, + "blockNumber": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "minerNames": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + } + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "FeeRate": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "PoolSlug": { + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ], + "type": "string" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/blocks/{height}", + "parameters": [ + { + "name": "height", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_cpfp", + "title": "CPFP info", + "description": "Returns ancestors and descendants for a CPFP (Child Pays For Parent) transaction, including the effective fee rate of the package.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)*\n\nREST operation: `GET /api/v1/cpfp/{txid}`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/CpfpInfo", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "CpfpCluster": { + "required": [ + "txs", + "chunks", + "chunkIndex" + ], + "type": "object", + "properties": { + "txs": { + "items": { + "$ref": "#/$defs/CpfpClusterTx" + }, + "type": "array" + }, + "chunks": { + "items": { + "$ref": "#/$defs/CpfpClusterChunk" + }, + "type": "array" + }, + "chunkIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "CpfpClusterChunk": { + "required": [ + "txs", + "feerate" + ], + "type": "object", + "properties": { + "txs": { + "type": "array", + "items": { + "$ref": "#/$defs/CpfpClusterTxIndex" + } + }, + "feerate": { + "$ref": "#/$defs/FeeRate" + } + } + }, + "CpfpClusterTx": { + "required": [ + "txid", + "weight", + "fee", + "parents" + ], + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + }, + "weight": { + "$ref": "#/$defs/Weight" + }, + "fee": { + "$ref": "#/$defs/Sats" + }, + "parents": { + "items": { + "$ref": "#/$defs/CpfpClusterTxIndex" + }, + "type": "array" + } + } + }, + "CpfpClusterTxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "CpfpEntry": { + "required": [ + "txid", + "weight", + "fee" + ], + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + }, + "weight": { + "$ref": "#/$defs/Weight" + }, + "fee": { + "$ref": "#/$defs/Sats" + } + } + }, + "CpfpInfo": { + "required": [ + "ancestors", + "descendants", + "effectiveFeePerVsize", + "sigops", + "fee", + "vsize", + "adjustedVsize" + ], + "type": "object", + "properties": { + "ancestors": { + "items": { + "$ref": "#/$defs/CpfpEntry" + }, + "type": "array" + }, + "bestDescendant": { + "anyOf": [ + { + "$ref": "#/$defs/CpfpEntry" + }, + { + "type": "null" + } + ] + }, + "descendants": { + "items": { + "$ref": "#/$defs/CpfpEntry" + }, + "type": "array" + }, + "effectiveFeePerVsize": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "vsize": { + "allOf": [ + { + "$ref": "#/$defs/VSize" + } + ] + }, + "adjustedVsize": { + "allOf": [ + { + "$ref": "#/$defs/VSize" + } + ] + }, + "cluster": { + "anyOf": [ + { + "$ref": "#/$defs/CpfpCluster" + }, + { + "type": "null" + } + ] + } + } + }, + "FeeRate": { + "format": "double", + "type": "number" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Txid": { + "type": "string" + }, + "VSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/cpfp/{txid}", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_difficulty_adjustment", + "title": "Difficulty adjustment", + "description": "Get current difficulty adjustment progress and estimates.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustment)*\n\nREST operation: `GET /api/v1/difficulty-adjustment`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/DifficultyAdjustment", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "DifficultyAdjustment": { + "required": [ + "progressPercent", + "difficultyChange", + "estimatedRetargetDate", + "remainingBlocks", + "remainingTime", + "previousRetarget", + "previousTime", + "nextRetargetHeight", + "timeAvg", + "adjustedTimeAvg", + "timeOffset", + "expectedBlocks" + ], + "type": "object", + "properties": { + "progressPercent": { + "format": "double", + "type": "number" + }, + "difficultyChange": { + "format": "double", + "type": "number" + }, + "estimatedRetargetDate": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "remainingBlocks": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "remainingTime": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "previousRetarget": { + "format": "double", + "type": "number" + }, + "previousTime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "nextRetargetHeight": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "timeAvg": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "adjustedTimeAvg": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "timeOffset": { + "format": "int64", + "type": "integer" + }, + "expectedBlocks": { + "format": "double", + "type": "number" + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/difficulty-adjustment", + "parameters": [] + } + }, + { + "tool": { + "name": "get_difficulty_adjustments", + "title": "Difficulty adjustments (all time)", + "description": "Get historical difficulty adjustments including timestamp, block height, difficulty value, and percentage change.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)*\n\nREST operation: `GET /api/v1/mining/difficulty-adjustments`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/DifficultyAdjustmentEntry" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "DifficultyAdjustmentEntry": { + "required": [ + "timestamp", + "height", + "difficulty", + "change_percent" + ], + "type": "object", + "properties": { + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "change_percent": { + "format": "double", + "type": "number" + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/difficulty-adjustments", + "parameters": [] + } + }, + { + "tool": { + "name": "get_difficulty_adjustments_by_period", + "title": "Difficulty adjustments", + "description": "Get historical difficulty adjustments for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)*\n\nREST operation: `GET /api/v1/mining/difficulty-adjustments/{time_period}`.", + "inputSchema": { + "type": "object", + "properties": { + "time_period": { + "$ref": "#/$defs/TimePeriod" + } + }, + "additionalProperties": false, + "required": [ + "time_period" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TimePeriod": { + "description": "Time period for mining statistics.\n\nUsed to specify the lookback window for pool statistics, hashrate calculations,\nand other time-based mining series.", + "type": "string", + "enum": [ + "24h", + "3d", + "1w", + "1m", + "3m", + "6m", + "1y", + "2y", + "3y", + "all" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/DifficultyAdjustmentEntry" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "DifficultyAdjustmentEntry": { + "required": [ + "timestamp", + "height", + "difficulty", + "change_percent" + ], + "type": "object", + "properties": { + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "change_percent": { + "format": "double", + "type": "number" + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/difficulty-adjustments/{time_period}", + "parameters": [ + { + "name": "time_period", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_disk_usage", + "title": "Disk usage", + "description": "Returns the disk space used by BRK and Bitcoin data.\n\nREST operation: `GET /api/server/disk`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/DiskUsage", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "DiskUsage": { + "required": [ + "brk", + "brk_bytes", + "bitcoin", + "bitcoin_bytes", + "ratio" + ], + "type": "object", + "properties": { + "brk": { + "type": "string" + }, + "brk_bytes": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "bitcoin": { + "type": "string" + }, + "bitcoin_bytes": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "ratio": { + "format": "double", + "type": "number" + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/server/disk", + "parameters": [] + } + }, + { + "tool": { + "name": "get_fullrbf_replacements", + "title": "Recent full-RBF replacements", + "description": "Same response shape as `GET /api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)*\n\nREST operation: `GET /api/v1/fullrbf/replacements`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/ReplacementNode" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "FeeRate": { + "format": "double", + "type": "number" + }, + "RbfTx": { + "required": [ + "txid", + "fee", + "vsize", + "value", + "rate", + "time", + "rbf" + ], + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + }, + "fee": { + "$ref": "#/$defs/Sats" + }, + "vsize": { + "$ref": "#/$defs/VSize" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "rate": { + "$ref": "#/$defs/FeeRate" + }, + "time": { + "$ref": "#/$defs/Timestamp" + }, + "rbf": { + "type": "boolean" + }, + "fullRbf": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "ReplacementNode": { + "required": [ + "tx", + "time", + "fullRbf", + "replaces" + ], + "type": "object", + "properties": { + "tx": { + "$ref": "#/$defs/RbfTx" + }, + "time": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "fullRbf": { + "type": "boolean" + }, + "interval": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint32" + }, + "mined": { + "type": [ + "boolean", + "null" + ] + }, + "replaces": { + "type": "array", + "items": { + "$ref": "#/$defs/ReplacementNode" + } + } + } + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Txid": { + "type": "string" + }, + "VSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/fullrbf/replacements", + "parameters": [] + } + }, + { + "tool": { + "name": "get_hashrate", + "title": "Network hashrate (all time)", + "description": "Get network hashrate and difficulty data for all time.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)*\n\nREST operation: `GET /api/v1/mining/hashrate`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/HashrateSummary", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "DifficultyEntry": { + "required": [ + "time", + "height", + "difficulty", + "adjustment" + ], + "type": "object", + "properties": { + "time": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "adjustment": { + "format": "double", + "type": "number" + } + } + }, + "HashrateEntry": { + "required": [ + "timestamp", + "avgHashrate" + ], + "type": "object", + "properties": { + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + } + } + }, + "HashrateSummary": { + "required": [ + "hashrates", + "difficulty", + "currentHashrate", + "currentDifficulty" + ], + "type": "object", + "properties": { + "hashrates": { + "items": { + "$ref": "#/$defs/HashrateEntry" + }, + "type": "array" + }, + "difficulty": { + "items": { + "$ref": "#/$defs/DifficultyEntry" + }, + "type": "array" + }, + "currentHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + }, + "currentDifficulty": { + "format": "double", + "type": "number" + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/hashrate", + "parameters": [] + } + }, + { + "tool": { + "name": "get_hashrate_by_period", + "title": "Network hashrate", + "description": "Get network hashrate and difficulty data for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)*\n\nREST operation: `GET /api/v1/mining/hashrate/{time_period}`.", + "inputSchema": { + "type": "object", + "properties": { + "time_period": { + "$ref": "#/$defs/TimePeriod" + } + }, + "additionalProperties": false, + "required": [ + "time_period" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TimePeriod": { + "description": "Time period for mining statistics.\n\nUsed to specify the lookback window for pool statistics, hashrate calculations,\nand other time-based mining series.", + "type": "string", + "enum": [ + "24h", + "3d", + "1w", + "1m", + "3m", + "6m", + "1y", + "2y", + "3y", + "all" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/HashrateSummary", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "DifficultyEntry": { + "required": [ + "time", + "height", + "difficulty", + "adjustment" + ], + "type": "object", + "properties": { + "time": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "adjustment": { + "format": "double", + "type": "number" + } + } + }, + "HashrateEntry": { + "required": [ + "timestamp", + "avgHashrate" + ], + "type": "object", + "properties": { + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + } + } + }, + "HashrateSummary": { + "required": [ + "hashrates", + "difficulty", + "currentHashrate", + "currentDifficulty" + ], + "type": "object", + "properties": { + "hashrates": { + "items": { + "$ref": "#/$defs/HashrateEntry" + }, + "type": "array" + }, + "difficulty": { + "items": { + "$ref": "#/$defs/DifficultyEntry" + }, + "type": "array" + }, + "currentHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + }, + "currentDifficulty": { + "format": "double", + "type": "number" + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/hashrate/{time_period}", + "parameters": [ + { + "name": "time_period", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_health", + "title": "Health check", + "description": "Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, request `GET /api/server/sync`.\n\nREST operation: `GET /health`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/Health", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Health": { + "required": [ + "status", + "service", + "version", + "timestamp", + "started_at", + "uptime_seconds", + "indexed_height", + "computed_height", + "tip_height", + "blocks_behind", + "last_indexed_at", + "last_indexed_at_unix" + ], + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "service": { + "type": "string" + }, + "version": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "uptime_seconds": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "indexed_height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "computed_height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "tip_height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "blocks_behind": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "last_indexed_at": { + "type": "string" + }, + "last_indexed_at_unix": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + } + } + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/health", + "parameters": [] + } + }, + { + "tool": { + "name": "get_historical_price", + "title": "Historical price", + "description": "Get historical BTC/USD price. Optionally specify a UNIX timestamp to get the price at that time.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-historical-price)*\n\nREST operation: `GET /api/v1/historical-price`.", + "inputSchema": { + "type": "object", + "properties": { + "timestamp": { + "$ref": "#/$defs/Timestamp" + } + }, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Timestamp": { + "description": "UNIX timestamp in seconds", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 1231006505, + 1672531200, + 1713571200, + 1743631892, + 1759000868 + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/HistoricalPrice", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Dollars": { + "format": "double", + "type": "number" + }, + "ExchangeRates": { + "type": "object" + }, + "HistoricalPrice": { + "required": [ + "prices", + "exchangeRates" + ], + "type": "object", + "properties": { + "prices": { + "items": { + "$ref": "#/$defs/HistoricalPriceEntry" + }, + "type": "array" + }, + "exchangeRates": { + "allOf": [ + { + "$ref": "#/$defs/ExchangeRates" + } + ] + } + } + }, + "HistoricalPriceEntry": { + "required": [ + "time", + "USD" + ], + "type": "object", + "properties": { + "time": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "USD": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/historical-price", + "parameters": [ + { + "name": "timestamp", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "get_indexes", + "title": "List available indexes", + "description": "Returns all available indexes with their accepted query aliases. Use any alias when querying series.\n\nREST operation: `GET /api/series/indexes`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/IndexInfo" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Index": { + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "type": "string" + }, + "IndexInfo": { + "required": [ + "index", + "aliases" + ], + "type": "object", + "properties": { + "index": { + "allOf": [ + { + "$ref": "#/$defs/Index" + } + ] + }, + "aliases": { + "items": { + "type": "string" + }, + "type": "array" + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/indexes", + "parameters": [] + } + }, + { + "tool": { + "name": "get_live_price", + "title": "Live BTC/USD price", + "description": "Returns the current BTC/USD price in dollars, derived from on-chain round-dollar output patterns in the last 12 blocks plus mempool.\n\nREST operation: `GET /api/mempool/price`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/Dollars", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Dollars": { + "format": "double", + "type": "number" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/mempool/price", + "parameters": [] + } + }, + { + "tool": { + "name": "get_mempool", + "title": "Mempool statistics", + "description": "Get current mempool statistics including transaction count, total vsize, total fees, and fee histogram.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool)*\n\nREST operation: `GET /api/mempool`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/MempoolInfo", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "MempoolInfo": { + "required": [ + "count", + "vsize", + "total_fee", + "fee_histogram" + ], + "type": "object", + "properties": { + "count": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "vsize": { + "allOf": [ + { + "$ref": "#/$defs/VSize" + } + ] + }, + "total_fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "fee_histogram": { + "additionalProperties": { + "$ref": "#/$defs/VSize" + }, + "type": "object" + } + } + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "VSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/mempool", + "parameters": [] + } + }, + { + "tool": { + "name": "get_mempool_blocks", + "title": "Projected mempool blocks", + "description": "Projected blocks for fee estimation. Block 0 reflects Bitcoin Core's actual next-block selection; blocks 1+ are a fee-tier approximation.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*\n\nREST operation: `GET /api/v1/fees/mempool-blocks`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/MempoolBlock" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "FeeRate": { + "format": "double", + "type": "number" + }, + "MempoolBlock": { + "required": [ + "blockSize", + "blockVSize", + "nTx", + "totalFees", + "medianFee", + "feeRange" + ], + "type": "object", + "properties": { + "blockSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "blockVSize": { + "format": "double", + "type": "number" + }, + "nTx": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "totalFees": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "feeRange": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/FeeRate" + }, + "minItems": 7 + } + } + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/fees/mempool-blocks", + "parameters": [] + } + }, + { + "tool": { + "name": "get_mempool_hash", + "title": "Mempool content hash", + "description": "Returns an opaque hash that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled.\n\nREST operation: `GET /api/mempool/hash`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/NextBlockHash", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "NextBlockHash": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/mempool/hash", + "parameters": [] + } + }, + { + "tool": { + "name": "get_mempool_recent", + "title": "Recent mempool transactions", + "description": "Get the last 10 transactions to enter the mempool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-recent)*\n\nREST operation: `GET /api/mempool/recent`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/MempoolRecentTx" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "MempoolRecentTx": { + "required": [ + "txid", + "fee", + "vsize", + "value" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "vsize": { + "allOf": [ + { + "$ref": "#/$defs/VSize" + } + ] + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Txid": { + "type": "string" + }, + "VSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/mempool/recent", + "parameters": [] + } + }, + { + "tool": { + "name": "get_mempool_txids", + "title": "Mempool transaction IDs", + "description": "Get all transaction IDs currently in the mempool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-transaction-ids)*\n\nREST operation: `GET /api/mempool/txids`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Txid" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "type": "string" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/mempool/txids", + "parameters": [] + } + }, + { + "tool": { + "name": "get_openapi", + "title": "OpenAPI specification", + "description": "Full OpenAPI 3.1 specification for this API.\n\nREST operation: `GET /openapi.json`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/openapi.json", + "parameters": [] + } + }, + { + "tool": { + "name": "get_oracle_histogram_outputs", + "title": "Output value histogram at height or day", + "description": "Unfiltered output value histogram for a confirmed point. A block height (`840000`) gives every output in that block, coinbase included, binned by value on the oracle log scale; a calendar date (`YYYY-MM-DD`) sums every block that day. A flat array of log-scale bins.\n\nREST operation: `GET /api/oracle/histogram/outputs/{point}`.", + "inputSchema": { + "type": "object", + "properties": { + "point": { + "type": "string", + "description": "Confirmed block height as decimal digits (`840000`) or calendar date in\n`YYYY-MM-DD` format.", + "examples": [ + "840000" + ] + } + }, + "additionalProperties": false, + "required": [ + "point" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "type": "integer", + "minimum": 0, + "format": "uint32" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/oracle/histogram/outputs/{point}", + "parameters": [ + { + "name": "point", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_oracle_histogram_outputs_live", + "title": "Live output value histogram", + "description": "Live unfiltered output value histogram for the forming mempool block. Every live output is binned by value on the oracle log scale; no oracle payment filters are applied. A flat array of log-scale bins, all zero when no mempool is configured.\n\nREST operation: `GET /api/oracle/histogram/outputs/live`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "type": "integer", + "minimum": 0, + "format": "uint32" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/oracle/histogram/outputs/live", + "parameters": [] + } + }, + { + "tool": { + "name": "get_oracle_histogram_payments", + "title": "Payment output histogram at height or day", + "description": "Smoothed histogram of oracle-eligible payment outputs for a confirmed point. A block height (`840000`) gives that block's oracle payment histogram; a calendar date (`YYYY-MM-DD`) gives the average of that day's per-block payment histograms. A flat array of log-scale bins.\n\nREST operation: `GET /api/oracle/histogram/payments/{point}`.", + "inputSchema": { + "type": "object", + "properties": { + "point": { + "type": "string", + "description": "Confirmed block height as decimal digits (`840000`) or calendar date in\n`YYYY-MM-DD` format.", + "examples": [ + "840000" + ] + } + }, + "additionalProperties": false, + "required": [ + "point" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "type": "integer", + "maximum": 65535, + "minimum": 0, + "format": "uint16" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/oracle/histogram/payments/{point}", + "parameters": [ + { + "name": "point", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_oracle_histogram_payments_live", + "title": "Live payment output histogram", + "description": "Live smoothed histogram of oracle-eligible payment outputs, binned by output value on the oracle log scale. It combines the committed oracle window with the forming mempool block. A flat array of log-scale bins.\n\nREST operation: `GET /api/oracle/histogram/payments/live`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "type": "integer", + "maximum": 65535, + "minimum": 0, + "format": "uint16" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/oracle/histogram/payments/live", + "parameters": [] + } + }, + { + "tool": { + "name": "get_oracle_price", + "title": "Live BTC/USD price", + "description": "Current BTC/USD price in dollars. Same value as `GET /api/mempool/price`. Confirmed per-height history is available at `GET /api/series/price/height`.\n\nREST operation: `GET /api/oracle/price`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/Dollars", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Dollars": { + "format": "double", + "type": "number" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/oracle/price", + "parameters": [] + } + }, + { + "tool": { + "name": "get_pool", + "title": "Mining pool details", + "description": "Get detailed information about a specific mining pool including block counts and shares for different time periods.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool)*\n\nREST operation: `GET /api/v1/mining/pool/{slug}`.", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "$ref": "#/$defs/PoolSlug" + } + }, + "additionalProperties": false, + "required": [ + "slug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolSlug": { + "description": "URL-friendly mining pool identifier", + "type": "string", + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/PoolDetail", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolBlockCounts": { + "required": [ + "all", + "24h", + "1w" + ], + "type": "object", + "properties": { + "all": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "24h": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "1w": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "PoolBlockShares": { + "required": [ + "all", + "24h", + "1w" + ], + "type": "object", + "properties": { + "all": { + "format": "double", + "type": "number" + }, + "24h": { + "format": "double", + "type": "number" + }, + "1w": { + "format": "double", + "type": "number" + } + } + }, + "PoolDetail": { + "required": [ + "pool", + "blockCount", + "blockShare", + "estimatedHashrate" + ], + "type": "object", + "properties": { + "pool": { + "allOf": [ + { + "$ref": "#/$defs/PoolDetailInfo" + } + ] + }, + "blockCount": { + "allOf": [ + { + "$ref": "#/$defs/PoolBlockCounts" + } + ] + }, + "blockShare": { + "allOf": [ + { + "$ref": "#/$defs/PoolBlockShares" + } + ] + }, + "estimatedHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + }, + "reportedHashrate": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint128" + }, + "totalReward": { + "anyOf": [ + { + "$ref": "#/$defs/Sats" + }, + { + "type": "null" + } + ] + } + } + }, + "PoolDetailInfo": { + "required": [ + "id", + "name", + "link", + "addresses", + "regexes", + "slug", + "unique_id" + ], + "type": "object", + "properties": { + "id": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "link": { + "type": "string" + }, + "addresses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "regexes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "slug": { + "allOf": [ + { + "$ref": "#/$defs/PoolSlug" + } + ] + }, + "unique_id": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "PoolSlug": { + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ], + "type": "string" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/pool/{slug}", + "parameters": [ + { + "name": "slug", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_pool_blocks", + "title": "Mining pool blocks", + "description": "Get the 10 most recent blocks mined by a specific pool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-blocks)*\n\nREST operation: `GET /api/v1/mining/pool/{slug}/blocks`.", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "$ref": "#/$defs/PoolSlug" + } + }, + "additionalProperties": false, + "required": [ + "slug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolSlug": { + "description": "URL-friendly mining pool identifier", + "type": "string", + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockInfoV1" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockExtras": { + "required": [ + "totalFees", + "medianFee", + "feeRange", + "reward", + "pool", + "avgFee", + "avgFeeRate", + "coinbaseRaw", + "coinbaseAddresses", + "coinbaseSignature", + "coinbaseSignatureAscii", + "avgTxSize", + "totalInputs", + "totalOutputs", + "totalOutputAmt", + "medianFeeAmt", + "feePercentiles", + "segwitTotalTxs", + "segwitTotalSize", + "segwitTotalWeight", + "header", + "utxoSetChange", + "utxoSetSize", + "totalInputAmt", + "virtualSize", + "orphans", + "price" + ], + "type": "object", + "properties": { + "totalFees": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "feeRange": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/FeeRate" + }, + "minItems": 7 + }, + "reward": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "pool": { + "allOf": [ + { + "$ref": "#/$defs/BlockPool" + } + ] + }, + "avgFee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "avgFeeRate": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "coinbaseRaw": { + "type": "string" + }, + "coinbaseAddress": { + "type": [ + "string", + "null" + ] + }, + "coinbaseAddresses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "coinbaseSignature": { + "type": "string" + }, + "coinbaseSignatureAscii": { + "type": "string" + }, + "avgTxSize": { + "format": "double", + "type": "number" + }, + "totalInputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFeeAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "feePercentiles": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/Sats" + }, + "minItems": 7 + }, + "segwitTotalTxs": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "segwitTotalSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "segwitTotalWeight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "header": { + "type": "string" + }, + "utxoSetChange": { + "format": "int64", + "type": "integer" + }, + "utxoSetSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalInputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "virtualSize": { + "format": "double", + "type": "number" + }, + "firstSeen": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint64" + }, + "orphans": { + "items": { + "type": "string" + }, + "type": "array" + }, + "price": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "BlockHash": { + "type": "string" + }, + "BlockInfoV1": { + "required": [ + "id", + "height", + "version", + "timestamp", + "bits", + "nonce", + "difficulty", + "merkle_root", + "tx_count", + "size", + "weight", + "previousblockhash", + "mediantime", + "extras" + ], + "type": "object", + "properties": { + "id": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "bits": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "nonce": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "merkle_root": { + "type": "string" + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "previousblockhash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "mediantime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "stale": { + "type": "boolean" + }, + "extras": { + "allOf": [ + { + "$ref": "#/$defs/BlockExtras" + } + ] + } + } + }, + "BlockPool": { + "required": [ + "id", + "name", + "slug", + "blockNumber" + ], + "type": "object", + "properties": { + "id": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "slug": { + "allOf": [ + { + "$ref": "#/$defs/PoolSlug" + } + ] + }, + "blockNumber": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "minerNames": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + } + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "FeeRate": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "PoolSlug": { + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ], + "type": "string" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/pool/{slug}/blocks", + "parameters": [ + { + "name": "slug", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_pool_blocks_from", + "title": "Mining pool blocks from height", + "description": "Get 10 blocks mined by a specific pool before (and including) the given height.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-blocks)*\n\nREST operation: `GET /api/v1/mining/pool/{slug}/blocks/{height}`.", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "$ref": "#/$defs/PoolSlug" + }, + "height": { + "$ref": "#/$defs/Height" + } + }, + "additionalProperties": false, + "required": [ + "slug", + "height" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Height": { + "description": "Block height", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 0, + 210000, + 420000, + 840000 + ] + }, + "PoolSlug": { + "description": "URL-friendly mining pool identifier", + "type": "string", + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/BlockInfoV1" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockExtras": { + "required": [ + "totalFees", + "medianFee", + "feeRange", + "reward", + "pool", + "avgFee", + "avgFeeRate", + "coinbaseRaw", + "coinbaseAddresses", + "coinbaseSignature", + "coinbaseSignatureAscii", + "avgTxSize", + "totalInputs", + "totalOutputs", + "totalOutputAmt", + "medianFeeAmt", + "feePercentiles", + "segwitTotalTxs", + "segwitTotalSize", + "segwitTotalWeight", + "header", + "utxoSetChange", + "utxoSetSize", + "totalInputAmt", + "virtualSize", + "orphans", + "price" + ], + "type": "object", + "properties": { + "totalFees": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "feeRange": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/FeeRate" + }, + "minItems": 7 + }, + "reward": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "pool": { + "allOf": [ + { + "$ref": "#/$defs/BlockPool" + } + ] + }, + "avgFee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "avgFeeRate": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "coinbaseRaw": { + "type": "string" + }, + "coinbaseAddress": { + "type": [ + "string", + "null" + ] + }, + "coinbaseAddresses": { + "items": { + "type": "string" + }, + "type": "array" + }, + "coinbaseSignature": { + "type": "string" + }, + "coinbaseSignatureAscii": { + "type": "string" + }, + "avgTxSize": { + "format": "double", + "type": "number" + }, + "totalInputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputs": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalOutputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "medianFeeAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "feePercentiles": { + "maxItems": 7, + "type": "array", + "items": { + "$ref": "#/$defs/Sats" + }, + "minItems": 7 + }, + "segwitTotalTxs": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "segwitTotalSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "segwitTotalWeight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "header": { + "type": "string" + }, + "utxoSetChange": { + "format": "int64", + "type": "integer" + }, + "utxoSetSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "totalInputAmt": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "virtualSize": { + "format": "double", + "type": "number" + }, + "firstSeen": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint64" + }, + "orphans": { + "items": { + "type": "string" + }, + "type": "array" + }, + "price": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "BlockHash": { + "type": "string" + }, + "BlockInfoV1": { + "required": [ + "id", + "height", + "version", + "timestamp", + "bits", + "nonce", + "difficulty", + "merkle_root", + "tx_count", + "size", + "weight", + "previousblockhash", + "mediantime", + "extras" + ], + "type": "object", + "properties": { + "id": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "bits": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "nonce": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "difficulty": { + "format": "double", + "type": "number" + }, + "merkle_root": { + "type": "string" + }, + "tx_count": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "previousblockhash": { + "allOf": [ + { + "$ref": "#/$defs/BlockHash" + } + ] + }, + "mediantime": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "stale": { + "type": "boolean" + }, + "extras": { + "allOf": [ + { + "$ref": "#/$defs/BlockExtras" + } + ] + } + } + }, + "BlockPool": { + "required": [ + "id", + "name", + "slug", + "blockNumber" + ], + "type": "object", + "properties": { + "id": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "slug": { + "allOf": [ + { + "$ref": "#/$defs/PoolSlug" + } + ] + }, + "blockNumber": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "minerNames": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + } + } + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "FeeRate": { + "format": "double", + "type": "number" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "PoolSlug": { + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ], + "type": "string" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/pool/{slug}/blocks/{height}", + "parameters": [ + { + "name": "slug", + "location": "path" + }, + { + "name": "height", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_pool_hashrate", + "title": "Mining pool hashrate", + "description": "Get hashrate history for a specific mining pool.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrate)*\n\nREST operation: `GET /api/v1/mining/pool/{slug}/hashrate`.", + "inputSchema": { + "type": "object", + "properties": { + "slug": { + "$ref": "#/$defs/PoolSlug" + } + }, + "additionalProperties": false, + "required": [ + "slug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolSlug": { + "description": "URL-friendly mining pool identifier", + "type": "string", + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/PoolHashrateEntry" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolHashrateEntry": { + "required": [ + "timestamp", + "avgHashrate", + "share", + "poolName" + ], + "type": "object", + "properties": { + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + }, + "share": { + "format": "double", + "type": "number" + }, + "poolName": { + "type": "string" + } + } + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/pool/{slug}/hashrate", + "parameters": [ + { + "name": "slug", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_pool_stats", + "title": "Mining pool statistics", + "description": "Get mining pool statistics for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)*\n\nREST operation: `GET /api/v1/mining/pools/{time_period}`.", + "inputSchema": { + "type": "object", + "properties": { + "time_period": { + "$ref": "#/$defs/TimePeriod" + } + }, + "additionalProperties": false, + "required": [ + "time_period" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TimePeriod": { + "description": "Time period for mining statistics.\n\nUsed to specify the lookback window for pool statistics, hashrate calculations,\nand other time-based mining series.", + "type": "string", + "enum": [ + "24h", + "3d", + "1w", + "1m", + "3m", + "6m", + "1y", + "2y", + "3y", + "all" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/PoolsSummary", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolSlug": { + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ], + "type": "string" + }, + "PoolStats": { + "required": [ + "poolId", + "name", + "link", + "blockCount", + "rank", + "emptyBlocks", + "slug", + "share", + "poolUniqueId" + ], + "type": "object", + "properties": { + "poolId": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "name": { + "type": "string" + }, + "link": { + "type": "string" + }, + "blockCount": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "rank": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "emptyBlocks": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "slug": { + "allOf": [ + { + "$ref": "#/$defs/PoolSlug" + } + ] + }, + "share": { + "format": "double", + "type": "number" + }, + "poolUniqueId": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "PoolsSummary": { + "required": [ + "pools", + "blockCount", + "lastEstimatedHashrate", + "lastEstimatedHashrate3d", + "lastEstimatedHashrate1w" + ], + "type": "object", + "properties": { + "pools": { + "items": { + "$ref": "#/$defs/PoolStats" + }, + "type": "array" + }, + "blockCount": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "lastEstimatedHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + }, + "lastEstimatedHashrate3d": { + "minimum": 0, + "type": "integer", + "format": "uint128" + }, + "lastEstimatedHashrate1w": { + "minimum": 0, + "type": "integer", + "format": "uint128" + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/pools/{time_period}", + "parameters": [ + { + "name": "time_period", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_pools", + "title": "List all mining pools", + "description": "Get list of all known mining pools with their identifiers.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)*\n\nREST operation: `GET /api/v1/mining/pools`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/PoolInfo" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolInfo": { + "required": [ + "name", + "slug", + "unique_id" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "allOf": [ + { + "$ref": "#/$defs/PoolSlug" + } + ] + }, + "unique_id": { + "maximum": 255, + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "PoolSlug": { + "enum": [ + "unknown", + "blockfills", + "ultimuspool", + "terrapool", + "luxor", + "1thash", + "btccom", + "bitfarms", + "huobipool", + "wayicn", + "canoepool", + "btctop", + "bitcoincom", + "175btc", + "gbminers", + "axbt", + "asicminer", + "bitminter", + "bitcoinrussia", + "btcserv", + "simplecoinus", + "btcguild", + "eligius", + "ozcoin", + "eclipsemc", + "maxbtc", + "triplemining", + "coinlab", + "50btc", + "ghashio", + "stminingcorp", + "bitparking", + "mmpool", + "polmine", + "kncminer", + "bitalo", + "f2pool", + "hhtt", + "megabigpower", + "mtred", + "nmcbit", + "yourbtcnet", + "givemecoins", + "braiinspool", + "antpool", + "multicoinco", + "bcpoolio", + "cointerra", + "kanopool", + "solock", + "ckpool", + "nicehash", + "bitclub", + "bitcoinaffiliatenetwork", + "btcc", + "bwpool", + "exxbw", + "bitsolo", + "bitfury", + "21inc", + "digitalbtc", + "8baochi", + "mybtccoinpool", + "tbdice", + "hashpool", + "nexious", + "bravomining", + "hotpool", + "okexpool", + "bcmonster", + "1hash", + "bixin", + "tatmaspool", + "viabtc", + "connectbtc", + "batpool", + "waterhole", + "dcexploration", + "dcex", + "btpool", + "58coin", + "bitcoinindia", + "shawnp0wers", + "phashio", + "rigpool", + "haozhuzhu", + "7pool", + "miningkings", + "hashbx", + "dpool", + "rawpool", + "haominer", + "helix", + "bitcoinukraine", + "poolin", + "secretsuperstar", + "tigerpoolnet", + "sigmapoolcom", + "okpooltop", + "hummerpool", + "tangpool", + "bytepool", + "spiderpool", + "novablock", + "miningcity", + "binancepool", + "minerium", + "lubiancom", + "okkong", + "aaopool", + "emcdpool", + "foundryusa", + "sbicrypto", + "arkpool", + "purebtccom", + "marapool", + "kucoinpool", + "entrustcharitypool", + "okminer", + "titan", + "pegapool", + "btcnuggets", + "cloudhashing", + "digitalxmintsy", + "telco214", + "btcpoolparty", + "multipool", + "transactioncoinmining", + "btcdig", + "trickysbtcpool", + "btcmp", + "eobot", + "unomp", + "patels", + "gogreenlight", + "bitcoinindiapool", + "ekanembtc", + "canoe", + "tiger", + "1m1x", + "zulupool", + "secpool", + "ocean", + "whitepool", + "wiz", + "wk057", + "futurebitapollosolo", + "carbonnegative", + "portlandhodl", + "phoenix", + "neopool", + "maxipool", + "bitfufupool", + "gdpool", + "miningdutch", + "publicpool", + "miningsquared", + "innopolistech", + "btclab", + "parasite", + "redrockpool", + "est3lar", + "braiinssolo", + "solopoolcom", + "noderunners", + "dmnd" + ], + "type": "string" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/pools", + "parameters": [] + } + }, + { + "tool": { + "name": "get_pools_hashrate", + "title": "All pools hashrate (all time)", + "description": "Get hashrate data for all mining pools.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)*\n\nREST operation: `GET /api/v1/mining/hashrate/pools`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/PoolHashrateEntry" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolHashrateEntry": { + "required": [ + "timestamp", + "avgHashrate", + "share", + "poolName" + ], + "type": "object", + "properties": { + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + }, + "share": { + "format": "double", + "type": "number" + }, + "poolName": { + "type": "string" + } + } + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/hashrate/pools", + "parameters": [] + } + }, + { + "tool": { + "name": "get_pools_hashrate_by_period", + "title": "All pools hashrate", + "description": "Get hashrate data for all mining pools for a time period. Valid periods: `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)*\n\nREST operation: `GET /api/v1/mining/hashrate/pools/{time_period}`.", + "inputSchema": { + "type": "object", + "properties": { + "time_period": { + "$ref": "#/$defs/TimePeriod" + } + }, + "additionalProperties": false, + "required": [ + "time_period" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TimePeriod": { + "description": "Time period for mining statistics.\n\nUsed to specify the lookback window for pool statistics, hashrate calculations,\nand other time-based mining series.", + "type": "string", + "enum": [ + "24h", + "3d", + "1w", + "1m", + "3m", + "6m", + "1y", + "2y", + "3y", + "all" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/PoolHashrateEntry" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PoolHashrateEntry": { + "required": [ + "timestamp", + "avgHashrate", + "share", + "poolName" + ], + "type": "object", + "properties": { + "timestamp": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "avgHashrate": { + "minimum": 0, + "type": "integer", + "format": "uint128" + }, + "share": { + "format": "double", + "type": "number" + }, + "poolName": { + "type": "string" + } + } + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/hashrate/pools/{time_period}", + "parameters": [ + { + "name": "time_period", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_precise_fees", + "title": "Precise recommended fees", + "description": "Recommended fee rates with sub-integer precision.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)*\n\nREST operation: `GET /api/v1/fees/precise`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/RecommendedFees", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "FeeRate": { + "format": "double", + "type": "number" + }, + "RecommendedFees": { + "required": [ + "fastestFee", + "halfHourFee", + "hourFee", + "economyFee", + "minimumFee" + ], + "type": "object", + "properties": { + "fastestFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "halfHourFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "hourFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "economyFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "minimumFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/fees/precise", + "parameters": [] + } + }, + { + "tool": { + "name": "get_prices", + "title": "Current BTC price", + "description": "Returns bitcoin latest price (on-chain derived, USD only).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-price)*\n\nREST operation: `GET /api/v1/prices`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/Prices", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Dollars": { + "format": "double", + "type": "number" + }, + "Prices": { + "required": [ + "time", + "USD" + ], + "type": "object", + "properties": { + "time": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "USD": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/prices", + "parameters": [] + } + }, + { + "tool": { + "name": "get_recommended_fees", + "title": "Recommended fees", + "description": "Recommended fee rates by confirmation target.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*\n\nREST operation: `GET /api/v1/fees/recommended`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/RecommendedFees", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "FeeRate": { + "format": "double", + "type": "number" + }, + "RecommendedFees": { + "required": [ + "fastestFee", + "halfHourFee", + "hourFee", + "economyFee", + "minimumFee" + ], + "type": "object", + "properties": { + "fastestFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "halfHourFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "hourFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "economyFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + }, + "minimumFee": { + "allOf": [ + { + "$ref": "#/$defs/FeeRate" + } + ] + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/fees/recommended", + "parameters": [] + } + }, + { + "tool": { + "name": "get_replacements", + "title": "Recent RBF replacements", + "description": "Returns up to 25 most-recent RBF replacement trees across the whole mempool. Each entry has the same shape as `tx_rbf().replacements`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-replacements)*\n\nREST operation: `GET /api/v1/replacements`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/ReplacementNode" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "FeeRate": { + "format": "double", + "type": "number" + }, + "RbfTx": { + "required": [ + "txid", + "fee", + "vsize", + "value", + "rate", + "time", + "rbf" + ], + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + }, + "fee": { + "$ref": "#/$defs/Sats" + }, + "vsize": { + "$ref": "#/$defs/VSize" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "rate": { + "$ref": "#/$defs/FeeRate" + }, + "time": { + "$ref": "#/$defs/Timestamp" + }, + "rbf": { + "type": "boolean" + }, + "fullRbf": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "ReplacementNode": { + "required": [ + "tx", + "time", + "fullRbf", + "replaces" + ], + "type": "object", + "properties": { + "tx": { + "$ref": "#/$defs/RbfTx" + }, + "time": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "fullRbf": { + "type": "boolean" + }, + "interval": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint32" + }, + "mined": { + "type": [ + "boolean", + "null" + ] + }, + "replaces": { + "type": "array", + "items": { + "$ref": "#/$defs/ReplacementNode" + } + } + } + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Txid": { + "type": "string" + }, + "VSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/replacements", + "parameters": [] + } + }, + { + "tool": { + "name": "get_reward_stats", + "title": "Mining reward statistics", + "description": "Get mining reward statistics for the last N blocks including total rewards, fees, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-reward-stats)*\n\nREST operation: `GET /api/v1/mining/reward-stats/{block_count}`.", + "inputSchema": { + "type": "object", + "properties": { + "block_count": { + "type": "integer", + "minimum": 0, + "format": "uint", + "description": "Number of recent blocks to include", + "examples": [ + 100 + ] + } + }, + "additionalProperties": false, + "required": [ + "block_count" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/RewardStats", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "RewardStats": { + "required": [ + "startBlock", + "endBlock", + "totalReward", + "totalFee", + "totalTx" + ], + "type": "object", + "properties": { + "startBlock": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "endBlock": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "totalReward": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "totalFee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "totalTx": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/mining/reward-stats/{block_count}", + "parameters": [ + { + "name": "block_count", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_series", + "title": "Get series data", + "description": "Fetch data for a specific series at the given index. Use query parameters to filter by date range and format (json/csv).\n\nREST operation: `GET /api/series/{series}/{index}`.", + "inputSchema": { + "type": "object", + "properties": { + "series": { + "$ref": "#/$defs/SeriesName", + "description": "Series name" + }, + "index": { + "$ref": "#/$defs/Index", + "description": "Aggregation index" + }, + "start": { + "$ref": "#/$defs/RangeIndex", + "description": "Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`" + }, + "end": { + "$ref": "#/$defs/RangeIndex", + "description": "Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`" + }, + "limit": { + "$ref": "#/$defs/Limit", + "description": "Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`" + }, + "format": { + "$ref": "#/$defs/Format", + "description": "Format of the output" + } + }, + "additionalProperties": false, + "required": [ + "series", + "index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Date": { + "description": "Date in YYYYMMDD format stored as u32", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "Format": { + "description": "Output format for API responses", + "type": "string", + "enum": [ + "json", + "csv" + ] + }, + "Index": { + "description": "Aggregation dimension for querying series. Includes time-based (date, week, month, year),\nblock-based (height, tx_index), and address/output type indexes.", + "type": "string", + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "examples": [ + "day1" + ] + }, + "Limit": { + "description": "Maximum number of results to return. Defaults to 100 if not specified.", + "type": "integer", + "format": "uint", + "minimum": 0, + "examples": [ + 1, + 10, + 100 + ] + }, + "RangeIndex": { + "description": "A range boundary: integer index, date, or timestamp.", + "anyOf": [ + { + "type": "integer", + "format": "int64" + }, + { + "$ref": "#/$defs/Date" + }, + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "SeriesName": { + "description": "Series name", + "type": "string", + "examples": [ + "price_close", + "market_cap", + "realized_price" + ] + }, + "Timestamp": { + "description": "UNIX timestamp in seconds", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 1231006505, + 1672531200, + 1713571200, + 1743631892, + 1759000868 + ] + } + } + }, + "outputSchema": { + "anyOf": [ + { + "$ref": "#/$defs/SeriesData" + }, + { + "type": "string", + "contentMediaType": "text/csv" + } + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Index": { + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "type": "string" + }, + "SeriesData": { + "required": [ + "version", + "index", + "start", + "end", + "stamp", + "data" + ], + "type": "object", + "properties": { + "version": { + "allOf": [ + { + "$ref": "#/$defs/Version" + } + ] + }, + "index": { + "allOf": [ + { + "$ref": "#/$defs/Index" + } + ] + }, + "type": { + "type": "string" + }, + "start": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "end": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "stamp": { + "type": "string" + }, + "data": { + "items": true, + "type": "array" + } + } + }, + "Version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/{series}/{index}", + "parameters": [ + { + "name": "series", + "location": "path" + }, + { + "name": "index", + "location": "path" + }, + { + "name": "start", + "location": "query" + }, + { + "name": "end", + "location": "query" + }, + { + "name": "limit", + "location": "query" + }, + { + "name": "format", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "get_series_bulk", + "title": "Bulk series data", + "description": "Fetch multiple series in a single request. Supports filtering by index and date range. Returns an array of SeriesData objects. For a single series, use `get_series` instead.\n\nREST operation: `GET /api/series/bulk`.", + "inputSchema": { + "type": "object", + "properties": { + "series": { + "$ref": "#/$defs/SeriesList", + "description": "Requested series" + }, + "index": { + "$ref": "#/$defs/Index", + "description": "Index to query" + }, + "start": { + "$ref": "#/$defs/RangeIndex", + "description": "Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`" + }, + "end": { + "$ref": "#/$defs/RangeIndex", + "description": "Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`" + }, + "limit": { + "$ref": "#/$defs/Limit", + "description": "Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`" + }, + "format": { + "$ref": "#/$defs/Format", + "description": "Format of the output" + } + }, + "additionalProperties": false, + "required": [ + "series", + "index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Date": { + "description": "Date in YYYYMMDD format stored as u32", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "Format": { + "description": "Output format for API responses", + "type": "string", + "enum": [ + "json", + "csv" + ] + }, + "Index": { + "description": "Aggregation dimension for querying series. Includes time-based (date, week, month, year),\nblock-based (height, tx_index), and address/output type indexes.", + "type": "string", + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "examples": [ + "day1" + ] + }, + "Limit": { + "description": "Maximum number of results to return. Defaults to 100 if not specified.", + "type": "integer", + "format": "uint", + "minimum": 0, + "examples": [ + 1, + 10, + 100 + ] + }, + "RangeIndex": { + "description": "A range boundary: integer index, date, or timestamp.", + "anyOf": [ + { + "type": "integer", + "format": "int64" + }, + { + "$ref": "#/$defs/Date" + }, + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "SeriesList": { + "description": "Comma-separated list of series names", + "type": "string", + "examples": [ + "date,price_close", + "price_close", + "price_close,market_cap", + "realized_price,market_cap,mvrv" + ] + }, + "Timestamp": { + "description": "UNIX timestamp in seconds", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 1231006505, + 1672531200, + 1713571200, + 1743631892, + 1759000868 + ] + } + } + }, + "outputSchema": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/SeriesData" + }, + "type": "array" + }, + { + "type": "string", + "contentMediaType": "text/csv" + } + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Index": { + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "type": "string" + }, + "SeriesData": { + "required": [ + "version", + "index", + "start", + "end", + "stamp", + "data" + ], + "type": "object", + "properties": { + "version": { + "allOf": [ + { + "$ref": "#/$defs/Version" + } + ] + }, + "index": { + "allOf": [ + { + "$ref": "#/$defs/Index" + } + ] + }, + "type": { + "type": "string" + }, + "start": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "end": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "stamp": { + "type": "string" + }, + "data": { + "items": true, + "type": "array" + } + } + }, + "Version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/bulk", + "parameters": [ + { + "name": "series", + "location": "query" + }, + { + "name": "index", + "location": "query" + }, + { + "name": "start", + "location": "query" + }, + { + "name": "end", + "location": "query" + }, + { + "name": "limit", + "location": "query" + }, + { + "name": "format", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "get_series_count", + "title": "Series count", + "description": "Returns the number of series available per index type.\n\nREST operation: `GET /api/series/count`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/SeriesCount" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "SeriesCount": { + "required": [ + "distinct", + "total", + "lazy", + "stored" + ], + "type": "object", + "properties": { + "distinct": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "total": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "lazy": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "stored": { + "minimum": 0, + "type": "integer", + "format": "uint" + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/count", + "parameters": [] + } + }, + { + "tool": { + "name": "get_series_data", + "title": "Get raw series data", + "description": "Returns just the data array without the SeriesData wrapper. Supports the same range and format parameters as `GET /api/series/{series}/{index}`.\n\nREST operation: `GET /api/series/{series}/{index}/data`.", + "inputSchema": { + "type": "object", + "properties": { + "series": { + "$ref": "#/$defs/SeriesName", + "description": "Series name" + }, + "index": { + "$ref": "#/$defs/Index", + "description": "Aggregation index" + }, + "start": { + "$ref": "#/$defs/RangeIndex", + "description": "Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`" + }, + "end": { + "$ref": "#/$defs/RangeIndex", + "description": "Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`" + }, + "limit": { + "$ref": "#/$defs/Limit", + "description": "Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`" + }, + "format": { + "$ref": "#/$defs/Format", + "description": "Format of the output" + } + }, + "additionalProperties": false, + "required": [ + "series", + "index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Date": { + "description": "Date in YYYYMMDD format stored as u32", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "Format": { + "description": "Output format for API responses", + "type": "string", + "enum": [ + "json", + "csv" + ] + }, + "Index": { + "description": "Aggregation dimension for querying series. Includes time-based (date, week, month, year),\nblock-based (height, tx_index), and address/output type indexes.", + "type": "string", + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "examples": [ + "day1" + ] + }, + "Limit": { + "description": "Maximum number of results to return. Defaults to 100 if not specified.", + "type": "integer", + "format": "uint", + "minimum": 0, + "examples": [ + 1, + 10, + 100 + ] + }, + "RangeIndex": { + "description": "A range boundary: integer index, date, or timestamp.", + "anyOf": [ + { + "type": "integer", + "format": "int64" + }, + { + "$ref": "#/$defs/Date" + }, + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "SeriesName": { + "description": "Series name", + "type": "string", + "examples": [ + "price_close", + "market_cap", + "realized_price" + ] + }, + "Timestamp": { + "description": "UNIX timestamp in seconds", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 1231006505, + 1672531200, + 1713571200, + 1743631892, + 1759000868 + ] + } + } + }, + "outputSchema": { + "anyOf": [ + { + "items": true, + "type": "array" + }, + { + "type": "string", + "contentMediaType": "text/csv" + } + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/{series}/{index}/data", + "parameters": [ + { + "name": "series", + "location": "path" + }, + { + "name": "index", + "location": "path" + }, + { + "name": "start", + "location": "query" + }, + { + "name": "end", + "location": "query" + }, + { + "name": "limit", + "location": "query" + }, + { + "name": "format", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "get_series_info", + "title": "Get series info", + "description": "Returns the supported indexes and value type for the specified series.\n\nREST operation: `GET /api/series/{series}`.", + "inputSchema": { + "type": "object", + "properties": { + "series": { + "$ref": "#/$defs/SeriesName" + } + }, + "additionalProperties": false, + "required": [ + "series" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "SeriesName": { + "description": "Series name", + "type": "string", + "examples": [ + "price_close", + "market_cap", + "realized_price" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/SeriesInfo", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Index": { + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "type": "string" + }, + "SeriesInfo": { + "required": [ + "indexes", + "type" + ], + "type": "object", + "properties": { + "indexes": { + "items": { + "$ref": "#/$defs/Index" + }, + "type": "array" + }, + "type": { + "type": "string" + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/{series}", + "parameters": [ + { + "name": "series", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_series_latest", + "title": "Get latest series value", + "description": "Returns the single most recent value for a series, unwrapped (not inside a SeriesData object).\n\nREST operation: `GET /api/series/{series}/{index}/latest`.", + "inputSchema": { + "type": "object", + "properties": { + "series": { + "$ref": "#/$defs/SeriesName", + "description": "Series name" + }, + "index": { + "$ref": "#/$defs/Index", + "description": "Aggregation index" + } + }, + "additionalProperties": false, + "required": [ + "series", + "index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Index": { + "description": "Aggregation dimension for querying series. Includes time-based (date, week, month, year),\nblock-based (height, tx_index), and address/output type indexes.", + "type": "string", + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "examples": [ + "day1" + ] + }, + "SeriesName": { + "description": "Series name", + "type": "string", + "examples": [ + "price_close", + "market_cap", + "realized_price" + ] + } + } + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/{series}/{index}/latest", + "parameters": [ + { + "name": "series", + "location": "path" + }, + { + "name": "index", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_series_len", + "title": "Get series data length", + "description": "Returns the total number of data points for a series at the given index.\n\nREST operation: `GET /api/series/{series}/{index}/len`.", + "inputSchema": { + "type": "object", + "properties": { + "series": { + "$ref": "#/$defs/SeriesName", + "description": "Series name" + }, + "index": { + "$ref": "#/$defs/Index", + "description": "Aggregation index" + } + }, + "additionalProperties": false, + "required": [ + "series", + "index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Index": { + "description": "Aggregation dimension for querying series. Includes time-based (date, week, month, year),\nblock-based (height, tx_index), and address/output type indexes.", + "type": "string", + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "examples": [ + "day1" + ] + }, + "SeriesName": { + "description": "Series name", + "type": "string", + "examples": [ + "price_close", + "market_cap", + "realized_price" + ] + } + } + }, + "outputSchema": { + "type": "integer", + "minimum": 0, + "format": "uint", + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/{series}/{index}/len", + "parameters": [ + { + "name": "series", + "location": "path" + }, + { + "name": "index", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_series_tree", + "title": "Series catalog", + "description": "Returns the complete hierarchical catalog of available series organized as a tree structure. Series are grouped by categories and subcategories.\n\nREST operation: `GET /api/series`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/TreeNode", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Index": { + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "type": "string" + }, + "SeriesLeafWithSchema": { + "required": [ + "name", + "kind", + "indexes", + "type" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "indexes": { + "items": { + "$ref": "#/$defs/Index" + }, + "type": "array", + "uniqueItems": true + }, + "type": { + "type": "string" + } + } + }, + "TreeNode": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/TreeNode" + }, + "type": "object" + }, + { + "allOf": [ + { + "$ref": "#/$defs/SeriesLeafWithSchema" + } + ] + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series", + "parameters": [] + } + }, + { + "tool": { + "name": "get_series_version", + "title": "Get series version", + "description": "Returns the current version of a series. Changes when the series data is updated.\n\nREST operation: `GET /api/series/{series}/{index}/version`.", + "inputSchema": { + "type": "object", + "properties": { + "series": { + "$ref": "#/$defs/SeriesName", + "description": "Series name" + }, + "index": { + "$ref": "#/$defs/Index", + "description": "Aggregation index" + } + }, + "additionalProperties": false, + "required": [ + "series", + "index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Index": { + "description": "Aggregation dimension for querying series. Includes time-based (date, week, month, year),\nblock-based (height, tx_index), and address/output type indexes.", + "type": "string", + "enum": [ + "minute10", + "minute30", + "hour1", + "hour4", + "hour12", + "day1", + "day3", + "week1", + "month1", + "month3", + "month6", + "year1", + "year10", + "halving", + "epoch", + "height", + "tx_index", + "txin_index", + "txout_index", + "empty_output_index", + "op_return_index", + "p2a_addr_index", + "p2ms_output_index", + "p2pk33_addr_index", + "p2pk65_addr_index", + "p2pkh_addr_index", + "p2sh_addr_index", + "p2tr_addr_index", + "p2wpkh_addr_index", + "p2wsh_addr_index", + "unknown_output_index", + "funded_addr_index", + "empty_addr_index" + ], + "examples": [ + "day1" + ] + }, + "SeriesName": { + "description": "Series name", + "type": "string", + "examples": [ + "price_close", + "market_cap", + "realized_price" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/Version", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Version": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/{series}/{index}/version", + "parameters": [ + { + "name": "series", + "location": "path" + }, + { + "name": "index", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_sync_status", + "title": "Sync status", + "description": "Returns the sync status of the indexer, including indexed height, tip height, blocks behind, and last indexed timestamp.\n\nREST operation: `GET /api/server/sync`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/SyncStatus", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "SyncStatus": { + "required": [ + "indexed_height", + "computed_height", + "tip_height", + "blocks_behind", + "last_indexed_at", + "last_indexed_at_unix" + ], + "type": "object", + "properties": { + "indexed_height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "computed_height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "tip_height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "blocks_behind": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "last_indexed_at": { + "type": "string" + }, + "last_indexed_at_unix": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + } + } + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/server/sync", + "parameters": [] + } + }, + { + "tool": { + "name": "get_transaction_times", + "title": "Transaction first-seen times", + "description": "Returns timestamps when transactions were first seen in the mempool. Returns 0 for mined or unknown transactions.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-times)*\n\nREST operation: `GET /api/v1/transaction-times`.", + "inputSchema": { + "type": "object", + "properties": { + "txId[]": { + "items": { + "$ref": "#/$defs/Txid" + }, + "type": "array", + "description": "Transaction IDs to look up (max 250 per request)." + } + }, + "additionalProperties": false, + "required": [ + "txId[]" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "outputSchema": { + "items": { + "type": "integer", + "minimum": 0, + "format": "uint64" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/transaction-times", + "parameters": [ + { + "name": "txId[]", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "get_tx", + "title": "Transaction information", + "description": "Retrieve complete transaction data by transaction ID (txid). Returns inputs, outputs, fee, size, and confirmation status.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction)*\n\nREST operation: `GET /api/tx/{txid}`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/Transaction", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "RawLockTime": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "SigOps": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Transaction": { + "required": [ + "txid", + "version", + "locktime", + "vin", + "vout", + "size", + "weight", + "sigops", + "fee", + "status" + ], + "type": "object", + "properties": { + "index": { + "anyOf": [ + { + "$ref": "#/$defs/TxIndex" + }, + { + "type": "null" + } + ] + }, + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "version": { + "allOf": [ + { + "$ref": "#/$defs/TxVersionRaw" + } + ] + }, + "locktime": { + "allOf": [ + { + "$ref": "#/$defs/RawLockTime" + } + ] + }, + "vin": { + "items": { + "$ref": "#/$defs/TxIn" + }, + "type": "array" + }, + "vout": { + "items": { + "$ref": "#/$defs/TxOut" + }, + "type": "array" + }, + "size": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/Weight" + } + ] + }, + "sigops": { + "allOf": [ + { + "$ref": "#/$defs/SigOps" + } + ] + }, + "fee": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "status": { + "allOf": [ + { + "$ref": "#/$defs/TxStatus" + } + ] + } + } + }, + "TxIn": { + "required": [ + "txid", + "vout", + "scriptsig", + "scriptsig_asm", + "witness", + "is_coinbase", + "sequence", + "inner_redeemscript_asm", + "inner_witnessscript_asm" + ], + "type": "object", + "properties": { + "txid": { + "allOf": [ + { + "$ref": "#/$defs/Txid" + } + ] + }, + "vout": { + "allOf": [ + { + "$ref": "#/$defs/Vout" + } + ] + }, + "prevout": { + "anyOf": [ + { + "$ref": "#/$defs/TxOut" + }, + { + "type": "null" + } + ] + }, + "scriptsig": { + "type": "string" + }, + "scriptsig_asm": { + "type": "string" + }, + "witness": { + "allOf": [ + { + "$ref": "#/$defs/Witness" + } + ] + }, + "is_coinbase": { + "type": "boolean" + }, + "sequence": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "inner_redeemscript_asm": { + "type": "string" + }, + "inner_witnessscript_asm": { + "type": "string" + } + } + }, + "TxIndex": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOut": { + "required": [ + "scriptpubkey", + "value" + ], + "type": "object", + "properties": { + "scriptpubkey": { + "type": "string" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "TxVersionRaw": { + "format": "int32", + "type": "integer" + }, + "Txid": { + "type": "string" + }, + "Vout": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "Weight": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Witness": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx/{txid}", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_by_index", + "title": "Txid by index", + "description": "Retrieve the transaction ID (txid) at a given global transaction index. Returns the txid as plain text.\n\nREST operation: `GET /api/tx-index/{index}`.", + "inputSchema": { + "type": "object", + "properties": { + "index": { + "$ref": "#/$defs/TxIndex" + } + }, + "additionalProperties": false, + "required": [ + "index" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "TxIndex": { + "description": "Chain-wide transaction index (0 = the genesis coinbase). For an\nin-block position, use `BlockTxIndex` instead.", + "type": "integer", + "format": "uint32", + "minimum": 0, + "examples": [ + 0 + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx-index/{index}", + "parameters": [ + { + "name": "index", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_hex", + "title": "Transaction hex", + "description": "Retrieve the raw transaction as a hex-encoded string. Returns the serialized transaction in hexadecimal format.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-hex)*\n\nREST operation: `GET /api/tx/{txid}/hex`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx/{txid}/hex", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_merkle_proof", + "title": "Transaction merkle proof", + "description": "Get the merkle inclusion proof for a transaction.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-merkle-proof)*\n\nREST operation: `GET /api/tx/{txid}/merkle-proof`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/MerkleProof", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "MerkleProof": { + "required": [ + "block_height", + "merkle", + "pos" + ], + "type": "object", + "properties": { + "block_height": { + "allOf": [ + { + "$ref": "#/$defs/Height" + } + ] + }, + "merkle": { + "items": { + "type": "string" + }, + "type": "array" + }, + "pos": { + "minimum": 0, + "type": "integer", + "format": "uint" + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx/{txid}/merkle-proof", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_merkleblock_proof", + "title": "Transaction merkleblock proof", + "description": "Get the merkleblock proof for a transaction (BIP37 format, hex encoded).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-merkleblock-proof)*\n\nREST operation: `GET /api/tx/{txid}/merkleblock-proof`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx/{txid}/merkleblock-proof", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_outspend", + "title": "Output spend status", + "description": "Get the spending status of a transaction output. Returns whether the output has been spent and, if so, the spending transaction details.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-outspend)*\n\nREST operation: `GET /api/tx/{txid}/outspend/{vout}`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid", + "description": "Transaction ID" + }, + "vout": { + "$ref": "#/$defs/Vout", + "description": "Output index" + } + }, + "additionalProperties": false, + "required": [ + "txid", + "vout" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + }, + "Vout": { + "description": "Index of the output being spent in the previous transaction", + "type": "integer", + "format": "uint16", + "minimum": 0, + "maximum": 65535, + "examples": [ + 0, + 1, + 2, + 5, + 10 + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/TxOutspend", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOutspend": { + "required": [ + "spent" + ], + "type": "object", + "properties": { + "spent": { + "type": "boolean" + }, + "txid": { + "anyOf": [ + { + "$ref": "#/$defs/Txid" + }, + { + "type": "null" + } + ] + }, + "vin": { + "anyOf": [ + { + "$ref": "#/$defs/Vin" + }, + { + "type": "null" + } + ] + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/TxStatus" + }, + { + "type": "null" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "Txid": { + "type": "string" + }, + "Vin": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx/{txid}/outspend/{vout}", + "parameters": [ + { + "name": "txid", + "location": "path" + }, + { + "name": "vout", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_outspends", + "title": "All output spend statuses", + "description": "Get the spending status of all outputs in a transaction. Returns an array with the spend status for each output.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-outspends)*\n\nREST operation: `GET /api/tx/{txid}/outspends`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/TxOutspend" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxOutspend": { + "required": [ + "spent" + ], + "type": "object", + "properties": { + "spent": { + "type": "boolean" + }, + "txid": { + "anyOf": [ + { + "$ref": "#/$defs/Txid" + }, + { + "type": "null" + } + ] + }, + "vin": { + "anyOf": [ + { + "$ref": "#/$defs/Vin" + }, + { + "type": "null" + } + ] + }, + "status": { + "anyOf": [ + { + "$ref": "#/$defs/TxStatus" + }, + { + "type": "null" + } + ] + } + } + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + }, + "Txid": { + "type": "string" + }, + "Vin": { + "maximum": 65535, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx/{txid}/outspends", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_raw", + "title": "Transaction raw", + "description": "Returns a transaction as binary data.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-raw)*\n\nREST operation: `GET /api/tx/{txid}/raw`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx/{txid}/raw", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_rbf", + "title": "RBF replacement history", + "description": "Returns the RBF replacement tree for a transaction, if any. Both `replacements` and `replaces` are null when the tx has no known RBF history within the mempool monitor's retention window.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-rbf-history)*\n\nREST operation: `GET /api/v1/tx/{txid}/rbf`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/RbfResponse", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "FeeRate": { + "format": "double", + "type": "number" + }, + "RbfResponse": { + "properties": { + "replacements": { + "anyOf": [ + { + "$ref": "#/$defs/ReplacementNode" + }, + { + "type": "null" + } + ] + }, + "replaces": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Txid" + } + } + }, + "type": "object" + }, + "RbfTx": { + "required": [ + "txid", + "fee", + "vsize", + "value", + "rate", + "time", + "rbf" + ], + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + }, + "fee": { + "$ref": "#/$defs/Sats" + }, + "vsize": { + "$ref": "#/$defs/VSize" + }, + "value": { + "allOf": [ + { + "$ref": "#/$defs/Sats" + } + ] + }, + "rate": { + "$ref": "#/$defs/FeeRate" + }, + "time": { + "$ref": "#/$defs/Timestamp" + }, + "rbf": { + "type": "boolean" + }, + "fullRbf": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "ReplacementNode": { + "required": [ + "tx", + "time", + "fullRbf", + "replaces" + ], + "type": "object", + "properties": { + "tx": { + "$ref": "#/$defs/RbfTx" + }, + "time": { + "allOf": [ + { + "$ref": "#/$defs/Timestamp" + } + ] + }, + "fullRbf": { + "type": "boolean" + }, + "interval": { + "minimum": 0, + "type": [ + "integer", + "null" + ], + "format": "uint32" + }, + "mined": { + "type": [ + "boolean", + "null" + ] + }, + "replaces": { + "type": "array", + "items": { + "$ref": "#/$defs/ReplacementNode" + } + } + } + }, + "Sats": { + "minimum": 0, + "type": "integer", + "format": "uint64" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Txid": { + "type": "string" + }, + "VSize": { + "minimum": 0, + "type": "integer", + "format": "uint64" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/tx/{txid}/rbf", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_tx_status", + "title": "Transaction status", + "description": "Retrieve the confirmation status of a transaction. Returns whether the transaction is confirmed and, if so, the block height, hash, and timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-status)*\n\nREST operation: `GET /api/tx/{txid}/status`.", + "inputSchema": { + "type": "object", + "properties": { + "txid": { + "$ref": "#/$defs/Txid" + } + }, + "additionalProperties": false, + "required": [ + "txid" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Txid": { + "description": "Transaction ID (hash)", + "type": "string", + "examples": [ + "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/TxStatus", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "BlockHash": { + "type": "string" + }, + "Height": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Timestamp": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "TxStatus": { + "required": [ + "confirmed" + ], + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + }, + "block_height": { + "anyOf": [ + { + "$ref": "#/$defs/Height" + }, + { + "type": "null" + } + ] + }, + "block_hash": { + "anyOf": [ + { + "$ref": "#/$defs/BlockHash" + }, + { + "type": "null" + } + ] + }, + "block_time": { + "anyOf": [ + { + "$ref": "#/$defs/Timestamp" + }, + { + "type": "null" + } + ] + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/tx/{txid}/status", + "parameters": [ + { + "name": "txid", + "location": "path" + } + ] + } + }, + { + "tool": { + "name": "get_urpd", + "title": "Latest URPD", + "description": "URPD for the most recent available date in the cohort. The response's `date` field echoes which date was served. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }`. `close` and each bucket's `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and bucket `supply` are BTC. `unrealized_pnl` can be negative.\n\nREST operation: `GET /api/urpd/{cohort}`.", + "inputSchema": { + "type": "object", + "properties": { + "cohort": { + "$ref": "#/$defs/Cohort" + }, + "agg": { + "$ref": "#/$defs/UrpdAggregation", + "description": "Aggregation strategy. Default: raw (no aggregation). Accepts `bucket` as alias." + }, + "weight": { + "$ref": "#/$defs/UrpdWeight", + "description": "Supply weighting. Default: raw (unweighted)." + } + }, + "additionalProperties": false, + "required": [ + "cohort" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Cohort": { + "description": "URPD cohort identifier. Use `GET /api/urpd` to list available cohorts.\n\nValidated at construction: non-empty, ASCII `[a-z0-9_]+`. Matches the\nschemars enum value set; the type therefore proves \"this is a valid\ncohort name\" wherever a `Cohort` is held.", + "type": "string", + "enum": [ + "all", + "sth", + "lth", + "utxos_under_1h_old", + "utxos_1h_to_1d_old", + "utxos_1d_to_1w_old", + "utxos_1w_to_1m_old", + "utxos_1m_to_2m_old", + "utxos_2m_to_3m_old", + "utxos_3m_to_4m_old", + "utxos_4m_to_5m_old", + "utxos_5m_to_6m_old", + "utxos_6m_to_9m_old", + "utxos_9m_to_1y_old", + "utxos_1y_to_18m_old", + "utxos_18m_to_2y_old", + "utxos_2y_to_3y_old", + "utxos_3y_to_4y_old", + "utxos_4y_to_5y_old", + "utxos_5y_to_6y_old", + "utxos_6y_to_7y_old", + "utxos_7y_to_8y_old", + "utxos_8y_to_10y_old", + "utxos_10y_to_12y_old", + "utxos_12y_to_15y_old", + "utxos_over_15y_old" + ] + }, + "UrpdAggregation": { + "description": "Aggregation strategy for URPD buckets.\nOptions: raw (no aggregation), lin200/lin500/lin1000 (linear $200/$500/$1000),\nlog10/log50/log100/log200/log500/log1000/log2000 (logarithmic with 10/50/100/200/500/1000/2000 buckets per decade).", + "type": "string", + "enum": [ + "raw", + "lin200", + "lin500", + "lin1000", + "log10", + "log50", + "log100", + "log200", + "log500", + "log1000", + "log2000" + ] + }, + "UrpdWeight": { + "description": "Weighting applied to a URPD: raw (unweighted), cointime, or coinflow.", + "type": "string", + "enum": [ + "raw", + "cointime", + "coinflow" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/Urpd", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Bitcoin": { + "format": "double", + "type": "number" + }, + "Cohort": { + "enum": [ + "all", + "sth", + "lth", + "utxos_under_1h_old", + "utxos_1h_to_1d_old", + "utxos_1d_to_1w_old", + "utxos_1w_to_1m_old", + "utxos_1m_to_2m_old", + "utxos_2m_to_3m_old", + "utxos_3m_to_4m_old", + "utxos_4m_to_5m_old", + "utxos_5m_to_6m_old", + "utxos_6m_to_9m_old", + "utxos_9m_to_1y_old", + "utxos_1y_to_18m_old", + "utxos_18m_to_2y_old", + "utxos_2y_to_3y_old", + "utxos_3y_to_4y_old", + "utxos_4y_to_5y_old", + "utxos_5y_to_6y_old", + "utxos_6y_to_7y_old", + "utxos_7y_to_8y_old", + "utxos_8y_to_10y_old", + "utxos_10y_to_12y_old", + "utxos_12y_to_15y_old", + "utxos_over_15y_old" + ], + "type": "string" + }, + "Date": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "Urpd": { + "required": [ + "cohort", + "date", + "weight", + "aggregation", + "close", + "total_supply", + "buckets" + ], + "type": "object", + "properties": { + "cohort": { + "$ref": "#/$defs/Cohort" + }, + "date": { + "$ref": "#/$defs/Date" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/UrpdWeight" + } + ] + }, + "aggregation": { + "allOf": [ + { + "$ref": "#/$defs/UrpdAggregation" + } + ] + }, + "close": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + }, + "total_supply": { + "allOf": [ + { + "$ref": "#/$defs/Bitcoin" + } + ] + }, + "buckets": { + "type": "array", + "items": { + "$ref": "#/$defs/UrpdBucket" + } + } + } + }, + "UrpdAggregation": { + "enum": [ + "raw", + "lin200", + "lin500", + "lin1000", + "log10", + "log50", + "log100", + "log200", + "log500", + "log1000", + "log2000" + ], + "type": "string" + }, + "UrpdBucket": { + "required": [ + "price_floor", + "supply", + "realized_cap", + "unrealized_pnl" + ], + "type": "object", + "properties": { + "price_floor": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + }, + "supply": { + "allOf": [ + { + "$ref": "#/$defs/Bitcoin" + } + ] + }, + "realized_cap": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + }, + "unrealized_pnl": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "UrpdWeight": { + "enum": [ + "raw", + "cointime", + "coinflow" + ], + "type": "string" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/urpd/{cohort}", + "parameters": [ + { + "name": "cohort", + "location": "path" + }, + { + "name": "agg", + "location": "query" + }, + { + "name": "weight", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "get_urpd_at", + "title": "URPD at date", + "description": "URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. `close`, `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and `supply` are BTC. `unrealized_pnl` can be negative.\n\nREST operation: `GET /api/urpd/{cohort}/{date}`.", + "inputSchema": { + "type": "object", + "properties": { + "cohort": { + "$ref": "#/$defs/Cohort" + }, + "date": { + "type": "string", + "description": "Calendar date of the URPD snapshot in `YYYY-MM-DD` format.", + "examples": [ + "2024-01-01" + ] + }, + "agg": { + "$ref": "#/$defs/UrpdAggregation", + "description": "Aggregation strategy. Default: raw (no aggregation). Accepts `bucket` as alias." + }, + "weight": { + "$ref": "#/$defs/UrpdWeight", + "description": "Supply weighting. Default: raw (unweighted)." + } + }, + "additionalProperties": false, + "required": [ + "cohort", + "date" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Cohort": { + "description": "URPD cohort identifier. Use `GET /api/urpd` to list available cohorts.\n\nValidated at construction: non-empty, ASCII `[a-z0-9_]+`. Matches the\nschemars enum value set; the type therefore proves \"this is a valid\ncohort name\" wherever a `Cohort` is held.", + "type": "string", + "enum": [ + "all", + "sth", + "lth", + "utxos_under_1h_old", + "utxos_1h_to_1d_old", + "utxos_1d_to_1w_old", + "utxos_1w_to_1m_old", + "utxos_1m_to_2m_old", + "utxos_2m_to_3m_old", + "utxos_3m_to_4m_old", + "utxos_4m_to_5m_old", + "utxos_5m_to_6m_old", + "utxos_6m_to_9m_old", + "utxos_9m_to_1y_old", + "utxos_1y_to_18m_old", + "utxos_18m_to_2y_old", + "utxos_2y_to_3y_old", + "utxos_3y_to_4y_old", + "utxos_4y_to_5y_old", + "utxos_5y_to_6y_old", + "utxos_6y_to_7y_old", + "utxos_7y_to_8y_old", + "utxos_8y_to_10y_old", + "utxos_10y_to_12y_old", + "utxos_12y_to_15y_old", + "utxos_over_15y_old" + ] + }, + "UrpdAggregation": { + "description": "Aggregation strategy for URPD buckets.\nOptions: raw (no aggregation), lin200/lin500/lin1000 (linear $200/$500/$1000),\nlog10/log50/log100/log200/log500/log1000/log2000 (logarithmic with 10/50/100/200/500/1000/2000 buckets per decade).", + "type": "string", + "enum": [ + "raw", + "lin200", + "lin500", + "lin1000", + "log10", + "log50", + "log100", + "log200", + "log500", + "log1000", + "log2000" + ] + }, + "UrpdWeight": { + "description": "Weighting applied to a URPD: raw (unweighted), cointime, or coinflow.", + "type": "string", + "enum": [ + "raw", + "cointime", + "coinflow" + ] + } + } + }, + "outputSchema": { + "$ref": "#/$defs/Urpd", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Bitcoin": { + "format": "double", + "type": "number" + }, + "Cohort": { + "enum": [ + "all", + "sth", + "lth", + "utxos_under_1h_old", + "utxos_1h_to_1d_old", + "utxos_1d_to_1w_old", + "utxos_1w_to_1m_old", + "utxos_1m_to_2m_old", + "utxos_2m_to_3m_old", + "utxos_3m_to_4m_old", + "utxos_4m_to_5m_old", + "utxos_5m_to_6m_old", + "utxos_6m_to_9m_old", + "utxos_9m_to_1y_old", + "utxos_1y_to_18m_old", + "utxos_18m_to_2y_old", + "utxos_2y_to_3y_old", + "utxos_3y_to_4y_old", + "utxos_4y_to_5y_old", + "utxos_5y_to_6y_old", + "utxos_6y_to_7y_old", + "utxos_7y_to_8y_old", + "utxos_8y_to_10y_old", + "utxos_10y_to_12y_old", + "utxos_12y_to_15y_old", + "utxos_over_15y_old" + ], + "type": "string" + }, + "Date": { + "minimum": 0, + "type": "integer", + "format": "uint32" + }, + "Dollars": { + "format": "double", + "type": "number" + }, + "Urpd": { + "required": [ + "cohort", + "date", + "weight", + "aggregation", + "close", + "total_supply", + "buckets" + ], + "type": "object", + "properties": { + "cohort": { + "$ref": "#/$defs/Cohort" + }, + "date": { + "$ref": "#/$defs/Date" + }, + "weight": { + "allOf": [ + { + "$ref": "#/$defs/UrpdWeight" + } + ] + }, + "aggregation": { + "allOf": [ + { + "$ref": "#/$defs/UrpdAggregation" + } + ] + }, + "close": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + }, + "total_supply": { + "allOf": [ + { + "$ref": "#/$defs/Bitcoin" + } + ] + }, + "buckets": { + "type": "array", + "items": { + "$ref": "#/$defs/UrpdBucket" + } + } + } + }, + "UrpdAggregation": { + "enum": [ + "raw", + "lin200", + "lin500", + "lin1000", + "log10", + "log50", + "log100", + "log200", + "log500", + "log1000", + "log2000" + ], + "type": "string" + }, + "UrpdBucket": { + "required": [ + "price_floor", + "supply", + "realized_cap", + "unrealized_pnl" + ], + "type": "object", + "properties": { + "price_floor": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + }, + "supply": { + "allOf": [ + { + "$ref": "#/$defs/Bitcoin" + } + ] + }, + "realized_cap": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + }, + "unrealized_pnl": { + "allOf": [ + { + "$ref": "#/$defs/Dollars" + } + ] + } + } + }, + "UrpdWeight": { + "enum": [ + "raw", + "cointime", + "coinflow" + ], + "type": "string" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/urpd/{cohort}/{date}", + "parameters": [ + { + "name": "cohort", + "location": "path" + }, + { + "name": "date", + "location": "path" + }, + { + "name": "agg", + "location": "query" + }, + { + "name": "weight", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "get_version", + "title": "API version", + "description": "Returns the current version of the API server\n\nREST operation: `GET /version`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "type": "string", + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/version", + "parameters": [] + } + }, + { + "tool": { + "name": "list_series", + "title": "Series list", + "description": "Paginated flat list of all available series names. Use `page` query param for pagination.\n\nREST operation: `GET /api/series/list`.", + "inputSchema": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "minimum": 0, + "format": "uint", + "description": "Pagination index", + "examples": [ + 0, + 1, + 2 + ] + }, + "per_page": { + "type": "integer", + "minimum": 0, + "format": "uint", + "description": "Results per page (default: 1000, max: 1000)", + "examples": [ + 100, + 1000 + ] + } + }, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/PaginatedSeries", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "PaginatedSeries": { + "required": [ + "current_page", + "max_page", + "total_count", + "per_page", + "has_more", + "series" + ], + "type": "object", + "properties": { + "current_page": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "max_page": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "total_count": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "per_page": { + "minimum": 0, + "type": "integer", + "format": "uint" + }, + "has_more": { + "type": "boolean" + }, + "series": { + "items": { + "type": "string" + }, + "type": "array" + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/list", + "parameters": [ + { + "name": "page", + "location": "query" + }, + { + "name": "per_page", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "list_urpd_cohorts", + "title": "Available URPD cohorts", + "description": "Cohorts for which URPD data is available. Returns names like `all`, `sth`, `lth`, `utxos_under_1h_old`.\n\nREST operation: `GET /api/urpd`.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Cohort" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Cohort": { + "enum": [ + "all", + "sth", + "lth", + "utxos_under_1h_old", + "utxos_1h_to_1d_old", + "utxos_1d_to_1w_old", + "utxos_1w_to_1m_old", + "utxos_1m_to_2m_old", + "utxos_2m_to_3m_old", + "utxos_3m_to_4m_old", + "utxos_4m_to_5m_old", + "utxos_5m_to_6m_old", + "utxos_6m_to_9m_old", + "utxos_9m_to_1y_old", + "utxos_1y_to_18m_old", + "utxos_18m_to_2y_old", + "utxos_2y_to_3y_old", + "utxos_3y_to_4y_old", + "utxos_4y_to_5y_old", + "utxos_5y_to_6y_old", + "utxos_6y_to_7y_old", + "utxos_7y_to_8y_old", + "utxos_8y_to_10y_old", + "utxos_10y_to_12y_old", + "utxos_12y_to_15y_old", + "utxos_over_15y_old" + ], + "type": "string" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/urpd", + "parameters": [] + } + }, + { + "tool": { + "name": "list_urpd_dates", + "title": "Available URPD dates", + "description": "Dates for which a URPD snapshot is available for the cohort and selected `weight`. One entry per UTC day, sorted ascending.\n\nREST operation: `GET /api/urpd/{cohort}/dates`.", + "inputSchema": { + "type": "object", + "properties": { + "cohort": { + "$ref": "#/$defs/Cohort" + }, + "weight": { + "$ref": "#/$defs/UrpdWeight", + "description": "Supply weighting. Default: raw (unweighted)." + } + }, + "additionalProperties": false, + "required": [ + "cohort" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Cohort": { + "description": "URPD cohort identifier. Use `GET /api/urpd` to list available cohorts.\n\nValidated at construction: non-empty, ASCII `[a-z0-9_]+`. Matches the\nschemars enum value set; the type therefore proves \"this is a valid\ncohort name\" wherever a `Cohort` is held.", + "type": "string", + "enum": [ + "all", + "sth", + "lth", + "utxos_under_1h_old", + "utxos_1h_to_1d_old", + "utxos_1d_to_1w_old", + "utxos_1w_to_1m_old", + "utxos_1m_to_2m_old", + "utxos_2m_to_3m_old", + "utxos_3m_to_4m_old", + "utxos_4m_to_5m_old", + "utxos_5m_to_6m_old", + "utxos_6m_to_9m_old", + "utxos_9m_to_1y_old", + "utxos_1y_to_18m_old", + "utxos_18m_to_2y_old", + "utxos_2y_to_3y_old", + "utxos_3y_to_4y_old", + "utxos_4y_to_5y_old", + "utxos_5y_to_6y_old", + "utxos_6y_to_7y_old", + "utxos_7y_to_8y_old", + "utxos_8y_to_10y_old", + "utxos_10y_to_12y_old", + "utxos_12y_to_15y_old", + "utxos_over_15y_old" + ] + }, + "UrpdWeight": { + "description": "Weighting applied to a URPD: raw (unweighted), cointime, or coinflow.", + "type": "string", + "enum": [ + "raw", + "cointime", + "coinflow" + ] + } + } + }, + "outputSchema": { + "items": { + "$ref": "#/$defs/Date" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Date": { + "minimum": 0, + "type": "integer", + "format": "uint32" + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/urpd/{cohort}/dates", + "parameters": [ + { + "name": "cohort", + "location": "path" + }, + { + "name": "weight", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "search_series", + "title": "Search series", + "description": "Fuzzy search for series by name. Supports partial matches and typos.\n\nREST operation: `GET /api/series/search`.", + "inputSchema": { + "type": "object", + "properties": { + "q": { + "$ref": "#/$defs/SeriesName", + "description": "Search query string" + }, + "limit": { + "$ref": "#/$defs/Limit", + "description": "Maximum number of results" + } + }, + "additionalProperties": false, + "required": [ + "q" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "Limit": { + "description": "Maximum number of results to return. Defaults to 100 if not specified.", + "type": "integer", + "format": "uint", + "minimum": 0, + "examples": [ + 1, + 10, + 100 + ] + }, + "SeriesName": { + "description": "Series name", + "type": "string", + "examples": [ + "price_close", + "market_cap", + "realized_price" + ] + } + } + }, + "outputSchema": { + "items": { + "type": "string" + }, + "type": "array", + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/series/search", + "parameters": [ + { + "name": "q", + "location": "query" + }, + { + "name": "limit", + "location": "query" + } + ] + } + }, + { + "tool": { + "name": "validate_address", + "title": "Validate address", + "description": "Validate a Bitcoin address and get information about its type and scriptPubKey. Returns `isvalid: false` with an error message for invalid addresses.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)*\n\nREST operation: `GET /api/v1/validate-address/{address}`.", + "inputSchema": { + "type": "object", + "properties": { + "address": { + "type": "string", + "description": "Bitcoin address to validate (can be any string)", + "examples": [ + "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + "not-a-valid-addr", + "bc1qinvalid" + ] + } + }, + "additionalProperties": false, + "required": [ + "address" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "$ref": "#/$defs/AddrValidation", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "AddrValidation": { + "required": [ + "isvalid" + ], + "type": "object", + "properties": { + "isvalid": { + "type": "boolean" + }, + "address": { + "type": [ + "string", + "null" + ] + }, + "scriptPubKey": { + "type": [ + "string", + "null" + ] + }, + "isscript": { + "type": [ + "boolean", + "null" + ] + }, + "iswitness": { + "type": [ + "boolean", + "null" + ] + }, + "witness_version": { + "maximum": 255, + "type": [ + "integer", + "null" + ], + "format": "uint8", + "minimum": 0 + }, + "witness_program": { + "type": [ + "string", + "null" + ] + }, + "error_locations": { + "items": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "type": [ + "array", + "null" + ] + }, + "error": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + "http": { + "method": "GET", + "path": "/api/v1/validate-address/{address}", + "parameters": [ + { + "name": "address", + "location": "path" + } + ] + } + } + ] +} diff --git a/crates/brk_mcp/src/config.rs b/crates/brk_mcp/src/config.rs new file mode 100644 index 000000000..7ece71069 --- /dev/null +++ b/crates/brk_mcp/src/config.rs @@ -0,0 +1,77 @@ +use axum::http::Uri; + +pub fn api_bases(input: &str) -> Result, String> { + let input = input.trim().trim_end_matches('/'); + if input.is_empty() { + return Err("REST API URL or host must not be empty".to_string()); + } + let bases = if input.contains("://") { + vec![input.to_string()] + } else { + vec![format!("https://{input}"), format!("http://{input}")] + }; + for base in &bases { + validate_api_base(base)?; + } + Ok(bases) +} + +fn validate_api_base(base: &str) -> Result<(), String> { + let uri: Uri = base + .parse() + .map_err(|error| format!("invalid REST API origin: {error}"))?; + if !matches!(uri.scheme_str(), Some("http" | "https")) || uri.authority().is_none() { + return Err("REST API origin must be an absolute HTTP(S) URL".to_string()); + } + if uri.query().is_some() || !matches!(uri.path(), "" | "/") { + return Err("REST API origin must not contain a path or query".to_string()); + } + if uri + .authority() + .is_some_and(|authority| authority.as_str().rsplit_once('@').is_some()) + { + return Err("REST API origin must not contain credentials".to_string()); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_origin_only_base_urls() { + assert!(validate_api_base("https://bitview.space").is_ok()); + assert!(validate_api_base("http://127.0.0.1:3110").is_ok()); + assert!(validate_api_base("https://bitview.space/api").is_err()); + assert!(validate_api_base("https://user:pass@bitview.space").is_err()); + } + + #[test] + fn expands_bare_hosts_with_https_first() { + assert_eq!( + api_bases("bitview.space").unwrap(), + ["https://bitview.space", "http://bitview.space"] + ); + assert_eq!( + api_bases("http://127.0.0.1:3110/").unwrap(), + ["http://127.0.0.1:3110"] + ); + } + + #[test] + fn reports_positional_origin_errors() { + assert_eq!( + validate_api_base("bitview.space").unwrap_err(), + "REST API origin must be an absolute HTTP(S) URL" + ); + assert_eq!( + validate_api_base("https://bitview.space/api").unwrap_err(), + "REST API origin must not contain a path or query" + ); + assert_eq!( + validate_api_base("https://user:pass@bitview.space").unwrap_err(), + "REST API origin must not contain credentials" + ); + } +} diff --git a/crates/brk_mcp/src/main.rs b/crates/brk_mcp/src/main.rs new file mode 100644 index 000000000..c1da93c8d --- /dev/null +++ b/crates/brk_mcp/src/main.rs @@ -0,0 +1,58 @@ +mod config; +mod manifest; +mod server; +mod upstream; + +use std::{env, error::Error, io}; + +use config::api_bases; +use manifest::Catalog; +use tokio::net::TcpListener; +use tracing::info; + +const BIND_START: std::net::SocketAddr = + std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), 3111); + +#[tokio::main] +async fn main() -> Result<(), Box> { + brk_logger::init_with_default_level(None, "info")?; + + let mut arguments = env::args(); + let _program = arguments.next(); + let api_base = arguments + .next() + .ok_or_else(|| io::Error::other("usage: brk_mcp "))?; + if arguments.next().is_some() { + return Err(io::Error::other("usage: brk_mcp ").into()); + } + let api_bases = api_bases(&api_base).map_err(io::Error::other)?; + let catalog = Catalog::embedded().map_err(io::Error::other)?; + let tool_count = catalog.tools().len(); + let app = server::router(api_bases, catalog); + let (listener, bind) = bind_available(BIND_START).await?; + + info!(%bind, tool_count, "Starting stateless BRK MCP server"); + axum::serve(listener, app).await?; + Ok(()) +} + +async fn bind_available( + start: std::net::SocketAddr, +) -> io::Result<(TcpListener, std::net::SocketAddr)> { + let last_port = start.port().saturating_add(100); + let mut last_error = None; + for port in start.port()..=last_port { + let mut candidate = start; + candidate.set_port(port); + match TcpListener::bind(candidate).await { + Ok(listener) => return Ok((listener, candidate)), + Err(error) => last_error = Some(error), + } + } + Err(last_error.unwrap_or_else(|| { + io::Error::new( + io::ErrorKind::AddrNotAvailable, + "no MCP bind port available", + ) + })) +} diff --git a/crates/brk_mcp/src/manifest.rs b/crates/brk_mcp/src/manifest.rs new file mode 100644 index 000000000..57ffcc396 --- /dev/null +++ b/crates/brk_mcp/src/manifest.rs @@ -0,0 +1,550 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use rmcp::model::Tool; +use serde::Deserialize; +use serde_json::{Map, Value}; + +const MANIFEST_JSON: &str = include_str!("../generated/manifest.json"); +const MAX_SCHEMA_DEPTH: usize = 32; +const MAX_PARAMETER_BYTES: usize = 8 * 1024; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct Manifest { + schema_version: u32, + operations: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Operation { + pub tool: Tool, + pub http: HttpOperation, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct HttpOperation { + pub method: String, + pub path: String, + pub parameters: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Parameter { + pub name: String, + pub location: ParameterLocation, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ParameterLocation { + Path, + Query, +} + +#[derive(Debug)] +pub struct Catalog { + tools: Vec, + operations: BTreeMap, +} + +impl Catalog { + pub fn embedded() -> Result { + Self::parse(MANIFEST_JSON) + } + + fn parse(source: &str) -> Result { + let manifest: Manifest = serde_json::from_str(source) + .map_err(|error| format!("invalid generated LLM manifest: {error}"))?; + if manifest.schema_version != 1 { + return Err(format!( + "unsupported generated LLM manifest version {}", + manifest.schema_version + )); + } + if manifest.operations.is_empty() { + return Err( + "generated LLM manifest is empty; run the BRK bindgen target first".to_string(), + ); + } + + let mut tools = Vec::with_capacity(manifest.operations.len()); + let mut operations = BTreeMap::new(); + for operation in manifest.operations { + validate_operation(&operation)?; + let name = operation.tool.name.to_string(); + tools.push(operation.tool.clone()); + if operations.insert(name.clone(), operation).is_some() { + return Err(format!("duplicate generated tool name {name}")); + } + } + tools.sort_unstable_by(|left, right| left.name.cmp(&right.name)); + + Ok(Self { tools, operations }) + } + + pub fn tools(&self) -> &[Tool] { + &self.tools + } + + pub fn operation(&self, name: &str) -> Option<&Operation> { + self.operations.get(name) + } +} + +impl Operation { + pub fn validate_arguments<'a>( + &'a self, + arguments: &'a Map, + ) -> Result<&'a Map, String> { + let root = Value::Object(self.tool.input_schema.as_ref().clone()); + let value = Value::Object(arguments.clone()); + validate_schema(&root, &root, &value, 0) + .map_err(|error| format!("invalid arguments: {error}"))?; + Ok(arguments) + } +} + +fn validate_operation(operation: &Operation) -> Result<(), String> { + if operation.http.method != "GET" { + return Err(format!( + "tool {} is not a read-only GET operation", + operation.tool.name + )); + } + let path = &operation.http.path; + if !path.starts_with('/') + || path.starts_with("//") + || path.contains('?') + || path.contains('#') + || path.split('/').any(|segment| segment == "..") + { + return Err(format!( + "tool {} has an unsafe generated path", + operation.tool.name + )); + } + + let root = operation.tool.input_schema.as_ref(); + if root.get("type").and_then(Value::as_str) != Some("object") { + return Err(format!( + "tool {} input schema must have object type", + operation.tool.name + )); + } + let properties = root + .get("properties") + .and_then(Value::as_object) + .ok_or_else(|| { + format!( + "tool {} input schema has no properties", + operation.tool.name + ) + })?; + + let mut names = BTreeSet::new(); + for parameter in &operation.http.parameters { + if !names.insert(¶meter.name) { + return Err(format!( + "tool {} repeats parameter {}", + operation.tool.name, parameter.name + )); + } + if !properties.contains_key(¶meter.name) { + return Err(format!( + "tool {} maps unknown parameter {}", + operation.tool.name, parameter.name + )); + } + let placeholder = format!("{{{}}}", parameter.name); + match parameter.location { + ParameterLocation::Path if !path.contains(&placeholder) => { + return Err(format!( + "tool {} path omits placeholder {placeholder}", + operation.tool.name + )); + } + ParameterLocation::Query if path.contains(&placeholder) => { + return Err(format!( + "tool {} query parameter {} is used in its path", + operation.tool.name, parameter.name + )); + } + _ => {} + } + } + if properties.len() != names.len() { + return Err(format!( + "tool {} contains an unmapped input property", + operation.tool.name + )); + } + if path.contains('{') || path.contains('}') { + for segment in path.split('/') { + if let Some(name) = segment + .strip_prefix('{') + .and_then(|value| value.strip_suffix('}')) + && !operation.http.parameters.iter().any(|parameter| { + parameter.location == ParameterLocation::Path && parameter.name == name + }) + { + return Err(format!( + "tool {} contains unmapped path placeholder {name}", + operation.tool.name + )); + } + } + } + + validate_schema_definition( + &Value::Object(operation.tool.input_schema.as_ref().clone()), + 0, + ) + .map_err(|error| { + format!( + "tool {} has invalid input schema: {error}", + operation.tool.name + ) + })?; + if let Some(output_schema) = &operation.tool.output_schema { + validate_schema_definition(&Value::Object(output_schema.as_ref().clone()), 0).map_err( + |error| { + format!( + "tool {} has invalid output schema: {error}", + operation.tool.name + ) + }, + )?; + } + Ok(()) +} + +fn validate_schema_definition(schema: &Value, depth: usize) -> Result<(), String> { + if depth > MAX_SCHEMA_DEPTH { + return Err("schema exceeds the maximum nesting depth".to_string()); + } + if schema.is_boolean() { + return Ok(()); + } + let object = schema + .as_object() + .ok_or_else(|| "schemas must be JSON objects".to_string())?; + if let Some(reference) = object.get("$ref").and_then(Value::as_str) + && !reference.starts_with("#/$defs/") + { + return Err(format!("non-local schema reference {reference}")); + } + for key in ["properties", "$defs"] { + if let Some(values) = object.get(key).and_then(Value::as_object) { + for nested in values.values() { + validate_schema_definition(nested, depth + 1)?; + } + } + } + if let Some(items) = object.get("items") { + validate_schema_definition(items, depth + 1)?; + } + for key in ["allOf", "anyOf", "oneOf"] { + if let Some(values) = object.get(key).and_then(Value::as_array) { + for nested in values { + validate_schema_definition(nested, depth + 1)?; + } + } + } + if let Some(not) = object.get("not") { + validate_schema_definition(not, depth + 1)?; + } + Ok(()) +} + +fn validate_schema( + root: &Value, + schema: &Value, + value: &Value, + depth: usize, +) -> Result<(), String> { + if depth > MAX_SCHEMA_DEPTH { + return Err("value exceeds the maximum schema depth".to_string()); + } + let object = schema + .as_object() + .ok_or_else(|| "encountered a non-object schema".to_string())?; + + if let Some(reference) = object.get("$ref").and_then(Value::as_str) { + let pointer = reference + .strip_prefix('#') + .ok_or_else(|| format!("external schema reference {reference} is forbidden"))?; + let target = root + .pointer(pointer) + .ok_or_else(|| format!("missing schema reference {reference}"))?; + validate_schema(root, target, value, depth + 1)?; + } + + if let Some(expected) = object.get("const") + && expected != value + { + return Err("value does not match const".to_string()); + } + if let Some(values) = object.get("enum").and_then(Value::as_array) + && !values.contains(value) + { + return Err("value is not in the allowed enum".to_string()); + } + + if let Some(schemas) = object.get("allOf").and_then(Value::as_array) { + for nested in schemas { + validate_schema(root, nested, value, depth + 1)?; + } + } + if let Some(schemas) = object.get("anyOf").and_then(Value::as_array) + && !schemas + .iter() + .any(|nested| validate_schema(root, nested, value, depth + 1).is_ok()) + { + return Err("value does not match any allowed schema".to_string()); + } + if let Some(schemas) = object.get("oneOf").and_then(Value::as_array) + && schemas + .iter() + .filter(|nested| validate_schema(root, nested, value, depth + 1).is_ok()) + .count() + != 1 + { + return Err("value does not match exactly one allowed schema".to_string()); + } + if let Some(nested) = object.get("not") + && validate_schema(root, nested, value, depth + 1).is_ok() + { + return Err("value matches a forbidden schema".to_string()); + } + + if let Some(expected) = object.get("type") { + let valid = match expected { + Value::String(expected) => matches_type(expected, value), + Value::Array(expected) => expected + .iter() + .filter_map(Value::as_str) + .any(|expected| matches_type(expected, value)), + _ => false, + }; + if !valid { + return Err(format!("expected type {expected}")); + } + } + + match value { + Value::String(value) => validate_string(object, value), + Value::Number(value) => validate_number(object, value), + Value::Array(value) => validate_array(root, object, value, depth), + Value::Object(value) => validate_object(root, object, value, depth), + Value::Null | Value::Bool(_) => Ok(()), + } +} + +fn matches_type(expected: &str, value: &Value) -> bool { + match expected { + "null" => value.is_null(), + "boolean" => value.is_boolean(), + "object" => value.is_object(), + "array" => value.is_array(), + "number" => value.is_number(), + "integer" => value.as_i64().is_some() || value.as_u64().is_some(), + "string" => value.is_string(), + _ => false, + } +} + +fn validate_string(schema: &Map, value: &str) -> Result<(), String> { + let length = value.chars().count() as u64; + if value.len() > MAX_PARAMETER_BYTES { + return Err(format!( + "string exceeds the {MAX_PARAMETER_BYTES}-byte parameter limit" + )); + } + if let Some(minimum) = schema.get("minLength").and_then(Value::as_u64) + && length < minimum + { + return Err(format!("string is shorter than {minimum} characters")); + } + if let Some(maximum) = schema.get("maxLength").and_then(Value::as_u64) + && length > maximum + { + return Err(format!("string is longer than {maximum} characters")); + } + Ok(()) +} + +fn validate_number(schema: &Map, value: &serde_json::Number) -> Result<(), String> { + let value = value + .as_f64() + .ok_or_else(|| "number cannot be represented safely".to_string())?; + if !value.is_finite() { + return Err("number must be finite".to_string()); + } + if let Some(minimum) = schema.get("minimum").and_then(Value::as_f64) + && value < minimum + { + return Err(format!("number is less than {minimum}")); + } + if let Some(maximum) = schema.get("maximum").and_then(Value::as_f64) + && value > maximum + { + return Err(format!("number is greater than {maximum}")); + } + if let Some(minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64) + && value <= minimum + { + return Err(format!("number must be greater than {minimum}")); + } + if let Some(maximum) = schema.get("exclusiveMaximum").and_then(Value::as_f64) + && value >= maximum + { + return Err(format!("number must be less than {maximum}")); + } + Ok(()) +} + +fn validate_array( + root: &Value, + schema: &Map, + values: &[Value], + depth: usize, +) -> Result<(), String> { + if let Some(minimum) = schema.get("minItems").and_then(Value::as_u64) + && values.len() < minimum as usize + { + return Err(format!("array has fewer than {minimum} items")); + } + if let Some(maximum) = schema.get("maxItems").and_then(Value::as_u64) + && values.len() > maximum as usize + { + return Err(format!("array has more than {maximum} items")); + } + if schema + .get("uniqueItems") + .and_then(Value::as_bool) + .unwrap_or(false) + { + for (index, value) in values.iter().enumerate() { + if values[..index].contains(value) { + return Err("array items must be unique".to_string()); + } + } + } + if let Some(items) = schema.get("items") { + for value in values { + validate_schema(root, items, value, depth + 1)?; + } + } + Ok(()) +} + +fn validate_object( + root: &Value, + schema: &Map, + values: &Map, + depth: usize, +) -> Result<(), String> { + if let Some(required) = schema.get("required").and_then(Value::as_array) { + for name in required.iter().filter_map(Value::as_str) { + if !values.contains_key(name) { + return Err(format!("missing required property {name}")); + } + } + } + + let properties = schema + .get("properties") + .and_then(Value::as_object) + .cloned() + .unwrap_or_default(); + for (name, value) in values { + if let Some(property_schema) = properties.get(name) { + validate_schema(root, property_schema, value, depth + 1)?; + continue; + } + match schema.get("additionalProperties") { + Some(Value::Bool(false)) => return Err(format!("unknown property {name}")), + Some(additional @ Value::Object(_)) => { + validate_schema(root, additional, value, depth + 1)?; + } + _ => {} + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rmcp::model::ToolAnnotations; + use serde_json::json; + use std::sync::Arc; + + fn operation() -> Operation { + let input_schema = json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "id": { "type": "integer", "minimum": 1 }, + "format": { "enum": ["json", "csv"] } + }, + "required": ["id"], + "additionalProperties": false + }) + .as_object() + .unwrap() + .clone(); + let output_schema = json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": true + }) + .as_object() + .unwrap() + .clone(); + Operation { + tool: Tool::new_with_raw("get_item", None, Arc::new(input_schema)) + .with_raw_output_schema(Arc::new(output_schema)) + .with_annotations( + ToolAnnotations::new() + .read_only(true) + .destructive(false) + .idempotent(true) + .open_world(true), + ), + http: HttpOperation { + method: "GET".to_string(), + path: "/api/items/{id}".to_string(), + parameters: vec![ + Parameter { + name: "id".to_string(), + location: ParameterLocation::Path, + }, + Parameter { + name: "format".to_string(), + location: ParameterLocation::Query, + }, + ], + }, + } + } + + #[test] + fn validates_generated_operations_and_arguments() { + let operation = operation(); + validate_operation(&operation).unwrap(); + operation + .validate_arguments(json!({ "id": 1, "format": "json" }).as_object().unwrap()) + .unwrap(); + assert!( + operation + .validate_arguments(json!({ "id": 0 }).as_object().unwrap()) + .is_err() + ); + assert!( + operation + .validate_arguments(json!({ "id": 1, "other": true }).as_object().unwrap()) + .is_err() + ); + } +} diff --git a/crates/brk_mcp/src/server.rs b/crates/brk_mcp/src/server.rs new file mode 100644 index 000000000..6a1481e1a --- /dev/null +++ b/crates/brk_mcp/src/server.rs @@ -0,0 +1,343 @@ +use std::{ + borrow::Cow, + sync::{Arc, Mutex}, + time::Instant, +}; + +use axum::{ + Router, + body::Body, + extract::State, + http::{ + Request, StatusCode, + header::{ORIGIN, RETRY_AFTER}, + }, + middleware::{self, Next}, + response::{IntoResponse, Response}, +}; +use base64::{Engine as _, prelude::BASE64_STANDARD}; +use rmcp::{ + ErrorData as McpError, RoleServer, ServerHandler, + model::{ + CacheScope, CallToolRequestParams, CallToolResponse, CallToolResult, ContentBlock, + DiscoverResult, Implementation, ListToolsResult, MetaObject, PaginatedRequestParams, + ProtocolVersion, ResourceContents, ServerCapabilities, ServerInfo, Tool, + }, + service::RequestContext, + transport::streamable_http_server::{ + StreamableHttpServerConfig, StreamableHttpService, session::never::NeverSessionManager, + }, +}; +use serde_json::{Value, json}; +use tokio::sync::Semaphore; +use tracing::{info, warn}; + +use crate::{ + manifest::Catalog, + upstream::{Upstream, UpstreamResponse}, +}; + +const CACHE_META_KEY: &str = "space.bitview/upstreamCache"; +const MAX_REQUEST_BYTES: usize = 64 * 1024; +const MAX_CONCURRENCY: usize = 16; +const REQUESTS_PER_SECOND: u32 = 50; +const CATALOG_TTL_MS: u64 = 3_600_000; +const MAX_UPSTREAM_ERROR_BYTES: usize = 2_048; +static SUPPORTED_PROTOCOL_VERSIONS: [ProtocolVersion; 1] = [ProtocolVersion::V_2026_07_28]; + +struct AppState { + catalog: Catalog, + upstream: Upstream, + concurrency: Arc, + rate_limit: Mutex, +} + +struct RateLimit { + window_started: Instant, + used: u32, +} + +#[derive(Clone)] +struct BrkMcp { + state: Arc, +} + +impl AppState { + fn new(api_bases: Vec, catalog: Catalog) -> Self { + let upstream = Upstream::new(api_bases); + Self { + concurrency: Arc::new(Semaphore::new(MAX_CONCURRENCY)), + rate_limit: Mutex::new(RateLimit { + window_started: Instant::now(), + used: 0, + }), + catalog, + upstream, + } + } + + fn admit(&self) -> bool { + let mut limit = self.rate_limit.lock().expect("rate limit mutex poisoned"); + if limit.window_started.elapsed().as_secs() >= 1 { + limit.window_started = Instant::now(); + limit.used = 0; + } + if limit.used >= REQUESTS_PER_SECOND { + return false; + } + limit.used += 1; + true + } +} + +pub fn router(api_bases: Vec, catalog: Catalog) -> Router { + let transport_config = StreamableHttpServerConfig::default() + .disable_allowed_hosts() + .with_sse_keep_alive(None) + .with_sse_retry(None) + .with_legacy_session_mode(false) + .with_json_response(true) + .with_max_request_body_bytes(MAX_REQUEST_BYTES) + .with_stateless_protocol_metadata_required(true); + + let state = Arc::new(AppState::new(api_bases, catalog)); + let handler = BrkMcp { + state: state.clone(), + }; + let service = StreamableHttpService::new( + move || Ok(handler.clone()), + Arc::new(NeverSessionManager::default()), + transport_config, + ); + + Router::new() + .route_service("/mcp", service) + .layer(middleware::from_fn_with_state(state, gateway_guard)) +} + +impl BrkMcp { + fn server_info(&self) -> ServerInfo { + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + .with_protocol_version(ProtocolVersion::V_2026_07_28) + .with_server_info(Implementation::new("brk_mcp", env!("CARGO_PKG_VERSION"))) + .with_instructions( + "Read-only Bitcoin analytics tools backed by BRK REST GET endpoints. \ + Calls use the configured public Cloudflare-cached API and never mutate BRK state. \ + Tool descriptions preserve the OpenAPI documentation and identify their underlying \ + REST operation. Use available list, search, and info operations to discover \ + identifiers before querying dynamic datasets. Binary responses are returned as \ + embedded MCP resources.", + ) + } + + fn tool_error(&self, message: impl Into) -> CallToolResponse { + CallToolResult::error(vec![ContentBlock::text(message)]) + .with_meta(Some(server_meta())) + .into() + } + + fn render_upstream( + &self, + response: UpstreamResponse, + has_output_schema: bool, + ) -> CallToolResponse { + if !(200..300).contains(&response.status) { + let detail = String::from_utf8_lossy( + &response.body[..response.body.len().min(MAX_UPSTREAM_ERROR_BYTES)], + ); + let detail = detail.trim(); + let message = if detail.is_empty() { + format!("BRK API returned HTTP {}", response.status) + } else { + format!("BRK API returned HTTP {}: {detail}", response.status) + }; + return self.tool_error(message); + } + + let content_type = response + .content_type + .split(';') + .next() + .unwrap_or("application/octet-stream") + .trim() + .to_ascii_lowercase(); + let meta = Some(upstream_meta(&response)); + + if content_type == "application/json" || content_type.ends_with("+json") { + return match serde_json::from_slice::(&response.body) { + Ok(value) => CallToolResult::structured(value).with_meta(meta).into(), + Err(_) => self.tool_error("BRK API returned invalid JSON"), + }; + } + + if content_type.starts_with("text/") { + return match String::from_utf8(response.body) { + Ok(text) => { + let mut result = + CallToolResult::success(vec![ContentBlock::text(text.clone())]); + if has_output_schema { + result.structured_content = Some(Value::String(text)); + } + result.with_meta(meta).into() + } + Err(_) => self.tool_error("BRK API returned invalid UTF-8 text"), + }; + } + + let resource = ResourceContents::blob(BASE64_STANDARD.encode(response.body), response.url) + .with_mime_type(content_type); + CallToolResult::success(vec![ContentBlock::resource(resource)]) + .with_meta(meta) + .into() + } +} + +impl ServerHandler for BrkMcp { + fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> { + Cow::Borrowed(&SUPPORTED_PROTOCOL_VERSIONS) + } + + fn discover( + &self, + _context: RequestContext, + ) -> impl Future> + Send + '_ { + std::future::ready(Ok(DiscoverResult::from_server_info( + SUPPORTED_PROTOCOL_VERSIONS.to_vec(), + self.server_info(), + ) + .with_ttl_ms(CATALOG_TTL_MS) + .with_cache_scope(CacheScope::Public))) + } + + fn list_tools( + &self, + request: Option, + _context: RequestContext, + ) -> impl Future> + Send + '_ { + let result = if request.and_then(|request| request.cursor).is_some() { + Err(McpError::invalid_params( + "This complete tool catalog does not accept a cursor", + None, + )) + } else { + let mut result = ListToolsResult::with_all_items(self.state.catalog.tools().to_vec()) + .with_ttl_ms(CATALOG_TTL_MS) + .with_cache_scope(CacheScope::Public); + result.meta = Some(server_meta()); + Ok(result) + }; + std::future::ready(result) + } + + fn get_tool(&self, name: &str) -> Option { + self.state + .catalog + .operation(name) + .map(|operation| operation.tool.clone()) + } + + async fn call_tool( + &self, + request: CallToolRequestParams, + _context: RequestContext, + ) -> Result { + let name = request.name; + let operation = self + .state + .catalog + .operation(&name) + .ok_or_else(|| McpError::invalid_params("Unknown tool name", None))?; + let arguments = request.arguments.unwrap_or_default(); + let arguments = operation + .validate_arguments(&arguments) + .map_err(|error| McpError::invalid_params(error, None))?; + let has_output_schema = operation.tool.output_schema.is_some(); + let prepared = self + .state + .upstream + .prepare(operation, arguments) + .map_err(|error| McpError::invalid_params(error, None))?; + let permit = self + .state + .concurrency + .clone() + .try_acquire_owned() + .map_err(|_| McpError::internal_error("MCP proxy is at its concurrency limit", None))?; + + let upstream = self.state.upstream.clone(); + let response = match tokio::task::spawn_blocking(move || { + let _permit = permit; + upstream.fetch(prepared) + }) + .await + { + Ok(Ok(response)) => response, + Ok(Err(error)) => return Ok(self.tool_error(error)), + Err(_) => return Ok(self.tool_error("BRK API request task failed")), + }; + if let Some(cache_status) = &response.cache_status { + info!( + tool = name.as_ref(), + cache_status, + cache_age = response.cache_age.as_deref().unwrap_or(""), + "BRK API response" + ); + } else { + warn!( + tool = name.as_ref(), + "BRK API response did not include CF-Cache-Status" + ); + } + Ok(self.render_upstream(response, has_output_schema)) + } + + fn get_info(&self) -> ServerInfo { + self.server_info() + } +} + +async fn gateway_guard( + State(state): State>, + request: Request, + next: Next, +) -> Response { + if request.headers().contains_key(ORIGIN) { + return (StatusCode::FORBIDDEN, "Forbidden origin").into_response(); + } + + if !state.admit() { + let mut response = + (StatusCode::TOO_MANY_REQUESTS, "Request rate limit exceeded").into_response(); + response.headers_mut().insert( + RETRY_AFTER, + "1".parse().expect("static retry header is valid"), + ); + return response; + } + + next.run(request).await +} + +fn server_meta() -> MetaObject { + let mut meta = MetaObject::new(); + meta.insert( + "io.modelcontextprotocol/serverInfo".to_string(), + json!({ + "name": "brk_mcp", + "version": env!("CARGO_PKG_VERSION"), + }), + ); + meta +} + +fn upstream_meta(response: &UpstreamResponse) -> MetaObject { + let mut meta = server_meta(); + meta.insert( + CACHE_META_KEY.to_string(), + json!({ + "status": response.cache_status, + "age": response.cache_age, + }), + ); + meta +} diff --git a/crates/brk_mcp/src/upstream.rs b/crates/brk_mcp/src/upstream.rs new file mode 100644 index 000000000..580d09e98 --- /dev/null +++ b/crates/brk_mcp/src/upstream.rs @@ -0,0 +1,262 @@ +use std::time::Duration; + +use serde_json::{Map, Value}; + +use crate::manifest::{Operation, ParameterLocation}; + +const MAX_UPSTREAM_URL_BYTES: usize = 32 * 1024; +const MAX_RESPONSE_BYTES: usize = 8 * 1024 * 1024; +const UPSTREAM_TIMEOUT: Duration = Duration::from_secs(15); + +#[derive(Clone)] +pub struct Upstream { + agent: ureq::Agent, + api_bases: Vec, +} + +pub struct PreparedRequest { + path: String, + query: Vec<(String, String)>, +} + +pub struct UpstreamResponse { + pub url: String, + pub status: u16, + pub content_type: String, + pub body: Vec, + pub cache_status: Option, + pub cache_age: Option, +} + +impl Upstream { + pub fn new(api_bases: Vec) -> Self { + let config = ureq::Agent::config_builder() + .timeout_global(Some(UPSTREAM_TIMEOUT)) + .http_status_as_error(false) + .max_redirects(0) + .user_agent(format!("brk-mcp/{}", env!("CARGO_PKG_VERSION"))) + .build(); + Self { + agent: ureq::Agent::new_with_config(config), + api_bases, + } + } + + pub fn prepare( + &self, + operation: &Operation, + arguments: &Map, + ) -> Result { + let mut path = operation.http.path.clone(); + let mut query = Vec::new(); + + for parameter in &operation.http.parameters { + let Some(value) = arguments.get(¶meter.name) else { + continue; + }; + if value.is_null() { + continue; + } + let values = parameter_values(value)?; + match parameter.location { + ParameterLocation::Path => { + if values.len() != 1 { + return Err(format!( + "path parameter {} must contain one scalar value", + parameter.name + )); + } + path = path.replace( + &format!("{{{}}}", parameter.name), + &encode_path_segment(&values[0]), + ); + } + ParameterLocation::Query => { + query.extend( + values + .into_iter() + .map(|value| (parameter.name.clone(), value)), + ); + } + } + } + + if path.contains('{') || path.contains('}') { + return Err("a required path parameter is missing".to_string()); + } + let query_bytes = query + .iter() + .map(|(name, value)| name.len() + value.len() + 2) + .sum::(); + let longest_base = self.api_bases.iter().map(String::len).max().unwrap_or(0); + if longest_base + .saturating_add(path.len()) + .saturating_add(query_bytes) + > MAX_UPSTREAM_URL_BYTES + { + return Err(format!( + "upstream URL exceeds the {MAX_UPSTREAM_URL_BYTES}-byte limit" + )); + } + + Ok(PreparedRequest { path, query }) + } + + pub fn fetch(&self, request: PreparedRequest) -> Result { + let PreparedRequest { path, query } = request; + let mut last_error = None; + for api_base in &self.api_bases { + let url = format!("{api_base}{path}"); + let response = self + .agent + .get(&url) + .query_pairs(query.iter().cloned()) + .header( + "Accept", + "application/json, text/plain, text/csv, application/octet-stream", + ) + .call(); + match response { + Ok(response) => return self.read_response(url, response), + Err(error) => last_error = Some(error), + } + } + Err(format!( + "BRK API request failed: {}", + last_error + .map(|error| error.to_string()) + .unwrap_or_else(|| "no API base URL configured".to_string()) + )) + } + + fn read_response( + &self, + url: String, + mut response: ureq::http::Response, + ) -> Result { + let status = response.status().as_u16(); + let content_type = response + .headers() + .get("content-type") + .and_then(|value| value.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + let cache_status = response + .headers() + .get("cf-cache-status") + .and_then(|value| value.to_str().ok()) + .map(str::to_string); + let cache_age = response + .headers() + .get("age") + .and_then(|value| value.to_str().ok()) + .map(str::to_string); + let body = response + .body_mut() + .with_config() + .limit(u64::try_from(MAX_RESPONSE_BYTES).unwrap_or(u64::MAX)) + .read_to_vec() + .map_err(|error| { + format!( + "BRK API response could not be read within the {}-byte limit: {error}", + MAX_RESPONSE_BYTES + ) + })?; + + Ok(UpstreamResponse { + url, + status, + content_type, + body, + cache_status, + cache_age, + }) + } +} + +fn parameter_values(value: &Value) -> Result, String> { + match value { + Value::String(value) => Ok(vec![value.clone()]), + Value::Number(value) => Ok(vec![value.to_string()]), + Value::Bool(value) => Ok(vec![value.to_string()]), + Value::Array(values) => values + .iter() + .map(|value| match value { + Value::String(value) => Ok(value.clone()), + Value::Number(value) => Ok(value.to_string()), + Value::Bool(value) => Ok(value.to_string()), + _ => Err("URL parameter arrays may contain only scalar values".to_string()), + }) + .collect(), + Value::Null => Ok(Vec::new()), + Value::Object(_) => Err("URL parameters may not be objects".to_string()), + } +} + +fn encode_path_segment(value: &str) -> String { + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + let mut encoded = String::with_capacity(value.len()); + for byte in value.bytes() { + if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') { + encoded.push(char::from(byte)); + } else { + encoded.push('%'); + encoded.push(char::from(HEX[(byte >> 4) as usize])); + encoded.push(char::from(HEX[(byte & 0x0f) as usize])); + } + } + encoded +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + io::{Read, Write}, + net::TcpListener, + thread, + }; + + #[test] + fn encodes_path_segments_without_leaving_separators() { + assert_eq!(encode_path_segment("a/b c"), "a%2Fb%20c"); + assert_eq!(encode_path_segment("ż"), "%C5%BC"); + } + + #[test] + fn falls_back_to_the_second_base_after_a_transport_failure() { + let unavailable = TcpListener::bind("127.0.0.1:0").unwrap(); + let unavailable_address = unavailable.local_addr().unwrap(); + drop(unavailable); + + let fallback = TcpListener::bind("127.0.0.1:0").unwrap(); + let fallback_address = fallback.local_addr().unwrap(); + let server = thread::spawn(move || { + let (mut stream, _) = fallback.accept().unwrap(); + let mut request = [0_u8; 1024]; + let read = stream.read(&mut request).unwrap(); + assert!(String::from_utf8_lossy(&request[..read]).starts_with("GET /health HTTP/1.1")); + stream + .write_all( + b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}", + ) + .unwrap(); + }); + + let upstream = Upstream::new(vec![ + format!("http://{unavailable_address}"), + format!("http://{fallback_address}"), + ]); + let response = upstream + .fetch(PreparedRequest { + path: "/health".to_string(), + query: Vec::new(), + }) + .unwrap(); + + server.join().unwrap(); + assert_eq!(response.status, 200); + assert_eq!(response.body, br#"{"ok":true}"#); + assert_eq!(response.url, format!("http://{fallback_address}/health")); + } +} diff --git a/crates/brk_server/src/api/addrs.rs b/crates/brk_server/src/api/addrs.rs index 01f830298..a342efe1c 100644 --- a/crates/brk_server/src/api/addrs.rs +++ b/crates/brk_server/src/api/addrs.rs @@ -41,7 +41,7 @@ impl AddrRoutes for ApiRouter { .id("get_address_hash_prefix_matches") .addrs_tag() .summary("Address hash-prefix matches") - .description("Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.") + .description("Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata with `GET /api/address/{address}`.") .json_response::() .not_modified() .bad_request() @@ -97,7 +97,7 @@ impl AddrRoutes for ApiRouter { .id("get_address_txs") .addrs_tag() .summary("Address transactions") - .description("Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, use `/address/{address}/txs/chain/{last_seen_txid}`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*") + .description("Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*") .json_response::>() .not_modified() .bad_request() @@ -120,7 +120,7 @@ impl AddrRoutes for ApiRouter { .id("get_address_confirmed_txs") .addrs_tag() .summary("Address confirmed transactions") - .description("Get the first 25 confirmed transactions for an address. For pagination, use the path-style form `/txs/chain/{last_seen_txid}`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*") + .description("Get the first 25 confirmed transactions for an address. For pagination, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*") .json_response::>() .not_modified() .bad_request() diff --git a/crates/brk_server/src/api/mempool.rs b/crates/brk_server/src/api/mempool.rs index 2f99918fc..02d3e0646 100644 --- a/crates/brk_server/src/api/mempool.rs +++ b/crates/brk_server/src/api/mempool.rs @@ -131,7 +131,7 @@ impl MempoolRoutes for ApiRouter { op.id("get_fullrbf_replacements") .mempool_tag() .summary("Recent full-RBF replacements") - .description("Like `/api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)*") + .description("Same response shape as `GET /api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF).\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)*") .json_response::>() .not_modified() .server_error() @@ -152,7 +152,7 @@ impl MempoolRoutes for ApiRouter { op.id("get_block_template") .mempool_tag() .summary("Projected next block template") - .description("Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template.") + .description("Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template.") .json_response::() .not_modified() .server_error() @@ -177,7 +177,7 @@ impl MempoolRoutes for ApiRouter { op.id("get_block_template_diff") .mempool_tag() .summary("Block template diff since hash") - .description("Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`.") + .description("Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `GET /api/v1/mempool/block-template`.") .json_response::() .not_modified() .not_found() diff --git a/crates/brk_server/src/api/mod.rs b/crates/brk_server/src/api/mod.rs index 2b9bdb624..c4a5b8d60 100644 --- a/crates/brk_server/src/api/mod.rs +++ b/crates/brk_server/src/api/mod.rs @@ -92,7 +92,7 @@ impl ApiRoutes for ApiRouter { .description( "Compact OpenAPI specification optimized for LLM consumption. \ Removes redundant fields while preserving essential API information. \ - Full spec available at `/openapi.json`.", + The full specification is available at `GET /openapi.json`.", ) .json_response::() }, diff --git a/crates/brk_server/src/api/openapi/mod.rs b/crates/brk_server/src/api/openapi/mod.rs index 294f6ce9d..417666994 100644 --- a/crates/brk_server/src/api/openapi/mod.rs +++ b/crates/brk_server/src/api/openapi/mod.rs @@ -187,7 +187,7 @@ All errors return structured JSON with a consistent format: or at any confirmed height: `raw` bins every output by value with no filtering, \ while `ema` is the smoothed round-dollar window the price is read from. The live \ price is also at `/api/mempool/price`. Confirmed per-height price history is at \ - `/api/vecs/height-to-price`." + `/api/series/price/height`." .to_string(), ), ..Default::default() diff --git a/crates/brk_server/src/api/oracle.rs b/crates/brk_server/src/api/oracle.rs index 06c173ae8..1c448ef98 100644 --- a/crates/brk_server/src/api/oracle.rs +++ b/crates/brk_server/src/api/oracle.rs @@ -33,8 +33,8 @@ impl OracleRoutes for ApiRouter { .summary("Live BTC/USD price") .description( "Current BTC/USD price in dollars. Same value as \ - `/api/mempool/price`. Confirmed per-height history is available at \ - `/api/vecs/height-to-price`.", + `GET /api/mempool/price`. Confirmed per-height history is available at \ + `GET /api/series/price/height`.", ) .json_response::() .not_modified() diff --git a/crates/brk_server/src/api/series.rs b/crates/brk_server/src/api/series.rs index 62ecf38b5..fdd1061f5 100644 --- a/crates/brk_server/src/api/series.rs +++ b/crates/brk_server/src/api/series.rs @@ -289,7 +289,8 @@ impl ApiSeriesRoutes for ApiRouter { .summary("Get raw series data") .description( "Returns just the data array without the SeriesData wrapper. \ - Supports the same range and format parameters as the standard endpoint." + Supports the same range and format parameters as \ + `GET /api/series/{series}/{index}`." ) .json_response::>() .csv_response() diff --git a/crates/brk_server/src/api/server.rs b/crates/brk_server/src/api/server.rs index 0c52978fb..930539afa 100644 --- a/crates/brk_server/src/api/server.rs +++ b/crates/brk_server/src/api/server.rs @@ -51,7 +51,7 @@ impl ServerRoutes for ApiRouter { op.id("get_health") .server_tag() .summary("Health check") - .description("Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`.") + .description("Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, request `GET /api/server/sync`.") .json_response::() }, ), diff --git a/crates/brk_server/src/api/urpd.rs b/crates/brk_server/src/api/urpd.rs index 44eb47336..2c73813f9 100644 --- a/crates/brk_server/src/api/urpd.rs +++ b/crates/brk_server/src/api/urpd.rs @@ -94,9 +94,11 @@ impl ApiUrpdRoutes for ApiRouter { .summary("Latest URPD") .description( "URPD for the most recent available date in the cohort. \ - The response's `date` field echoes which date was served.\n\n\ - See the URPD tag description for the response shape, `agg`, and \ - `weight` options.", + The response's `date` field echoes which date was served. Returns \ + `{ cohort, date, weight, aggregation, close, total_supply, buckets }`. \ + `close` and each bucket's `price_floor`, `realized_cap`, and \ + `unrealized_pnl` are USD; `total_supply` and bucket `supply` are BTC. \ + `unrealized_pnl` can be negative.", ) .json_response::() .not_modified() @@ -132,9 +134,9 @@ impl ApiUrpdRoutes for ApiRouter { .description( "URPD for a (cohort, date) pair. Returns \ `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where \ - each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`.\n\n\ - See the URPD tag description for unit conventions, `agg`, and \ - `weight` options.", + each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. \ + `close`, `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; \ + `total_supply` and `supply` are BTC. `unrealized_pnl` can be negative.", ) .json_response::() .not_modified() diff --git a/crates/brk_server/src/lib.rs b/crates/brk_server/src/lib.rs index 831d6da85..9f8fa4222 100644 --- a/crates/brk_server/src/lib.rs +++ b/crates/brk_server/src/lib.rs @@ -335,7 +335,10 @@ pub fn generate_bindings( ) -> std::io::Result<()> { let openapi_json = serde_json::to_string(openapi) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - brk_bindgen::generate_clients(vecs, &openapi_json, output_paths) + let output_paths = output_paths.clone().llm_manifest( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../brk_mcp/generated/manifest.json"), + ); + brk_bindgen::generate_clients(vecs, &openapi_json, &output_paths) } #[cfg(test)] diff --git a/crates/brk_server/src/params/addr_hash_prefix_param.rs b/crates/brk_server/src/params/addr_hash_prefix_param.rs index 448ecc454..bc63dca25 100644 --- a/crates/brk_server/src/params/addr_hash_prefix_param.rs +++ b/crates/brk_server/src/params/addr_hash_prefix_param.rs @@ -5,5 +5,7 @@ use serde::Deserialize; #[derive(Deserialize, JsonSchema)] pub struct AddrHashPrefixParam { pub addr_type: OutputType, + /// First 1–16 hexadecimal nibbles of the RapidHash v3 hash over the raw + /// address payload bytes. pub prefix: String, } diff --git a/crates/brk_server/src/params/height_or_date_param.rs b/crates/brk_server/src/params/height_or_date_param.rs index 6283e6cfe..718920a1c 100644 --- a/crates/brk_server/src/params/height_or_date_param.rs +++ b/crates/brk_server/src/params/height_or_date_param.rs @@ -9,6 +9,8 @@ use crate::Error; /// per-day variant, choosing the matching cache strategy. #[derive(Deserialize, JsonSchema)] pub struct HeightOrDateParam { + /// Confirmed block height as decimal digits (`840000`) or calendar date in + /// `YYYY-MM-DD` format. #[schemars(example = &"840000")] pub point: String, } diff --git a/crates/brk_server/src/params/next_block_hash_param.rs b/crates/brk_server/src/params/next_block_hash_param.rs index 81d3d34ec..9ebc8faa5 100644 --- a/crates/brk_server/src/params/next_block_hash_param.rs +++ b/crates/brk_server/src/params/next_block_hash_param.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use brk_types::NextBlockHash; -/// `since` hash for `/api/v1/mempool/block-template/diff/{hash}`. +/// Prior-template hash for `GET /api/v1/mempool/block-template/diff/{hash}`. #[derive(Deserialize, JsonSchema)] pub struct NextBlockHashParam { pub hash: NextBlockHash, diff --git a/crates/brk_server/src/params/urpd_params.rs b/crates/brk_server/src/params/urpd_params.rs index 25d1eb87b..11b5f00e0 100644 --- a/crates/brk_server/src/params/urpd_params.rs +++ b/crates/brk_server/src/params/urpd_params.rs @@ -7,6 +7,7 @@ use brk_types::{Cohort, Date, UrpdAggregation, UrpdWeight}; #[derive(Deserialize, JsonSchema)] pub struct UrpdParams { pub cohort: Cohort, + /// Calendar date of the URPD snapshot in `YYYY-MM-DD` format. #[schemars(with = "String", example = &"2024-01-01")] pub date: Date, } diff --git a/crates/brk_types/src/block_template.rs b/crates/brk_types/src/block_template.rs index 8834ca686..696453b82 100644 --- a/crates/brk_types/src/block_template.rs +++ b/crates/brk_types/src/block_template.rs @@ -9,8 +9,7 @@ use crate::{MempoolBlock, NextBlockHash, Transaction}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct BlockTemplate { - /// Pass back as `` on - /// `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas. + /// Pass to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas. pub hash: NextBlockHash, /// Aggregate stats for this block (size, vsize, fee range, ...). diff --git a/crates/brk_types/src/next_block_hash.rs b/crates/brk_types/src/next_block_hash.rs index 6dd3fc91c..17e6de4c3 100644 --- a/crates/brk_types/src/next_block_hash.rs +++ b/crates/brk_types/src/next_block_hash.rs @@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize}; /// Content hash of the projected next block (block 0 of the mempool /// snapshot). Same value as the mempool ETag. Opaque token: pass back -/// as `since` on `/api/v1/mempool/block-template/diff/{hash}` to fetch -/// deltas. +/// to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas. #[derive( Debug, Default, diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index b91aa0467..fcc25d9dd 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -40,7 +40,8 @@ /** * @typedef {Object} AddrHashPrefixParam * @property {OutputType} addrType - * @property {string} prefix + * @property {string} prefix - First 1–16 hexadecimal nibbles of the RapidHash v3 hash over the raw +address payload bytes. */ /** * Address statistics in the mempool (unconfirmed transactions only) @@ -271,8 +272,7 @@ Matches mempool.space/bitcoin-cli behavior. * `GET /api/v1/mempool/block-template`. * * @typedef {Object} BlockTemplate - * @property {NextBlockHash} hash - Pass back as `` on -`/api/v1/mempool/block-template/diff/{hash}` to fetch deltas. + * @property {NextBlockHash} hash - Pass to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas. * @property {MempoolBlock} stats - Aggregate stats for this block (size, vsize, fee range, ...). * @property {Transaction[]} transactions - Full transaction bodies in `getblocktemplate` order. */ @@ -646,7 +646,8 @@ ancestors and no descendants (matches mempool.space). * per-day variant, choosing the matching cache strategy. * * @typedef {Object} HeightOrDateParam - * @property {string} point + * @property {string} point - Confirmed block height as decimal digits (`840000`) or calendar date in +`YYYY-MM-DD` format. */ /** * Block height path parameter @@ -764,13 +765,12 @@ ancestors and no descendants (matches mempool.space). /** * Content hash of the projected next block (block 0 of the mempool * snapshot). Same value as the mempool ETag. Opaque token: pass back - * as `since` on `/api/v1/mempool/block-template/diff/{hash}` to fetch - * deltas. + * to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas. * * @typedef {number} NextBlockHash */ /** - * `since` hash for `/api/v1/mempool/block-template/diff/{hash}`. + * Prior-template hash for `GET /api/v1/mempool/block-template/diff/{hash}`. * * @typedef {Object} NextBlockHashParam * @property {NextBlockHash} hash @@ -1410,7 +1410,7 @@ on serialization otherwise. * * @typedef {Object} UrpdParams * @property {Cohort} cohort - * @property {string} date + * @property {string} date - Calendar date of the URPD snapshot in `YYYY-MM-DD` format. */ /** * Query parameters for URPD endpoints. @@ -11470,7 +11470,7 @@ class BrkClient extends BrkClientBase { /** * Health check * - * Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`. + * Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, request `GET /api/server/sync`. * * Endpoint: `GET /health` * @param {{ signal?: AbortSignal, onValue?: (value: Health) => void, cache?: boolean, memCache?: boolean }} [options] @@ -11654,7 +11654,7 @@ class BrkClient extends BrkClientBase { /** * Get raw series data * - * Returns just the data array without the SeriesData wrapper. Supports the same range and format parameters as the standard endpoint. + * Returns just the data array without the SeriesData wrapper. Supports the same range and format parameters as `GET /api/series/{series}/{index}`. * * Endpoint: `GET /api/series/{series}/{index}/data` * @@ -11797,9 +11797,7 @@ class BrkClient extends BrkClientBase { /** * Latest URPD * - * URPD for the most recent available date in the cohort. The response's `date` field echoes which date was served. - * - * See the URPD tag description for the response shape, `agg`, and `weight` options. + * URPD for the most recent available date in the cohort. The response's `date` field echoes which date was served. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }`. `close` and each bucket's `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and bucket `supply` are BTC. `unrealized_pnl` can be negative. * * Endpoint: `GET /api/urpd/{cohort}` * @@ -11821,14 +11819,12 @@ class BrkClient extends BrkClientBase { /** * URPD at date * - * URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. - * - * See the URPD tag description for unit conventions, `agg`, and `weight` options. + * URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. `close`, `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and `supply` are BTC. `unrealized_pnl` can be negative. * * Endpoint: `GET /api/urpd/{cohort}/{date}` * * @param {Cohort} cohort - * @param {string} date + * @param {string} date - Calendar date of the URPD snapshot in `YYYY-MM-DD` format. * @param {UrpdAggregation=} [agg] - Aggregation strategy. Default: raw (no aggregation). Accepts `bucket` as alias. * @param {UrpdWeight=} [weight] - Supply weighting. Default: raw (unweighted). * @param {{ signal?: AbortSignal, onValue?: (value: Urpd) => void, cache?: boolean, memCache?: boolean }} [options] @@ -11899,12 +11895,13 @@ class BrkClient extends BrkClientBase { /** * Address hash-prefix matches * - * Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`. + * Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata with `GET /api/address/{address}`. * * Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}` * * @param {OutputType} addr_type - * @param {string} prefix + * @param {string} prefix - First 1–16 hexadecimal nibbles of the RapidHash v3 hash over the raw +address payload bytes. * @param {{ signal?: AbortSignal, onValue?: (value: AddrHashPrefixMatches) => void, cache?: boolean, memCache?: boolean }} [options] * @returns {Promise} */ @@ -11934,7 +11931,7 @@ class BrkClient extends BrkClientBase { /** * Address transactions * - * Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, use `/address/{address}/txs/chain/{last_seen_txid}`. + * Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* * @@ -11952,7 +11949,7 @@ class BrkClient extends BrkClientBase { /** * Address confirmed transactions * - * Get the first 25 confirmed transactions for an address. For pagination, use the path-style form `/txs/chain/{last_seen_txid}`. + * Get the first 25 confirmed transactions for an address. For pagination, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)* * @@ -12768,7 +12765,7 @@ class BrkClient extends BrkClientBase { /** * Recent full-RBF replacements * - * Like `/api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). + * Same response shape as `GET /api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)* * @@ -12784,7 +12781,7 @@ class BrkClient extends BrkClientBase { /** * Projected next block template * - * Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. + * Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. * * Endpoint: `GET /api/v1/mempool/block-template` * @param {{ signal?: AbortSignal, onValue?: (value: BlockTemplate) => void, cache?: boolean, memCache?: boolean }} [options] @@ -12798,7 +12795,7 @@ class BrkClient extends BrkClientBase { /** * Block template diff since hash * - * Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`. + * Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `GET /api/v1/mempool/block-template`. * * Endpoint: `GET /api/v1/mempool/block-template/diff/{hash}` * @@ -12828,7 +12825,7 @@ class BrkClient extends BrkClientBase { /** * Live BTC/USD price * - * Current BTC/USD price in dollars. Same value as `/api/mempool/price`. Confirmed per-height history is available at `/api/vecs/height-to-price`. + * Current BTC/USD price in dollars. Same value as `GET /api/mempool/price`. Confirmed per-height history is available at `GET /api/series/price/height`. * * Endpoint: `GET /api/oracle/price` * @param {{ signal?: AbortSignal, onValue?: (value: Dollars) => void, cache?: boolean, memCache?: boolean }} [options] @@ -12860,7 +12857,8 @@ class BrkClient extends BrkClientBase { * * Endpoint: `GET /api/oracle/histogram/payments/{point}` * - * @param {string} point + * @param {string} point - Confirmed block height as decimal digits (`840000`) or calendar date in +`YYYY-MM-DD` format. * @param {{ signal?: AbortSignal, onValue?: (value: number[]) => void, cache?: boolean, memCache?: boolean }} [options] * @returns {Promise} */ @@ -12890,7 +12888,8 @@ class BrkClient extends BrkClientBase { * * Endpoint: `GET /api/oracle/histogram/outputs/{point}` * - * @param {string} point + * @param {string} point - Confirmed block height as decimal digits (`840000`) or calendar date in +`YYYY-MM-DD` format. * @param {{ signal?: AbortSignal, onValue?: (value: number[]) => void, cache?: boolean, memCache?: boolean }} [options] * @returns {Promise} */ @@ -13152,7 +13151,7 @@ class BrkClient extends BrkClientBase { /** * Compact OpenAPI specification * - * Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`. + * Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. The full specification is available at `GET /openapi.json`. * * Endpoint: `GET /api.json` * @param {{ signal?: AbortSignal, onValue?: (value: *) => void, cache?: boolean, memCache?: boolean }} [options] diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 54b98296e..0733d6b9b 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -53,8 +53,7 @@ BlockHash = str BlockTxIndex = int # Content hash of the projected next block (block 0 of the mempool # snapshot). Same value as the mempool ETag. Opaque token: pass back -# as `since` on `/api/v1/mempool/block-template/diff/{hash}` to fetch -# deltas. +# to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas. NextBlockHash = int # Transaction locktime. Values below 500,000,000 are interpreted as block heights; values at or above are Unix timestamps. RawLockTime = int @@ -317,6 +316,11 @@ class AddrHashPrefixMatches(TypedDict): addresses: List[Addr] class AddrHashPrefixParam(TypedDict): + """ + Attributes: + prefix: First 1–16 hexadecimal nibbles of the RapidHash v3 hash over the raw +address payload bytes. + """ addr_type: OutputType prefix: str @@ -793,8 +797,7 @@ class BlockTemplate(TypedDict): `GET /api/v1/mempool/block-template`. Attributes: - hash: Pass back as `` on -`/api/v1/mempool/block-template/diff/{hash}` to fetch deltas. + hash: Pass to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas. stats: Aggregate stats for this block (size, vsize, fee range, ...). transactions: Full transaction bodies in `getblocktemplate` order. """ @@ -1165,6 +1168,10 @@ class HeightOrDateParam(TypedDict): Path parameter accepting either a block height (`840000`) or a calendar date (`YYYY-MM-DD`). The handler resolves it and dispatches to the per-height or per-day variant, choosing the matching cache strategy. + + Attributes: + point: Confirmed block height as decimal digits (`840000`) or calendar date in +`YYYY-MM-DD` format. """ point: str @@ -1265,7 +1272,7 @@ class MerkleProof(TypedDict): class NextBlockHashParam(TypedDict): """ - `since` hash for `/api/v1/mempool/block-template/diff/{hash}`. + Prior-template hash for `GET /api/v1/mempool/block-template/diff/{hash}`. """ hash: NextBlockHash @@ -1801,6 +1808,9 @@ class UrpdCohortParam(TypedDict): class UrpdParams(TypedDict): """ Path parameters for `/api/urpd/{cohort}/{date}`. + + Attributes: + date: Calendar date of the URPD snapshot in `YYYY-MM-DD` format. """ cohort: Cohort date: str @@ -8479,7 +8489,7 @@ class BrkClient(BrkClientBase): def get_health(self) -> Health: """Health check. - Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`. + Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, request `GET /api/server/sync`. Endpoint: `GET /health`""" return self.get_json('/health') @@ -8586,7 +8596,7 @@ class BrkClient(BrkClientBase): def get_series_data(self, series: SeriesName, index: Index, start: Optional[RangeIndex] = None, end: Optional[RangeIndex] = None, limit: Optional[Limit] = None, format: Optional[Format] = None) -> Union[List[bool], str]: """Get raw series data. - Returns just the data array without the SeriesData wrapper. Supports the same range and format parameters as the standard endpoint. + Returns just the data array without the SeriesData wrapper. Supports the same range and format parameters as `GET /api/series/{series}/{index}`. Endpoint: `GET /api/series/{series}/{index}/data`""" params = [] @@ -8666,9 +8676,7 @@ class BrkClient(BrkClientBase): def get_urpd(self, cohort: Cohort, agg: Optional[UrpdAggregation] = None, weight: Optional[UrpdWeight] = None) -> Urpd: """Latest URPD. - URPD for the most recent available date in the cohort. The response's `date` field echoes which date was served. - - See the URPD tag description for the response shape, `agg`, and `weight` options. + URPD for the most recent available date in the cohort. The response's `date` field echoes which date was served. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }`. `close` and each bucket's `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and bucket `supply` are BTC. `unrealized_pnl` can be negative. Endpoint: `GET /api/urpd/{cohort}`""" params = [] @@ -8681,9 +8689,7 @@ class BrkClient(BrkClientBase): def get_urpd_at(self, cohort: Cohort, date: str, agg: Optional[UrpdAggregation] = None, weight: Optional[UrpdWeight] = None) -> Urpd: """URPD at date. - URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. - - See the URPD tag description for unit conventions, `agg`, and `weight` options. + URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. `close`, `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and `supply` are BTC. `unrealized_pnl` can be negative. Endpoint: `GET /api/urpd/{cohort}/{date}`""" params = [] @@ -8730,7 +8736,7 @@ class BrkClient(BrkClientBase): def get_address_hash_prefix_matches(self, addr_type: OutputType, prefix: str) -> AddrHashPrefixMatches: """Address hash-prefix matches. - Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`. + Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata with `GET /api/address/{address}`. Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`""" return self.get_json(f'/api/address/hash-prefix/{addr_type}/{prefix}') @@ -8748,7 +8754,7 @@ class BrkClient(BrkClientBase): def get_address_txs(self, address: Addr) -> List[Transaction]: """Address transactions. - Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, use `/address/{address}/txs/chain/{last_seen_txid}`. + Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* @@ -8758,7 +8764,7 @@ class BrkClient(BrkClientBase): def get_address_confirmed_txs(self, address: Addr) -> List[Transaction]: """Address confirmed transactions. - Get the first 25 confirmed transactions for an address. For pagination, use the path-style form `/txs/chain/{last_seen_txid}`. + Get the first 25 confirmed transactions for an address. For pagination, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)* @@ -9226,7 +9232,7 @@ class BrkClient(BrkClientBase): def get_fullrbf_replacements(self) -> List[ReplacementNode]: """Recent full-RBF replacements. - Like `/api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). + Same response shape as `GET /api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). *[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)* @@ -9236,7 +9242,7 @@ class BrkClient(BrkClientBase): def get_block_template(self) -> BlockTemplate: """Projected next block template. - Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. + Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. Endpoint: `GET /api/v1/mempool/block-template`""" return self.get_json('/api/v1/mempool/block-template') @@ -9244,7 +9250,7 @@ class BrkClient(BrkClientBase): def get_block_template_diff(self, hash: NextBlockHash) -> BlockTemplateDiff: """Block template diff since hash. - Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`. + Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `GET /api/v1/mempool/block-template`. Endpoint: `GET /api/v1/mempool/block-template/diff/{hash}`""" return self.get_json(f'/api/v1/mempool/block-template/diff/{hash}') @@ -9260,7 +9266,7 @@ class BrkClient(BrkClientBase): def get_oracle_price(self) -> Dollars: """Live BTC/USD price. - Current BTC/USD price in dollars. Same value as `/api/mempool/price`. Confirmed per-height history is available at `/api/vecs/height-to-price`. + Current BTC/USD price in dollars. Same value as `GET /api/mempool/price`. Confirmed per-height history is available at `GET /api/series/price/height`. Endpoint: `GET /api/oracle/price`""" return self.get_json('/api/oracle/price') @@ -9440,7 +9446,7 @@ class BrkClient(BrkClientBase): def get_api(self) -> Any: """Compact OpenAPI specification. - Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`. + Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. The full specification is available at `GET /openapi.json`. Endpoint: `GET /api.json`""" return self.get_json('/api.json') diff --git a/packages/brk_client/uv.lock b/packages/brk_client/uv.lock index f444e844d..ed8781b11 100644 --- a/packages/brk_client/uv.lock +++ b/packages/brk_client/uv.lock @@ -50,7 +50,7 @@ wheels = [ [[package]] name = "brk-client" -version = "0.3.0" +version = "0.3.6" source = { editable = "." } [package.dev-dependencies] @@ -196,7 +196,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ @@ -217,7 +217,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ @@ -325,7 +325,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -744,11 +744,11 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.10.*'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -821,9 +821,9 @@ resolution-markers = [ "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } wheels = [ @@ -932,7 +932,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "polars-runtime-32", version = "1.36.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "polars-runtime-32", version = "1.36.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/dc/56f2a90c79a2cb13f9e956eab6385effe54216ae7a2068b3a6406bae4345/polars-1.36.1.tar.gz", hash = "sha256:12c7616a2305559144711ab73eaa18814f7aa898c522e7645014b68f1432d54c", size = 711993, upload-time = "2025-12-10T01:14:53.033Z" } wheels = [ @@ -953,7 +953,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "polars-runtime-32", version = "1.37.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "polars-runtime-32", version = "1.37.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/ae/dfebf31b9988c20998140b54d5b521f64ce08879f2c13d9b4d44d7c87e32/polars-1.37.1.tar.gz", hash = "sha256:0309e2a4633e712513401964b4d95452f124ceabf7aec6db50affb9ced4a274e", size = 715572, upload-time = "2026-01-12T23:27:03.267Z" } wheels = [ @@ -1045,13 +1045,13 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pluggy", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -1072,13 +1072,13 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ diff --git a/website/llms-full.txt b/website/llms-full.txt index b2fc194c6..0738147e3 100644 --- a/website/llms-full.txt +++ b/website/llms-full.txt @@ -17,11 +17,11 @@ For machine-readable tool construction, use [https://bitview.space/openapi.json] Address hash-prefix matches -Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`. +Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata with `GET /api/address/{address}`. Parameters: - `addr_type` (path, OutputType, required) -- `prefix` (path, string, required) +- `prefix` (path, string, required): First 1–16 hexadecimal nibbles of the RapidHash v3 hash over the raw address payload bytes. Returns: JSON `AddrHashPrefixMatches` @@ -48,7 +48,7 @@ curl -s "https://bitview.space/api/address/
" Address transactions -Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, use `/address/{address}/txs/chain/{last_seen_txid}`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* +Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* Parameters: - `address` (path, Addr, required) @@ -63,7 +63,7 @@ curl -s "https://bitview.space/api/address/
/txs" Address confirmed transactions -Get the first 25 confirmed transactions for an address. For pagination, use the path-style form `/txs/chain/{last_seen_txid}`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)* +Get the first 25 confirmed transactions for an address. For pagination, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)* Parameters: - `address` (path, Addr, required) @@ -126,7 +126,7 @@ curl -s "https://bitview.space/api/address/
/utxo" Compact OpenAPI specification -Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`. +Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. The full specification is available at `GET /openapi.json`. Returns: JSON `*` @@ -445,7 +445,7 @@ curl -s "https://bitview.space/api/v1/fees/recommended" Recent full-RBF replacements -Like `/api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). *[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)* +Same response shape as `GET /api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). *[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)* Returns: JSON `ReplacementNode[]` @@ -459,7 +459,7 @@ curl -s "https://bitview.space/api/v1/fullrbf/replacements" Health check -Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`. +Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, request `GET /api/server/sync`. Returns: JSON `Health` @@ -550,7 +550,7 @@ curl -s "https://bitview.space/api/mempool/txids" Projected next block template -Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. +Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. Returns: JSON `BlockTemplate` @@ -562,7 +562,7 @@ curl -s "https://bitview.space/api/v1/mempool/block-template" Block template diff since hash -Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`. +Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `GET /api/v1/mempool/block-template`. Parameters: - `hash` (path, NextBlockHash, required) @@ -869,7 +869,7 @@ Output value histogram at height or day Unfiltered output value histogram for a confirmed point. A block height (`840000`) gives every output in that block, coinbase included, binned by value on the oracle log scale; a calendar date (`YYYY-MM-DD`) sums every block that day. A flat array of log-scale bins. Parameters: -- `point` (path, string, required) +- `point` (path, string, required): Confirmed block height as decimal digits (`840000`) or calendar date in `YYYY-MM-DD` format. Returns: JSON `integer[]` @@ -896,7 +896,7 @@ Payment output histogram at height or day Smoothed histogram of oracle-eligible payment outputs for a confirmed point. A block height (`840000`) gives that block's oracle payment histogram; a calendar date (`YYYY-MM-DD`) gives the average of that day's per-block payment histograms. A flat array of log-scale bins. Parameters: -- `point` (path, string, required) +- `point` (path, string, required): Confirmed block height as decimal digits (`840000`) or calendar date in `YYYY-MM-DD` format. Returns: JSON `integer[]` @@ -908,7 +908,7 @@ curl -s "https://bitview.space/api/oracle/histogram/payments/" Live BTC/USD price -Current BTC/USD price in dollars. Same value as `/api/mempool/price`. Confirmed per-height history is available at `/api/vecs/height-to-price`. +Current BTC/USD price in dollars. Same value as `GET /api/mempool/price`. Confirmed per-height history is available at `GET /api/series/price/height`. Returns: JSON `Dollars` @@ -1073,7 +1073,7 @@ curl -s "https://bitview.space/api/series//?start=&end=/dates?weight=" URPD at date -URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. See the URPD tag description for unit conventions, `agg`, and `weight` options. +URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. `close`, `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and `supply` are BTC. `unrealized_pnl` can be negative. Parameters: - `cohort` (path, Cohort, required) -- `date` (path, string, required) +- `date` (path, string, required): Calendar date of the URPD snapshot in `YYYY-MM-DD` format. - `agg` (query, UrpdAggregation, optional): Aggregation strategy. Default: raw (no aggregation). Accepts `bucket` as alias. - `weight` (query, UrpdWeight, optional): Supply weighting. Default: raw (unweighted). @@ -1599,7 +1599,7 @@ curl -s "https://bitview.space/version" ### `BlockTemplate` -- `hash`: `NextBlockHash` (required) — Pass back as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas. +- `hash`: `NextBlockHash` (required) — Pass to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas. - `stats`: `MempoolBlock` (required) — Aggregate stats for this block (size, vsize, fee range, ...). - `transactions`: `Transaction[]` (required) — Full transaction bodies in `getblocktemplate` order. diff --git a/website/scripts/options/frameworks/coinflow.js b/website/scripts/options/frameworks/coinflow.js index 98c80bd8d..cd2984c39 100644 --- a/website/scripts/options/frameworks/coinflow.js +++ b/website/scripts/options/frameworks/coinflow.js @@ -3,7 +3,7 @@ import { colors } from "../../utils/colors.js"; import { Unit } from "../../utils/units.js"; import { ageRanges } from "../age-ranges.js"; import { line, price } from "../series.js"; -import { satsBtcUsd } from "../shared.js"; +import { satsBtcUsd, simplePriceRatioTree } from "../shared.js"; /** * @typedef {Object} CoinflowAgeRange @@ -76,6 +76,10 @@ export function createCoinflowSection() { { name: "All", color: colors.loss, tree: coinflow }, ...terms, ]; + const frameworkCohorts = [ + { name: "All", color: colors.coinflow, tree: coinflow }, + ...terms, + ]; const horizons = /** @type {const} */ ([ { key: "_8y", name: "8Y" }, @@ -93,11 +97,75 @@ export function createCoinflowSection() { return { name: "Coinflow", tree: [ + { + name: "Price", + tree: [ + { + name: "Compare", + title: "Coinflow Price by Holder Term", + top: frameworkCohorts.map(({ name, color, tree }) => + price({ + series: tree.price, + name, + color, + }), + ), + bottom: frameworkCohorts.map(({ name, color, tree }) => + line({ + series: tree.price.ratio, + name: `Spot / ${name}`, + color, + unit: Unit.ratio, + }), + ), + }, + ...frameworkCohorts.map(({ name, color, tree }) => { + const title = + name === "All" ? "Coinflow Price" : `${name} Coinflow Price`; + const [chart] = simplePriceRatioTree({ + pattern: tree.price, + title, + legend: name, + color, + }); + return { ...chart, name }; + }), + ], + }, + { + name: "Capitalization", + tree: [ + { + name: "Compare", + title: "Coinflow Cap by Holder Term", + bottom: frameworkCohorts.map(({ name, color, tree }) => + line({ + series: tree.cap.usd, + name, + color, + unit: Unit.usd, + }), + ), + }, + ...frameworkCohorts.map(({ name, color, tree }) => ({ + name, + title: name === "All" ? "Coinflow Cap" : `${name} Coinflow Cap`, + bottom: [ + line({ + series: tree.cap.usd, + name, + color, + unit: Unit.usd, + }), + ], + })), + ], + }, { name: "Supply", tree: [ { - name: "Breakdown", + name: "Overview", title: "Mobile vs Immobile Supply", bottom: [ ...satsBtcUsd({ @@ -113,130 +181,96 @@ export function createCoinflowSection() { ], }, { - name: "Mobile by Term", - title: "Mobile Supply by Holder Term", - bottom: terms.flatMap(({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.supply.mobile, - name, - color, - }), - ), + name: "By Holder Term", + tree: [ + { + name: "Mobile", + title: "Mobile Supply by Holder Term", + bottom: terms.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.supply.mobile, + name, + color, + }), + ), + }, + { + name: "Immobile", + title: "Immobile Supply by Holder Term", + bottom: terms.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.supply.immobile, + name, + color, + }), + ), + }, + ], }, { - name: "Immobile by Term", - title: "Immobile Supply by Holder Term", - bottom: terms.flatMap(({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.supply.immobile, - name, - color, - }), - ), - }, - { - name: "Mobile in Loss", - title: "Mobile Supply in Loss by Holder Term", - bottom: cohorts.map(({ name, color, tree }) => - line({ - series: tree.supply.mobile.inLoss.share, - name, - color, - unit: Unit.ratio, - }), - ), - }, - { - name: "In Loss by Horizon", - tree: cohorts.map(({ name, tree }) => ({ - name, - title: - name === "All" - ? "Mobile Supply in Loss by Horizon" - : `${name} Mobile Supply in Loss by Horizon`, - bottom: horizons.map((horizon) => - line({ - series: tree.horizon[horizon.key].supply.inLoss.share, - name: horizon.name, - color: horizon.color, - unit: Unit.ratio, - }), - ), - })), - }, - ], - }, - { - name: "Cap", - title: "Coinflow Cap", - bottom: [ - line({ - series: coinflow.cap.usd, - name: "All", - color: colors.coinflow, - unit: Unit.usd, - }), - ...terms.map(({ name, color, tree }) => - line({ - series: tree.cap.usd, - name, - color, - unit: Unit.usd, - }), - ), - ], - }, - { - name: "Price", - title: "Coinflow Price", - top: [ - price({ - series: coinflow.price, - name: "All", - color: colors.coinflow, - }), - ...terms.map(({ name, color, tree }) => - price({ - series: tree.price, - name, - color, - }), - ), - ], - bottom: [ - line({ - series: coinflow.price.ratio, - name: "Spot / All", - color: colors.coinflow, - unit: Unit.ratio, - }), - ...terms.map(({ name, color, tree }) => - line({ - series: tree.price.ratio, - name: `Spot / ${name}`, - color, - unit: Unit.ratio, - }), - ), - ], - }, - { - name: "Age Range", - tree: [ - ageRangeRatioChart(ranges, "mobility", "Mobility"), - ageRangeRatioChart(ranges, "spendingRate", "Spending Rate"), - ageRangeRatioChart( - ranges, - "spendingExposure", - "Spending Exposure", - ), - { - name: "Supply", + name: "By UTXO Age", tree: [ ageRangeSupplyChart(ranges, "mobile", "Mobile"), ageRangeSupplyChart(ranges, "immobile", "Immobile"), ], }, + { + name: "In Loss", + tree: [ + { + name: "By Holder Term", + title: "Mobile Supply in Loss by Holder Term", + bottom: cohorts.map(({ name, color, tree }) => + line({ + series: tree.supply.mobile.inLoss.share, + name, + color, + unit: Unit.ratio, + }), + ), + }, + { + name: "By Horizon", + tree: cohorts.map(({ name, tree }) => ({ + name, + title: + name === "All" + ? "Mobile Supply in Loss by Horizon" + : `${name} Mobile Supply in Loss by Horizon`, + bottom: horizons.map((horizon) => + line({ + series: + tree.horizon[horizon.key].supply.inLoss.share, + name: horizon.name, + color: horizon.color, + unit: Unit.ratio, + }), + ), + })), + }, + ], + }, + ], + }, + { + name: "Activity", + tree: [ + { + name: "By UTXO Age", + tree: [ + ageRangeRatioChart(ranges, "mobility", "Mobility"), + ageRangeRatioChart( + ranges, + "spendingRate", + "Spending Rate", + ), + ageRangeRatioChart( + ranges, + "spendingExposure", + "Spending Exposure", + ), + ], + }, ], }, ], diff --git a/website/scripts/options/frameworks/cointime/age-range.js b/website/scripts/options/frameworks/cointime/age-range.js index 6fed3e2f1..41bea6587 100644 --- a/website/scripts/options/frameworks/cointime/age-range.js +++ b/website/scripts/options/frameworks/cointime/age-range.js @@ -75,36 +75,26 @@ function coindaysTree(ranges, key, name) { return { name, tree: [ - { - name: "Average", - tree: ROLLING_WINDOWS.map((window) => ({ - name: window.name, - title: `${window.title} Average ${name} by UTXO Age`, - bottom: ranges.map((range) => - line({ - series: range.tree[key].average[window.key], - name: range.name, - color: range.color, - unit: Unit.coindays, - }), - ), - })), - }, - { - name: "Sum", - tree: ROLLING_WINDOWS.map((window) => ({ - name: window.name, - title: `${window.title} ${name} by UTXO Age`, - bottom: ranges.map((range) => - line({ - series: range.tree[key].sum[window.key], - name: range.name, - color: range.color, - unit: Unit.coindays, - }), - ), - })), - }, + ...ROLLING_WINDOWS.map((window) => ({ + name: window.name, + title: `${window.title} ${name} by UTXO Age`, + bottom: ranges.flatMap((range) => [ + line({ + series: range.tree[key].sum[window.key], + name: range.name, + color: range.color, + unit: Unit.coindays, + }), + line({ + series: range.tree[key].average[window.key], + name: `${range.name} Avg`, + color: range.color, + unit: Unit.coindays, + defaultActive: false, + style: 1, + }), + ]), + })), { name: "Cumulative", title: `Cumulative ${name} by UTXO Age`, @@ -125,35 +115,37 @@ function coindaysTree(ranges, key, name) { * @param {readonly CointimeAgeRange[]} ranges * @returns {PartialOptionsGroup} */ -export function createCointimeAgeRangeSection(ranges) { +export function createCointimeAgeRangeSupplySection(ranges) { return { - name: "Age Range", + name: "By UTXO Age", tree: [ - { - name: "Supply", - tree: [ - supplyChart(ranges, "awake", "Awake"), - supplyChart(ranges, "dormant", "Dormant"), - ], - }, - { - name: "Activity", - tree: [ - activityChart( - ranges, - "wakefulness", - "Wakefulness", - "Wakefulness", - ), - activityChart(ranges, "dormancy", "Dormancy", "Dormancy"), - activityChart( - ranges, - "wakefulnessToDormancy", - "Activity Ratio", - "Wakefulness / Dormancy", - ), - ], - }, + supplyChart(ranges, "awake", "Awake"), + supplyChart(ranges, "dormant", "Dormant"), + ], + }; +} + +/** + * @param {readonly CointimeAgeRange[]} ranges + * @returns {PartialOptionsGroup} + */ +export function createCointimeAgeRangeActivitySection(ranges) { + return { + name: "By UTXO Age", + tree: [ + activityChart( + ranges, + "wakefulness", + "Wakefulness", + "Wakefulness", + ), + activityChart(ranges, "dormancy", "Dormancy", "Dormancy"), + activityChart( + ranges, + "wakefulnessToDormancy", + "Activity Ratio", + "Wakefulness / Dormancy", + ), { name: "Coindays", tree: [ diff --git a/website/scripts/options/frameworks/cointime/index.js b/website/scripts/options/frameworks/cointime/index.js index 1a9cb01ba..e52ea23be 100644 --- a/website/scripts/options/frameworks/cointime/index.js +++ b/website/scripts/options/frameworks/cointime/index.js @@ -11,7 +11,10 @@ import { } from "../../series.js"; import { ageRanges } from "../../age-ranges.js"; import { satsBtcUsd, simplePriceRatioTree } from "../../shared.js"; -import { createCointimeAgeRangeSection } from "./age-range.js"; +import { + createCointimeAgeRangeActivitySection, + createCointimeAgeRangeSupplySection, +} from "./age-range.js"; /** * Create Cointime section @@ -42,7 +45,6 @@ export function createCointimeSection() { // Reference lines for cap comparisons const capReferenceLines = /** @type {const} */ ([ - { series: supply.marketCap.usd, name: "Market", color: colors.default }, { series: all.realized.cap.usd, name: "Realized", @@ -183,7 +185,7 @@ export function createCointimeSection() { name: "Cointime", tree: [ { - name: "Prices", + name: "Price", tree: [ { name: "Compare", @@ -202,27 +204,15 @@ export function createCointimeSection() { ...prices.map(({ pattern, name, color, defaultActive }) => price({ series: pattern, name, color, defaultActive }), ), + ...awakeCohorts.map(({ name, color, tree }) => + price({ + series: tree.awake.price, + name: name === "All" ? "Awake" : `${name} Awake`, + color, + }), + ), ], }, - { - name: "Awake", - title: "Awake Price by Holder Term", - top: awakeCohorts.map(({ name, color, tree }) => - price({ - series: tree.awake.price, - name, - color, - }), - ), - bottom: awakeCohorts.map(({ name, color, tree }) => - line({ - series: tree.awake.price.ratio, - name: `Spot / ${name}`, - color, - unit: Unit.ratio, - }), - ), - }, ...prices.map(({ pattern, name, title, color }) => { const [chart] = simplePriceRatioTree({ pattern, @@ -232,11 +222,21 @@ export function createCointimeSection() { }); return { ...chart, name }; }), + ...awakeCohorts.map(({ name, color, tree }) => { + const awakeName = name === "All" ? "Awake" : `${name} Awake`; + const [chart] = simplePriceRatioTree({ + pattern: tree.awake.price, + title: `${awakeName} Price`, + legend: awakeName, + color, + }); + return { ...chart, name: awakeName }; + }), ], }, { - name: "Caps", + name: "Capitalization", tree: [ { name: "Compare", @@ -248,20 +248,16 @@ export function createCointimeSection() { ...caps.map(({ series, name, color, defaultActive }) => line({ series, name, color, defaultActive, unit: Unit.usd }), ), + ...awakeCohorts.map(({ name, color, tree }) => + line({ + series: tree.awake.cap.usd, + name: name === "All" ? "Awake" : `${name} Awake`, + color, + unit: Unit.usd, + }), + ), ], }, - { - name: "Awake", - title: "Awake Cap by Holder Term", - bottom: awakeCohorts.map(({ name, color, tree }) => - line({ - series: tree.awake.cap.usd, - name, - color, - unit: Unit.usd, - }), - ), - }, ...caps.map(({ series, name, color }) => ({ name, title: `${name} Cap`, @@ -277,6 +273,29 @@ export function createCointimeSection() { ), ], })), + ...awakeCohorts.map(({ name, color, tree }) => { + const awakeName = name === "All" ? "Awake" : `${name} Awake`; + return { + name: awakeName, + title: `${awakeName} Cap`, + bottom: [ + line({ + series: tree.awake.cap.usd, + name: awakeName, + color, + unit: Unit.usd, + }), + ...capReferenceLines.map((ref) => + line({ + series: ref.series, + name: ref.name, + color: ref.color, + unit: Unit.usd, + }), + ), + ], + }; + }), ], }, @@ -284,264 +303,296 @@ export function createCointimeSection() { name: "Supply", tree: [ { - name: "Breakdown", - title: "Active vs Vaulted Supply", - bottom: supplyBreakdown.flatMap(({ pattern, name, color }) => - satsBtcUsd({ pattern, name, color }), - ), - }, - { - name: "Active in Loss", - title: "Active Supply in Loss", - bottom: [ - line({ - series: cointimeSupply.active.inLoss.share, - name: "Share", - color: colors.loss, - unit: Unit.ratio, - }), + name: "Overview", + tree: [ + { + name: "Active vs Vaulted", + title: "Active vs Vaulted Supply", + bottom: supplyBreakdown.flatMap(({ pattern, name, color }) => + satsBtcUsd({ pattern, name, color }), + ), + }, + { + name: "Awake vs Dormant", + title: "Awake vs Dormant Supply", + bottom: [ + ...satsBtcUsd({ + pattern: cointime.awake.supply, + name: "Awake", + color: colors.awake, + }), + ...satsBtcUsd({ + pattern: cointime.dormant.supply, + name: "Dormant", + color: colors.dormant, + }), + ], + }, ], }, { - name: "Awake vs Dormant", - title: "Awake vs Dormant Supply", - bottom: [ - ...satsBtcUsd({ - pattern: cointime.awake.supply, + name: "By Holder Term", + tree: [ + { name: "Awake", - color: colors.awake, - }), - ...satsBtcUsd({ - pattern: cointime.dormant.supply, + title: "Awake Supply by Holder Term", + bottom: awakeCohorts.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.awake.supply, + name, + color, + }), + ), + }, + { name: "Dormant", - color: colors.dormant, - }), + title: "Dormant Supply by Holder Term", + bottom: awakeCohorts.flatMap(({ name, color, tree }) => + satsBtcUsd({ + pattern: tree.dormant.supply, + name, + color, + }), + ), + }, ], }, + createCointimeAgeRangeSupplySection(cointimeAgeRanges), { - name: "Awake by Term", - title: "Awake Supply by Holder Term", - bottom: awakeCohorts.flatMap(({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.awake.supply, - name, - color, - }), - ), - }, - { - name: "Dormant by Term", - title: "Dormant Supply by Holder Term", - bottom: awakeCohorts.flatMap(({ name, color, tree }) => - satsBtcUsd({ - pattern: tree.dormant.supply, - name, - color, - }), - ), - }, - { - name: "Awake in Loss", - title: "Awake Supply in Loss by Holder Term", - bottom: awakeCohorts.map(({ name, color, tree }) => - line({ - series: tree.awake.supply.inLoss.share, - name, - color, - unit: Unit.ratio, - }), - ), + name: "In Loss", + tree: [ + { + name: "Active", + title: "Active Supply in Loss", + bottom: [ + line({ + series: cointimeSupply.active.inLoss.share, + name: "Share", + color: colors.loss, + unit: Unit.ratio, + }), + ], + }, + { + name: "Awake by Holder Term", + title: "Awake Supply in Loss by Holder Term", + bottom: awakeCohorts.map(({ name, color, tree }) => + line({ + series: tree.awake.supply.inLoss.share, + name, + color, + unit: Unit.ratio, + }), + ), + }, + ], }, ], }, - createCointimeAgeRangeSection(cointimeAgeRanges), - { name: "Activity", - title: "Liveliness & Vaultedness", - bottom: [ - line({ - series: activity.liveliness, - name: "Liveliness", - color: colors.liveliness, - unit: Unit.ratio, - }), - line({ - series: activity.vaultedness, - name: "Vaultedness", - color: colors.vaulted, - unit: Unit.ratio, - }), - line({ - series: activity.ratio, - name: "Liveliness / Vaultedness", - color: colors.activity, - unit: Unit.ratio, - defaultActive: false, - }), - ], - }, - - { - name: "Coinblocks", - tree: [ - ...multiSeriesTree({ - entries: coinblocks.map(({ pattern, name, color }) => ({ - name, - color, - average: pattern.average, - sum: pattern.sum, - cumulative: pattern.cumulative, - })), - metric: "Coinblocks", - unit: Unit.coinblocks, - }), - ...coinblocks.map(({ pattern, name, title: metric, color }) => ({ - name, - tree: sumsAndAveragesCumulative({ - sum: pattern.sum, - average: pattern.average, - cumulative: pattern.cumulative, - metric, - unit: Unit.coinblocks, - color, - }), - })), - ], - }, - - { - name: "Value", - tree: [ - ...multiSeriesTree({ - entries: [ - ...cointimeValues.map(({ pattern, name, color }) => ({ - name, - color, - average: pattern.average, - sum: pattern.sum, - cumulative: pattern.cumulative, - })), - { - name: vocdd.name, - color: vocdd.color, - average: vocdd.pattern.average, - sum: vocdd.pattern.sum, - cumulative: vocdd.pattern.cumulative, - }, - ], - metric: "Cointime Value", - unit: Unit.usd, - }), - ...cointimeValues.map(({ pattern, name, title: metric, color }) => ({ - name, - tree: sumsAndAveragesCumulative({ - sum: pattern.sum, - average: pattern.average, - cumulative: pattern.cumulative, - metric, - unit: Unit.usd, - color, - }), - })), - { - name: vocdd.name, - tree: sumsAndAveragesCumulative({ - sum: vocdd.pattern.sum, - average: vocdd.pattern.average, - cumulative: vocdd.pattern.cumulative, - metric: vocdd.title, - unit: Unit.usd, - color: vocdd.color, - }), - }, - ], - }, - - { - name: "Indicators", tree: [ { - name: "AVIV", - title: "AVIV Ratio", + name: "Overview", + title: "Liveliness & Vaultedness", bottom: [ line({ - series: cap.aviv.ratio, - name: "AVIV", - unit: Unit.ratio, - }), - ], - }, - { - name: "Reserve Risk", - title: "Reserve Risk", - bottom: [ - line({ - series: reserveRisk.value, - name: "Ratio", - color: colors.reserveRisk, - unit: Unit.ratio, - }), - ], - }, - ], - }, - - { - name: "Adjusted", - tree: [ - { - name: "Inflation", - title: "Cointime-Adjusted Inflation", - bottom: [ - dots({ - series: supply.inflationRate.percent, - name: "Base", - color: colors.base, - unit: Unit.percentage, - }), - ...percentRatioDots({ - pattern: adjusted.inflationRate, - name: "Cointime-Adjusted", - color: colors.adjusted, - }), - ], - }, - { - name: "BTC Velocity", - title: "Cointime-Adjusted BTC Velocity", - bottom: [ - line({ - series: supply.velocity.native, - name: "Base", - color: colors.base, + series: activity.liveliness, + name: "Liveliness", + color: colors.liveliness, unit: Unit.ratio, }), line({ - series: adjusted.txVelocityNative, - name: "Cointime-Adjusted", - color: colors.adjusted, - unit: Unit.ratio, - }), - ], - }, - { - name: "USD Velocity", - title: "Cointime-Adjusted USD Velocity", - bottom: [ - line({ - series: supply.velocity.fiat, - name: "Base", - color: colors.thermo, - unit: Unit.ratio, - }), - line({ - series: adjusted.txVelocityFiat, - name: "Cointime-Adjusted", + series: activity.vaultedness, + name: "Vaultedness", color: colors.vaulted, unit: Unit.ratio, }), + line({ + series: activity.ratio, + name: "Liveliness / Vaultedness", + color: colors.activity, + unit: Unit.ratio, + defaultActive: false, + }), + ], + }, + { + name: "Coinblocks", + tree: [ + { + name: "Compare", + tree: multiSeriesTree({ + entries: coinblocks.map(({ pattern, name, color }) => ({ + name, + color, + average: pattern.average, + sum: pattern.sum, + cumulative: pattern.cumulative, + })), + metric: "Coinblocks", + unit: Unit.coinblocks, + }), + }, + ...coinblocks.map( + ({ pattern, name, title: metric, color }) => ({ + name, + tree: sumsAndAveragesCumulative({ + sum: pattern.sum, + average: pattern.average, + cumulative: pattern.cumulative, + metric, + unit: Unit.coinblocks, + color, + }), + }), + ), + ], + }, + createCointimeAgeRangeActivitySection(cointimeAgeRanges), + ], + }, + + { + name: "Economics", + tree: [ + { + name: "Cointime Value", + tree: [ + { + name: "Compare", + tree: multiSeriesTree({ + entries: [ + ...cointimeValues.map(({ pattern, name, color }) => ({ + name, + color, + average: pattern.average, + sum: pattern.sum, + cumulative: pattern.cumulative, + })), + { + name: vocdd.name, + color: vocdd.color, + average: vocdd.pattern.average, + sum: vocdd.pattern.sum, + cumulative: vocdd.pattern.cumulative, + }, + ], + metric: "Cointime Value", + unit: Unit.usd, + }), + }, + ...cointimeValues.map( + ({ pattern, name, title: metric, color }) => ({ + name, + tree: sumsAndAveragesCumulative({ + sum: pattern.sum, + average: pattern.average, + cumulative: pattern.cumulative, + metric, + unit: Unit.usd, + color, + }), + }), + ), + { + name: vocdd.name, + tree: sumsAndAveragesCumulative({ + sum: vocdd.pattern.sum, + average: vocdd.pattern.average, + cumulative: vocdd.pattern.cumulative, + metric: vocdd.title, + unit: Unit.usd, + color: vocdd.color, + }), + }, + ], + }, + { + name: "Indicators", + tree: [ + { + name: "AVIV", + title: "AVIV Ratio", + bottom: [ + line({ + series: cap.aviv.ratio, + name: "AVIV", + unit: Unit.ratio, + }), + ], + }, + { + name: "Reserve Risk", + title: "Reserve Risk", + bottom: [ + line({ + series: reserveRisk.value, + name: "Ratio", + color: colors.reserveRisk, + unit: Unit.ratio, + }), + ], + }, + ], + }, + { + name: "Adjustments", + tree: [ + { + name: "Inflation", + title: "Cointime-Adjusted Inflation", + bottom: [ + dots({ + series: supply.inflationRate.percent, + name: "Base", + color: colors.base, + unit: Unit.percentage, + }), + ...percentRatioDots({ + pattern: adjusted.inflationRate, + name: "Cointime-Adjusted", + color: colors.adjusted, + }), + ], + }, + { + name: "BTC Velocity", + title: "Cointime-Adjusted BTC Velocity", + bottom: [ + line({ + series: supply.velocity.native, + name: "Base", + color: colors.base, + unit: Unit.ratio, + }), + line({ + series: adjusted.txVelocityNative, + name: "Cointime-Adjusted", + color: colors.adjusted, + unit: Unit.ratio, + }), + ], + }, + { + name: "USD Velocity", + title: "Cointime-Adjusted USD Velocity", + bottom: [ + line({ + series: supply.velocity.fiat, + name: "Base", + color: colors.thermo, + unit: Unit.ratio, + }), + line({ + series: adjusted.txVelocityFiat, + name: "Cointime-Adjusted", + color: colors.vaulted, + unit: Unit.ratio, + }), + ], + }, ], }, ], diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index be2965086..5b5a9a56f 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -939,6 +939,11 @@ export function createMarketSection() { name: "True Market Mean", color: colors.trueMarketMean, }), + price({ + series: cointime.awake.price, + name: "Awake Price", + color: colors.awake, + }), price({ series: coinflow.price, name: "Coinflow Price", diff --git a/website/scripts/options/partial.js b/website/scripts/options/partial.js index ea3c61a90..f661ada16 100644 --- a/website/scripts/options/partial.js +++ b/website/scripts/options/partial.js @@ -34,10 +34,9 @@ import { oraclePaymentsHeatmapOption, } from "../../src/heatmap/oracle.js"; import { - urpdAgeBandHeatmapFolders, - urpdAllHeatmapOptions, - urpdLthHeatmapOptions, - urpdSthHeatmapOptions, + coinflowWeightedUrpdHeatmapTree, + cointimeWeightedUrpdHeatmapTree, + rawUrpdHeatmapTree, } from "../../src/heatmap/urpd.js"; // Re-export types for external consumers @@ -343,10 +342,15 @@ export function createPartialOptions() { { name: "Price Distributions", tree: [ - ...urpdAllHeatmapOptions, - { name: "STH", tree: urpdSthHeatmapOptions }, - { name: "LTH", tree: urpdLthHeatmapOptions }, - { name: "Age Bands", tree: urpdAgeBandHeatmapFolders }, + ...rawUrpdHeatmapTree, + { + name: "Cointime Weighted", + tree: cointimeWeightedUrpdHeatmapTree, + }, + { + name: "Coinflow Weighted", + tree: coinflowWeightedUrpdHeatmapTree, + }, ], }, ], diff --git a/website/src/heatmap/urpd.js b/website/src/heatmap/urpd.js index 189ca1b26..2fba5e950 100644 --- a/website/src/heatmap/urpd.js +++ b/website/src/heatmap/urpd.js @@ -113,38 +113,89 @@ const AGE_BANDS = [ { name: "Over 15y", cohort: "utxos_over_15y_old" }, ]; -export const urpdAllHeatmapOptions = createCohortHeatmapOptions({ - cohort: "all", +export const rawUrpdHeatmapTree = createUrpdHeatmapTree(); +export const cointimeWeightedUrpdHeatmapTree = createUrpdHeatmapTree({ + weight: "cointime", + weightTitle: "Cointime-Weighted", }); -export const urpdSthHeatmapOptions = createCohortHeatmapOptions({ - cohort: "sth", - titlePrefix: "STH", +export const coinflowWeightedUrpdHeatmapTree = createUrpdHeatmapTree({ + weight: "coinflow", + weightTitle: "Coinflow-Weighted", }); -export const urpdLthHeatmapOptions = createCohortHeatmapOptions({ - cohort: "lth", - titlePrefix: "LTH", -}); -export const urpdAgeBandHeatmapFolders = AGE_BANDS.map(({ name, cohort }) => ({ - name, - tree: createCohortHeatmapOptions({ cohort, titlePrefix: name }), -})); + +/** + * @param {Object} args + * @param {UrpdWeight} [args.weight] + * @param {string} [args.weightTitle] + * @returns {PartialOptionsTree} + */ +function createUrpdHeatmapTree({ weight, weightTitle } = {}) { + return [ + ...createCohortHeatmapOptions({ cohort: "all", weight, weightTitle }), + { + name: "STH", + tree: createCohortHeatmapOptions({ + cohort: "sth", + titlePrefix: "STH", + weight, + weightTitle, + }), + }, + { + name: "LTH", + tree: createCohortHeatmapOptions({ + cohort: "lth", + titlePrefix: "LTH", + weight, + weightTitle, + }), + }, + { + name: "Age Bands", + tree: AGE_BANDS.map(({ name, cohort }) => ({ + name, + tree: createCohortHeatmapOptions({ + cohort, + titlePrefix: name, + weight, + weightTitle, + }), + })), + }, + ]; +} /** * @param {Object} args * @param {UrpdCohort} args.cohort * @param {string} [args.titlePrefix] + * @param {UrpdWeight} [args.weight] + * @param {string} [args.weightTitle] * @returns {PartialHeatmapOption[]} */ -function createCohortHeatmapOptions({ cohort, titlePrefix }) { +function createCohortHeatmapOptions({ + cohort, + titlePrefix, + weight, + weightTitle, +}) { return METRICS.map((metric) => { - const title = titlePrefix - ? `${titlePrefix} ${metric.title} Distribution` - : `${metric.title} Distribution`; + const title = [weightTitle, titlePrefix, metric.title, "Distribution"] + .filter(Boolean) + .join(" "); + const tooltip = weightTitle + ? { + ...metric.tooltip, + valueLabel: `${weightTitle} ${metric.tooltip.valueLabel.toLowerCase()}`, + } + : metric.tooltip; return createUrpdHeatmapOption({ ...metric, cohort, title, + tooltip, + weight, }); }); } @@ -154,6 +205,7 @@ function createCohortHeatmapOptions({ cohort, titlePrefix }) { * @param {UrpdCohort} args.cohort * @param {string} args.name * @param {string} args.title + * @param {UrpdWeight} [args.weight] * @param {(bucket: Urpd["buckets"][number]) => number} args.getValue * @param {HeatmapColorFn} args.color * @param {{ valueLabel?: string, formatValue?: (value: number) => string }} args.tooltip @@ -163,6 +215,7 @@ function createUrpdHeatmapOption({ cohort, name, title, + weight, getValue, color, tooltip, @@ -173,7 +226,7 @@ function createUrpdHeatmapOption({ title, points: { fetch: (date, signal, onPoints) => - fetchUrpdPoints(cohort, date, signal, getValue, onPoints), + fetchUrpdPoints(cohort, date, weight, signal, getValue, onPoints), }, grid: createAverageGrid({ yMin: MIN_LOG, @@ -201,19 +254,27 @@ function createUrpdHeatmapOption({ /** * @param {UrpdCohort} cohort * @param {string} date + * @param {UrpdWeight} [weight] * @param {AbortSignal} signal * @param {(bucket: Urpd["buckets"][number]) => number} getValue * @param {(points: HeatmapPoints) => void} [onPoints] * @returns {Promise} */ -async function fetchUrpdPoints(cohort, date, signal, getValue, onPoints) { +async function fetchUrpdPoints( + cohort, + date, + weight, + signal, + getValue, + onPoints, +) { /** @type {HeatmapPoints | undefined} */ let points; const urpd = await brk.getUrpdAt( cohort, date, AGGREGATION, - undefined, + weight, { signal, onValue: onPoints diff --git a/website_next/llms-full.txt b/website_next/llms-full.txt index b2fc194c6..0738147e3 100644 --- a/website_next/llms-full.txt +++ b/website_next/llms-full.txt @@ -17,11 +17,11 @@ For machine-readable tool construction, use [https://bitview.space/openapi.json] Address hash-prefix matches -Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`. +Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata with `GET /api/address/{address}`. Parameters: - `addr_type` (path, OutputType, required) -- `prefix` (path, string, required) +- `prefix` (path, string, required): First 1–16 hexadecimal nibbles of the RapidHash v3 hash over the raw address payload bytes. Returns: JSON `AddrHashPrefixMatches` @@ -48,7 +48,7 @@ curl -s "https://bitview.space/api/address/
" Address transactions -Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, use `/address/{address}/txs/chain/{last_seen_txid}`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* +Get transaction history for an address, newest first. Returns up to 50 mempool transactions plus a confirmed page sized to fill the response to 50 total (chain floor of 25, so 25-50 confirmed depending on mempool weight). To paginate further confirmed history, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)* Parameters: - `address` (path, Addr, required) @@ -63,7 +63,7 @@ curl -s "https://bitview.space/api/address/
/txs" Address confirmed transactions -Get the first 25 confirmed transactions for an address. For pagination, use the path-style form `/txs/chain/{last_seen_txid}`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)* +Get the first 25 confirmed transactions for an address. For pagination, request `GET /api/address/{address}/txs/chain/{after_txid}` with the last returned txid. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)* Parameters: - `address` (path, Addr, required) @@ -126,7 +126,7 @@ curl -s "https://bitview.space/api/address/
/utxo" Compact OpenAPI specification -Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`. +Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. The full specification is available at `GET /openapi.json`. Returns: JSON `*` @@ -445,7 +445,7 @@ curl -s "https://bitview.space/api/v1/fees/recommended" Recent full-RBF replacements -Like `/api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). *[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)* +Same response shape as `GET /api/v1/replacements`, but limited to trees where at least one predecessor was non-signaling (full-RBF). *[Mempool.space docs](https://mempool.space/docs/api/rest#get-fullrbf-replacements)* Returns: JSON `ReplacementNode[]` @@ -459,7 +459,7 @@ curl -s "https://bitview.space/api/v1/fullrbf/replacements" Health check -Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`. +Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, request `GET /api/server/sync`. Returns: JSON `Health` @@ -550,7 +550,7 @@ curl -s "https://bitview.space/api/mempool/txids" Projected next block template -Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. +Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template. Returns: JSON `BlockTemplate` @@ -562,7 +562,7 @@ curl -s "https://bitview.space/api/v1/mempool/block-template" Block template diff since hash -Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`. +Delta of the projected next block since ``. `order` is the full new template in order: each entry is either a number (index into the prior template the client cached at ``) or a transaction object (new body to insert at this position). Walk `order` once to rebuild; `removed` is a convenience list of txids that left so clients can evict cached bodies. After applying, use the response `hash` as `` on the next call to keep iterating. Returns `404` when `` has aged out of server history; clients should fall back to `GET /api/v1/mempool/block-template`. Parameters: - `hash` (path, NextBlockHash, required) @@ -869,7 +869,7 @@ Output value histogram at height or day Unfiltered output value histogram for a confirmed point. A block height (`840000`) gives every output in that block, coinbase included, binned by value on the oracle log scale; a calendar date (`YYYY-MM-DD`) sums every block that day. A flat array of log-scale bins. Parameters: -- `point` (path, string, required) +- `point` (path, string, required): Confirmed block height as decimal digits (`840000`) or calendar date in `YYYY-MM-DD` format. Returns: JSON `integer[]` @@ -896,7 +896,7 @@ Payment output histogram at height or day Smoothed histogram of oracle-eligible payment outputs for a confirmed point. A block height (`840000`) gives that block's oracle payment histogram; a calendar date (`YYYY-MM-DD`) gives the average of that day's per-block payment histograms. A flat array of log-scale bins. Parameters: -- `point` (path, string, required) +- `point` (path, string, required): Confirmed block height as decimal digits (`840000`) or calendar date in `YYYY-MM-DD` format. Returns: JSON `integer[]` @@ -908,7 +908,7 @@ curl -s "https://bitview.space/api/oracle/histogram/payments/" Live BTC/USD price -Current BTC/USD price in dollars. Same value as `/api/mempool/price`. Confirmed per-height history is available at `/api/vecs/height-to-price`. +Current BTC/USD price in dollars. Same value as `GET /api/mempool/price`. Confirmed per-height history is available at `GET /api/series/price/height`. Returns: JSON `Dollars` @@ -1073,7 +1073,7 @@ curl -s "https://bitview.space/api/series//?start=&end=/dates?weight=" URPD at date -URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. See the URPD tag description for unit conventions, `agg`, and `weight` options. +URPD for a (cohort, date) pair. Returns `{ cohort, date, weight, aggregation, close, total_supply, buckets }` where each bucket is `{ price_floor, supply, realized_cap, unrealized_pnl }`. `close`, `price_floor`, `realized_cap`, and `unrealized_pnl` are USD; `total_supply` and `supply` are BTC. `unrealized_pnl` can be negative. Parameters: - `cohort` (path, Cohort, required) -- `date` (path, string, required) +- `date` (path, string, required): Calendar date of the URPD snapshot in `YYYY-MM-DD` format. - `agg` (query, UrpdAggregation, optional): Aggregation strategy. Default: raw (no aggregation). Accepts `bucket` as alias. - `weight` (query, UrpdWeight, optional): Supply weighting. Default: raw (unweighted). @@ -1599,7 +1599,7 @@ curl -s "https://bitview.space/version" ### `BlockTemplate` -- `hash`: `NextBlockHash` (required) — Pass back as `` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas. +- `hash`: `NextBlockHash` (required) — Pass to `GET /api/v1/mempool/block-template/diff/{hash}` to fetch deltas. - `stats`: `MempoolBlock` (required) — Aggregate stats for this block (size, vsize, fee range, ...). - `transactions`: `Transaction[]` (required) — Full transaction bodies in `getblocktemplate` order.