Docker-first fork of Ashi Whirlpool Analysis, a Python-based tool for tracing Whirlpool CoinJoin transaction lineage on the Bitcoin blockchain.
Whirlpool.Observer runs isolated in Docker on a server. It tracks the 0.25 BTC and 0.025 BTC Whirlpool pools, stores scan state in a bind-mounted SQLite database, exposes a live pure-black web dashboard, and automatically refreshes CSV reports and PNG charts when the scanner catches up to the blockchain tip and after each configurable recheck interval.
This project is based on the original work by Ziya Sadr:
- Original repository: https://github.com/Ziya-Sadr/Ashi-Whirlpool-Analysis
- Blockchain Sync: Fetches Bitcoin transaction and raw block data from a mempool.space-compatible REST API.
- Configurable API Source: Uses the public
https://mempool.space/apiendpoint by default, but can be pointed at any self-hosted mempool.space API URL. - Local Database: Stores sync progress, Whirlpool transactions, TX0 metadata, tracked UTXOs, and web dashboard data in
./data/whirlpool.dbon the host. - Whirlpool Detection: Tracks valid 5-input, 5-output Whirlpool-style CoinJoin descendants that spend tracked anonymity-set UTXOs from a single pool.
- TX0 Detection: Inspects non-remix inputs to identify TX0 transactions, premix outputs, coordinator fee outputs, entered capacity, and fee-efficiency stats.
- Anonymity Set Tracking: Marks tracked UTXOs as spent and adds outputs from valid descendant mixes back into the tracked anonymity set.
- Automatic Reporting: Writes refreshed CSV reports to
./reportswhenever the scanner reaches the blockchain tip and after every recheck. - Automatic Charting: Writes refreshed PNG charts to
./reportsafter report generation. Chart failures are logged and do not stop scanning or CSV report generation. - Live Web UI: Serves Whirlpool.Observer on port
8080by default, with live charts, sync progress, pool stats, TX0/cycle stats, and Whirlpool transaction browsing. - Social/Favicon Assets: Serves favicon and social preview images from
./assets.
No API keys are required.
The tool uses a mempool.space-compatible REST API for blockchain data. By default it uses:
https://mempool.space/apiYou can point it at your own self-hosted mempool.space instance by setting MEMPOOL_API_URL to the full API base URL, including /api.
Examples:
MEMPOOL_API_URL=https://mymempool.example.com/api
MEMPOOL_API_URL=http://192.168.1.50:4080/apiThe scanner uses these mempool.space-compatible endpoints:
/blocks/tip/heightto get the current chain tip height./block-height/:heightto resolve a block height to a block hash./block/:hash/rawto download the complete raw binary block./tx/:txidto fetch known genesis transactions and TX0/parent transaction metadata.
Because full raw blocks are downloaded with /block/:hash/raw, the 25-transaction pagination limit on /block/:hash/txs is not used by this tool.
Install these on the host/server:
- Docker
- Docker Compose
Docker Compose bind mounts two host directories into the container:
./data->/data: stores the persistent SQLite database, includingwhirlpool.db../reports->/reports: stores generated CSV report and PNG chart files.
These directories are kept outside the container so scan progress, enriched TX0 metadata, dashboard data, and reports survive container rebuilds, restarts, and removals.
From the repository root:
docker compose buildSocial-card crawlers such as Twitter/X, Facebook, Signal, Telegram, and link preview checkers often require absolute URLs in Open Graph and Twitter card metadata. Whirlpool.Observer therefore bakes the public site URL into observer.html during the Docker image build.
If you are only running locally, the default is fine:
WHIRLPOOL_PUBLIC_URL=http://localhost:8080If you publicly expose Whirlpool.Observer, set the public URL before building:
WHIRLPOOL_PUBLIC_URL=https://observer.example.com docker compose build --no-cacheFor a persistent setting, add it to .env:
WHIRLPOOL_PUBLIC_URL=https://observer.example.comThen rebuild the image:
docker compose build --no-cacheThis value is build-time only. If you change it later, rebuild the image before restarting the container.
If you run Whirlpool.Observer behind a Tor hidden service, you can also bake in an Onion-Location header at image build time:
WHIRLPOOL_ONION_LOCATION=http://exampleexampleexampleexampleexampleexampleexampleexample.onion docker compose build --no-cacheOr add it to .env:
WHIRLPOOL_ONION_LOCATION=http://exampleexampleexampleexampleexampleexampleexampleexample.onionThen rebuild:
docker compose build --no-cacheWhen configured, every HTTP response includes:
Onion-Location: http://exampleexampleexampleexampleexampleexampleexampleexample.onionLeave WHIRLPOOL_ONION_LOCATION empty to disable this header. This is also build-time only; rebuild the image after changing it.
The default API URL is set in docker-compose.yml as:
MEMPOOL_API_URL: ${MEMPOOL_API_URL:-https://mempool.space/api}To use your own self-hosted mempool.space instance for one run, pass the variable before Docker Compose:
MEMPOOL_API_URL=https://mymempool.example.com/api docker compose up -dFor a persistent local setting, create a .env file beside docker-compose.yml:
MEMPOOL_API_URL=https://mymempool.example.com/apiThen start normally with docker compose up -d.
The scanner rechecks for new blocks every 12 hours by default after it reaches the blockchain tip.
You can change this with WHIRLPOOL_RESCAN_HOURS:
WHIRLPOOL_RESCAN_HOURS=1 docker compose up -dThe web UI is exposed on host port 8080 by default. Change it with:
WHIRLPOOL_WEB_PORT=8081 docker compose up -dThese values can also be placed in .env:
MEMPOOL_API_URL=https://mymempool.example.com/api
WHIRLPOOL_RESCAN_HOURS=1
WHIRLPOOL_WEB_PORT=8081Run the scanner and web UI in the background:
docker compose up -dOpen the dashboard in a browser:
http://localhost:8080If running on a server, replace localhost with the server IP or hostname.
The container starts the scanner automatically. It resumes from the last processed block stored in ./data/whirlpool.db.
The dashboard provides live, interactive visual analysis while the scanner is running:
- Pure-black dashboard theme.
- Favicon loaded from
assets/Ashigaru_Whirlpool_Logo_White.png. - Logo displayed next to the Whirlpool.Observer title.
- Open Graph and Twitter/X social preview cards using
assets/social.png. - Sync/scanning progress without needing to read logs.
- “Synced to blockheight” state and next update countdown after initial sync.
- Total unspent capacity.
- Total entered capacity detected from TX0 premix outputs.
- Per-pool cycle count, where a cycle means one tracked Whirlpool transaction.
- Per-pool TX0 count.
- Total entered capacity by pool graph.
- Live unspent capacity by pool graph.
- Total UTXOs entered graph.
- Total unspent UTXOs graph.
- Paginated Whirlpool transaction list with pool filtering.
- Clickable Whirlpool transaction IDs that open
http://am-i.exposed/#tx=<txid>. - Desktop-only TX0 input and fee-efficiency details.
- Mobile-optimized cycle cards without the extra TX0 detail column.
- Collapsible reference/FAQ section explaining key terms and calculations.
Existing partially-filled databases are supported. New schema tables are created without deleting existing scan progress, and missing input/TX0 metadata is backfilled from already-discovered Whirlpool transactions before normal scanning continues.
The embedded web server suppresses per-request HTTP access logs to prevent Docker logs from growing rapidly while the browser refreshes live API data.
Docker Compose also limits container log growth:
logging:
driver: "json-file"
options:
max-size: "64k"
max-file: "1"View logs with:
docker compose logs -f whirlpool-observerdocker compose stopThis stops the scanner and web UI but keeps the container, database, and reports.
The scanner handles SIGTERM / SIGINT gracefully: it stops between blocks, commits progress, and closes SQLite cleanly.
docker compose up -dThe scanner resumes from the last processed block in ./data/whirlpool.db.
docker compose downThis removes the container and default Docker network, but it does not delete ./data or ./reports.
Reports and static PNG charts are generated automatically by the running scanner.
When the scanner reaches the current blockchain tip, it will:
- Delete old generated CSV and PNG files from
./reports. - Generate a new simple CSV report.
- Generate a new detailed CSV report.
- Generate a new combined pool capacity PNG chart.
- Generate a new total unspent UTXO count PNG chart.
- Sleep for the configured recheck interval.
- Recheck the blockchain tip and repeat the report/chart refresh after it catches up again.
Generated files use these filename patterns:
whirlpool_simplereport_YYYYMMDD_HHMMSS.csvwhirlpool_report_YYYYMMDD_HHMMSS.csvwhirlpool_capacity_chart_YYYYMMDD_HHMMSS.pngwhirlpool_utxo_chart_YYYYMMDD_HHMMSS.png
The web UI renders interactive charts live from the database and does not depend on these static PNG files.
You can run a one-off stats command through Docker Compose:
docker compose run --rm whirlpool-observer statsDockerfile: builds the isolated runtime image, exposes the web UI port, and bakesWHIRLPOOL_PUBLIC_URL/WHIRLPOOL_ONION_LOCATIONinto the image at build time.docker-compose.yml: defines the service, bind mountsdataandreports, maps the web UI port, limits log file growth, and exposes configurable environment variables/build args.observer.html: serves the Whirlpool.Observer dashboard UI and includes favicon/social-card metadata.assets/Ashigaru_Whirlpool_Logo_White.png: favicon, Apple touch icon, and title logo.assets/social.png: Open Graph and Twitter/X social preview image.requirements.txt: lists runtime dependencies, includingflaskfor the web UI andmatplotlibfor PNG chart rendering..dockerignore: keeps local databases, reports, virtualenvs, and cache files out of the Docker build context.data/.gitkeep: keeps the persistent data directory in the repository.reports/.gitkeep: keeps the report output directory in the repository.
/assets/whirlpool.db contains a sample synced database upto Blockheight 948,651 which you may use for testing. It is reccomended that you scan the blockchain yourself however.
This project is licensed under the MIT License.
See the LICENSE file for more details.