A Bitcoin block clock for E-Ink displays.
Real-time network data, miner stats, and curated memes from the Einundzwanzig community in one framed screen.
What is this? • Technical Stuff • Gallery • Getting Started • Configuration • Maintenance • Architecture • Documentation • Support
mempaper is a Bitcoin block clock on E-Ink - but with style! Instead of boring blockchain monitoring, it shows the best Bitcoin memes from the Einundzwanzig community alongside real-time network data, miner stats, and wallet balances.
It connects to your local (or public) mempool instance to visualize the Bitcoin network status, mine data from your Bitaxe, and display Bitcoin memes on a beautiful e-Paper display mounted in a picture frame.
The memes on this display come from einundzwanzig-memes.space - a community project collecting the best Bitcoin memes from the German-speaking Bitcoin scene, and a big shoutout and thank you to them for building and maintaining this awesome project. The broader Einundzwanzig community is a major inspiration.
Disclaimer: This mempaper app is an independent project and has no connection or affiliation with the Einundzwanzig association.
|
DISPLAY 7.3" Waveshare E-Ink (7-color) 13.3" Waveshare E-Ink Spectra 6 |
RESOLUTION 800x480 px (7.3") 1600x1200 px (13.3") |
|
HARDWARE Raspberry Pi Zero 1 WH / 2 WH |
SOFTWARE Python backend (Flask + Jinja2, Flask-SocketIO, Gunicorn/gevent, Pillow/numpy) Web frontend (Vanilla JS + Socket.IO) |
|
POWER USAGE ~1 Watt |
DATA SOURCE mempool.space API — public or self-hosted mempool on your own node |
- Real-time Data - BTC price, halving countdown, network hashrate, difficulty, fees, and remaining supply via mempool.space or self-hosted mempool instance
- Hardware Support - Ready for Raspberry Pi (Zero/3/4/5) and Waveshare e-Paper displays (7.3" 7-color, 13.3" 6-color)
- Web Dashboard - Responsive interface for configuration, monitoring, and live block notifications
- Miner Integration - Monitor Bitaxe miner stats, aggregate hashrate, best difficulty, and found blocks
- Wallet Monitoring - Track on-chain balances for addresses, XPUBs, and ZPUBs with automatic address derivation
- Block Reward Monitoring - Track mining pool payouts and solo mining rewards for specific addresses
- Lightning Donations - Display incoming Lightning tips via LNbits webhook relay
- Meme Rotation - Curated Bitcoin memes from the Einundzwanzig community, with custom upload support
- OPSec Mode - One-click toggle to show a random cover image on the e-ink display instead of Bitcoin data
- Privacy Controls - Public mempool warnings, wallet cache wipe, User-Agent stripping
- Security - Argon2id password hashing, rate limiting, encrypted configuration, and basic auth for mempool
- Auto Updates - Scheduled software and system updates from the web UI
- Multi-language - English, German, Spanish, French, and Italian
- WiFi Onboarding - Hotspot-based setup flow for shipped devices, no SSH required
Waveshare 7.3" e-Paper (7-color)
7.3" E-Paper Display - Dark Mode |
7.3" E-Paper Display - Light Mode |
Waveshare 13.3" e-Paper (6-color)
13.3" E-Paper Display - Dark Mode |
13.3" E-Paper Display - Light Mode |
Secure login with Argon2id encryption |
Dashboard - Dark Mode |
Dashboard - Light Mode |
Configuration Settings |
Live system stats on every SSH login — block height, mempool status, memes count, service uptime, and display info.
Three ways in — pick the one that describes you:
| You are… | Go to |
|---|---|
| Setting up a mempaper someone gave you | First-Time Setup — power on, join the hotspot, connect your Wi-Fi. No SSH, no terminal. |
| Building one yourself from parts | Build Your Own — shopping list, one-line installer, display wiring |
| Running a mempaper already | Maintenance — updates, SSH access, admin accounts |
Handing a device to someone else? See Preparing a Device for Someone Else.
This is the flow for a device that arrives with no Wi-Fi configured — one prepared for shipping, or a freshly flashed card. It walks you through Wi-Fi and creating your admin account entirely from your phone; no SSH or technical knowledge is required.
If you just ran the installer yourself, you do not need this. Your Pi is already on Wi-Fi and running — go straight to Configuration.
The device ships with the delivery-state image on the e-ink display.
On first boot the device detects that no Wi-Fi is configured and starts a WPA2-protected setup hotspot. This takes between 90 seconds and 2 minutes 21 seconds — the Pi has to boot, initialise the Wi-Fi radio, and switch to AP mode. Once ready, the e-ink display refreshes and shows the network name, its password and a QR code.
- SSID:
setup-xxxxxxxxxx, random for each setup session. It carries no brand, so a network scan does not advertise what the device is. - Security: WPA2-PSK with a random 24-character password, generated per session and shown only on the panel. Everything the setup page then carries — your home Wi-Fi password, the admin account you create — is encrypted over the air.
- Scan the first QR code with your phone: it carries the network and its password, so the phone joins without anything being typed. The password is printed underneath for cameras that will not scan.
- There is no separate portal password. Being on the network is what grants access to the setup page.
Tip: Wait for the display to change from the delivery-state image to the hotspot screen before trying to connect. If nothing has changed after 2 minutes, the hotspot failed to start — power-cycle the device and try again.
Once connected to the hotspot, open http://10.42.0.1:5000/setup — the QR code on the right of the e-ink screen goes to the same address, and captive-portal detection usually opens it by itself. The page asks you to:
- Select a language (English, German, Spanish, French, Italian)
- Choose your home Wi-Fi from the scanned list, or enter a hidden SSID
- Enter the Wi-Fi password
- Create an admin account — username and password for the dashboard
Once the device joins your home Wi-Fi, the display shows a success screen telling you how to reach the dashboard from your network.
After 60 seconds the display switches to normal operation and renders its first dashboard image. From here, see Configuration.
Forgotten admin password, or you want to start fresh? There are three ways.
When you can still log in, this is the complete one: Settings > General > Advanced > Factory Reset, at the bottom. A confirmation lists exactly what goes, and the device then erases everything, returns the panel to the delivery screen, and powers itself off.
- All saved Wi-Fi networks
- All admin accounts and their passwords
- Wallet addresses, Bitaxe miners, donation history and every cache
- Every setting, back to its default -- theming, mempool host, schedules
- Network-bound encryption (Tang): the sealed data is removed before the encryption is turned off, so nothing sensitive is written back in the clear
- Your uploaded OPSec cover photos, and the meme tags you edited
- The SSH keys the device manages
The installed software stays, and so does the meme library -- it is shipped content rather than yours, and re-downloading a few thousand files over Tor is not something a reset should commit you to silently.
It powers off rather than rebooting, so the panel keeps the delivery image. Reconnect the power when you want it back: finding no Wi-Fi, it starts the setup hotspot and shows the network and password to join.
Allow around two to three minutes before pulling the plug -- the delivery image has to be rendered and written to the panel first, and interrupting an e-ink refresh is the one thing the display must not suffer. The dialog counts that time down and tells you when it is safe.
If the device is already in hotspot/setup mode (for example its stored Wi-Fi is unavailable), the setup page has a Reset Device button at the bottom. It clears:
- All admin accounts
- Wallet addresses and monitoring data
- Bitaxe miner configuration
- Donation history and webhook URLs
- Mempool authentication
The device stays in setup mode so you can reconfigure Wi-Fi and create a new admin account.
For a full reset including saved Wi-Fi profiles, power-cycle the device three times:
- Power on and wait for the e-ink display to refresh — on a Pi Zero this can take up to about 3 minutes 30 seconds. Only then power off.
- Repeat twice more. On the third boot the reset triggers automatically; nothing else to press.
The device recognises 3 boot timestamps inside a 15-minute window and then clears all user data, deletes every saved Wi-Fi profile, renders the delivery-state image, and restarts the setup hotspot.
Important: Wait for the e-ink refresh each time before cutting power. That refresh is the device's own confirmation that it finished booting, recorded the timestamp, and flushed writes to the SD card — pulling power earlier risks corrupting the filesystem. Three cycles of ~3:30 still fit comfortably inside the 15-minute window.
Everything needed to build a mempaper from scratch.
Note: Prices are approximate and vary by region and vendor. The Raspberry Pi Zero 2 W is recommended over the original Zero W for better performance.
Shared components (~63 EUR) — the same whichever display you choose:
| Component | Description | Price | Link |
|---|---|---|---|
| Raspberry Pi Zero 1 WH / 2 WH | Main controller (512MB RAM, WiFi/BT) | ~21 EUR | Zero 1 WH | Zero 2 WH |
| MicroSD Card | 32GB or larger, Class 10 recommended | ~13 EUR | SanDisk Extreme 64GB |
| USB-C Power Supply | 5V/2.5A minimum | ~10 EUR | |
| USB-C to Micro-USB Adapter | 2-pack adapter for power routing | ~4 EUR | Amazon |
| 90 deg USB-C Panel Mount Cable | Right-angle extension for clean cable routing | ~15 EUR | Amazon |
Cable Routing: The USB-C to Micro-USB adapter and 90 deg panel mount cable allow you to cleanly route power from the Raspberry Pi to the back of the picture frame for a professional finish.
Option A -- Waveshare 7.3" e-Paper (7-color) -- total ~215 EUR
| Component | Description | Price | Link |
|---|---|---|---|
| Waveshare 7.3" e-Paper (F) | 7-color e-ink display (800x480) | ~88 EUR | Waveshare | Amazon |
| Photo Frame | 18x24cm frame for display mounting | ~47 EUR | allesrahmen.de |
| Passepartout (Mat Board) | 180x240mm outer, 94x158mm opening | ~17 EUR | wandstyle.com |
Passepartout Dimensions: The 7.3" display has a visible area of 160x96mm. The passepartout opening is 158x94mm (2mm smaller on each side) to hold the display securely in place.
Option B -- Waveshare 13.3" e-Paper (6-color) -- total ~518 EUR
| Component | Description | Price | Link |
|---|---|---|---|
| Waveshare 13.3" e-Paper (E) | 6-color Spectra 6 e-ink display (1200x1600) | ~362 EUR | Waveshare | Amazon |
| Photo Frame | 28x35cm frame for display mounting | ~76 EUR | allesrahmen.de |
| Passepartout (Mat Board) | 280x350mm outer, 200x268mm opening | ~17 EUR | wandstyle.com |
Assembly Photos (Click to expand)
All components ready for assembly
Raspberry Pi Zero W connected to e-Paper display via SPI Control interface
Flash Raspberry Pi OS Lite 32-bit (Trixie / Debian 13), connect the Pi to your Wi-Fi, then paste this single command:
sudo apt install -y git \
&& git clone https://github.com/satcat21/btc-mempaper.git \
&& cd btc-mempaper && bash install.shRun it as your normal user (e.g. pi) — not as root; the script uses sudo where it needs to. It asks every configuration question upfront (display type, admin account, Tor, optional security features), then installs without further interruption.
Allow 10–20 minutes on a Pi Zero 1 WH. On ARMv6 the installer compiles gevent and Pillow from source, because no prebuilt wheels exist for that CPU.
When the installer finishes, the device goes straight into normal operation — it is already on your Wi-Fi, so there is no onboarding step. Open http://<pi-ip>:5000 and log in with the admin account you just created.
What the installer does (click to expand)
- Creates the
mempaperservice account - Installs all system and Python packages
- Creates a swap file where the device has none, so a source build cannot be killed for want of memory
- Rebuilds gevent and Pillow from source on ARMv6 (Pi Zero 1 WH) where the piwheels build is incompatible
- Copies the example config (skipped if
config/config.jsonalready exists) - Configures the e-ink display (interactive prompt)
- Generates and installs the
mempaper.servicesystemd unit - Sets up Wi-Fi hotspot permissions
- Disables UFW and
nftables— their default chains drop the DHCP broadcasts the setup hotspot depends on - Optionally configures fail2ban
- Starts the service
Supported OS: Raspberry Pi OS Lite 32-bit, Trixie (Debian 13).
Debian 12 (Bookworm) is not supported and not tested. It will still install — you just get less of a guarantee.
apt-requirements.txtpins exact package versions, so that a device installs the combination each release was tested against; several are the revisions carrying Debian's security fixes (…+deb13u1and similar). Those version strings exist only in the Trixie archive, and apt matches a pinned version exactly. So the file names the suite its pins belong to, and a device running anything else ignores them and installs the same packages unpinned — you get whatever versions that archive currently offers, floating, with nothing held. The installer says so when it happens.What you lose on Bookworm is the tested combination, not the ability to install. Nothing needs editing by hand. Two caveats worth knowing:
tools/python_versionexpects Python 3.13 on Trixie against Bookworm's 3.11. Bookworm's own entry (bookworm=11) is satisfied by what it ships, so no interpreter rebuild is triggered — but the project is exercised on 3.13.requirements.txtis not codename-bound; those pins come from PyPI and install the same on either. But one may require a newer Python than 3.11, and piwheels builds per Python minor — a package with a prebuilt armhf wheel for 3.13 may have none for 3.11 and will compile from source instead (slow on a Pi Zero, not fatal). A wheel can also be named for a platform it cannot run on: piwheels builds on ARMv8 hardware and publishes the result underlinux_armv6lnames.python tools/check_wheels.py --platform linux_armv6l --python cp313 --arm-level 6 --suggestreads what is inside each wheel and exits non-zero if anything would be built from source.To pin a combination for Bookworm yourself, run it, confirm it works, then promote what the device recorded — see docs/MAINTENANCE_GUIDE.md.
Verify it came up:
sudo systemctl status mempaper.service
sudo journalctl -u mempaper.service -fPC / Windows — development only, no e-ink display
git clone https://github.com/satcat21/btc-mempaper.git
cd btc-mempaper
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txtDisplay wiring and SPI are handled by the installer. It enables SPI, downloads the Waveshare driver for the model you picked, and disables Wi-Fi power saving on the Pi Zero W. Changing display later is not possible from the web UI — the Settings page shows the configured model read-only. Re-run
install.sh, or run the display tool directly:cd /home/mempaper/btc-mempaper sudo -u mempaper .venv/bin/python tools/configure_display.py
Prefer to do it by hand, or need to understand what the script touches? Manual Installation lists every command
install.shruns.
Only needed if you are handing a mempaper to somebody who should set it up themselves. This resets the device and leaves it in the delivery state:
cd /home/mempaper/btc-mempaper
sudo -u mempaper .venv/bin/python tools/delivery_state.pyWhat this does:
- renders a clean delivery image on e-ink
- leaves startup behavior in integrated mode (
mempaper.serviceonly) - clears setup-mode state so the next boot starts clean
What the recipient then experiences is the First-Time Setup flow above — worth reading once, since it is what you are handing over:
mempaper.servicestarts on boot and first attempts a normal Wi-Fi connection- Finding none after the startup grace period, it brings up the
setup-xxxxxxxxxxhotspot - The recipient connects, opens
http://10.42.0.1:5000, and enters their Wi-Fi credentials - On success the hotspot shuts down and normal operation resumes automatically
Navigate to Settings in the web interface at http://<pi-ip>:5000.
- Mempool Connection -- Default is
mempool.space. Change IP/Port to use a local node or self-hosted mempool instance. - Display -- Toggle "E-Ink Display Connected" to ON.
- Bitaxe -- Add miner IPs to monitor hashrate.
For advanced manual configuration, edit config/config.json.
Wallet addresses and xpubs live in a separate 600 file, not encrypted. Any key
the device could derive by itself would also be reconstructible by whoever holds the
device, so encrypting under one would imply a protection it could not deliver.
Physical access to the device means access to this data.
If a stolen device must not give up your addresses and balances, run a Tang server on your LAN — a node, a NAS, or a small Proxmox LXC. mempaper then seals its wallet data with a random 256-bit key held off the device, so carried off your network it cannot be decrypted at all. Tang needs very little: 3.7 MiB RAM idle. Its keypair is P-521 — roughly 256 bits of security, held on that host and never sent to the Pi.
If the Tang host is unreachable, mempaper still starts and runs — the wallet and donation blocks are disabled until it returns, then restore themselves automatically.
Enable it before entering your xpubs. Deleted data lingers in freed flash blocks, so sealing later cannot reach addresses already written to the card in clear text.
Setup, docker-compose.yml, LXC sizing and the limits of this approach:
Self-Hosting Guide → Tang.
The block height number is colored by the current fee. Colored against a fixed table, that stops telling you anything: through 2024–2025 fees sat between 0 and 4 sat/vB almost permanently, which is one shade of green from one end of the year to the other.
So the color is anchored to the median fee of the last 30 days instead. The question it answers is not "what is the fee" — the number below already says that — but "is this cheap for right now?", which is the one that decides whether you consolidate UTXOs today or wait.
Which fee gets measured is up to you: Fee Used for the Block Height
(fee_parameter), in the Theming section right above the color group, picks the
priority tier, and the same tier is both printed under the height and compared
against the median. Choose the one you actually transact at. Because the
baseline is what blocks really cost, a fast fee sits above it more often and
reads warm, while a minimum fee sits below it and reads cool — which is exactly
the answer each question deserves.
Both bars above are the same scale. Only the sat/vB underneath them changes, and that is the whole idea: 3 sat/vB is a bad deal when normal is 2, and a bargain when normal is 20. The same number, opposite advice.
| You see | It means |
|---|---|
| Blue | Far below normal — as cheap as this network gets. Move coins |
| Green | Comfortably below normal |
| Your base color | Within 5% of the median. Ordinary |
| Yellow → amber | Somewhat above normal |
| Orange | Expensive for the times |
| Red | Several times the going rate. Wait unless it is urgent |
The digits carry a vertical gradient, and both ends are fee readings: the previous block at the top, the current one at the bottom. So the number shows not just where fees are but which way they just moved.
| What you see | What just happened |
|---|---|
| Both ends cool | It was cheap and it still is |
| Both ends warm | It spiked and has stayed there |
| Cool over warm | Fees have just jumped |
| Warm over cool | Fees have just crashed |
| Two tones of your color | Both blocks were ordinary. Nothing to report |
Against a 30-day median of 20 sat/vB, on a dark theme:
| Move | Top (last block) | Bottom (this block) |
|---|---|---|
| 8 → 8 — still cheap | #0089E0 blue |
#72BEED light blue |
| 8 → 40 — spiked | #0089E0 blue |
#FAA376 light orange |
| 40 → 8 — crashed | #F75907 orange |
#72BEED light blue |
| 40 → 40 — still dear | #F75907 orange |
#FAA376 light orange |
| 20 → 20 — ordinary | #C8C8D2 your color |
#E0E0E6 lighter tone of it |
An end whose block sat within 5% of the median has nothing to say, so it carries
your color instead of a hue that would mean nothing. Two ordinary blocks in a
row therefore render the digits as two tones of your color — which is why the
default is a neutral grey (#3C3C46 on light, #C8C8D2 on dark) rather than
something that competes with the fee hues around it.
Pick it per theme under General → Advanced → Block Height Color & Scale. That panel previews all four cases above in both themes, and its fee colors are computed by the renderer itself, so the preview cannot drift from the panel.
Tone follows the theme, so the bottom — where the fee label sits — is always the readable end:
| Theme | Top (last block) | Bottom (this block) |
|---|---|---|
| Dark | full value | lightened |
| Light | lightened | deepened |
The ramp spans the digits themselves, cap top to baseline, so both ends are drawn at full strength rather than fading out into the descender space.
Three scales are available from the same panel — neutral-centred (default), a continuous blue-to-red rainbow, or the original fixed thresholds with your own editable limits. On e-ink the same colors are used, and the panel dithers them onto the six or seven inks it has — that blend is what gives the gradient its intermediate and lighter tones. Details and tuning: Config Reference → Block height color scale.
The dashboard image is composed of a meme and a set of optional info blocks displayed alongside it. Each block can be independently enabled or disabled in Settings. If more blocks are enabled than fit the available space, a random subset is shown each refresh.
| Block | Config key | What it shows |
|---|---|---|
| BTC Price | show_btc_price_block |
Current price in fiat and Moscow Time (sats/fiat) |
| Countdown | show_countdown_block |
Remaining BTC supply and percentage mined |
| Halving | show_halving_block |
Estimated next halving date and blocks remaining |
| Network | show_network_block |
Global hashrate and mining difficulty |
| Bitaxe | show_bitaxe_block |
Aggregate hashrate and found blocks or best difficulty |
| Wallet Balances | show_wallet_balances_block |
On-chain balances for addresses / XPUBs / ZPUBs |
| Lightning Donation | show_donation_block |
Latest Lightning donation via LNbits webhook |
All blocks are on by default except Bitaxe, Wallet Balances, and Donation, which require additional setup.
See Configuration Reference for every setting in detail.
Everything for running a mempaper day to day, whether you built it, were given it, or look after several.
Service control — over SSH:
sudo systemctl status mempaper.service # is it running?
sudo journalctl -u mempaper.service -f # live logs
sudo systemctl restart mempaper.service # restart after manual config editsFor OS-level upkeep — safe apt upgrades and Python version changes — see the Maintenance Guide.
Each admin generates an SSH key pair once on their own machine, then adds the public key through the web UI (Settings → General → Advanced → SSH Access). It gets installed for both the mempaper account (scoped sudo) and pi (full sudo).
Full procedure, including disabling password login: Security Guide → SSH keys and password login.
Multiple admin users are supported. Users are stored as Argon2id hashes in config/config.json under the admin_users key.
cd /home/mempaper/btc-mempaper
sudo -u mempaper .venv/bin/python tools/setup_user.py # create or update
sudo -u mempaper .venv/bin/python tools/setup_user.py --list # list users
sudo -u mempaper .venv/bin/python tools/setup_user.py --delete alice # delete a userUse the venv interpreter and the
mempaperuser, as above. A barepythonmisses the dependencies, and running aspiwrites a root-owned config the service cannot read.
The script refuses to delete the last remaining user to prevent lockout.
The script can be run while the service is running -- the application picks up the config change automatically. For password resets it is safer to stop the service first:
sudo systemctl stop mempaper.
Existing installations are migrated automatically on first startup: the single admin_username / admin_password_hash fields in the config are moved into the admin_users dict -- no manual action required.
mempaper can be updated directly from the web UI. Navigate to Settings > Updates to see the current version and available releases.
Software Update section in Settings
- Open the Software Updates section in Settings
- Select the desired release from the dropdown (latest is pre-selected and highlighted in orange)
- Click Update and confirm
- The app will fetch the release, install dependencies, and restart the service
- If the e-ink display is currently refreshing, the restart waits until the display is idle
- The page refreshes automatically once the service is back online
Enable scheduled updates to keep mempaper up to date without manual intervention:
| Setting | Config Key | Description |
|---|---|---|
| Automatic Updates | auto_update_enabled |
Enable/disable scheduled updates |
| Update Time | auto_update_time |
Time of day to check for updates (HH:MM, default: 03:00) |
| Update Days | auto_update_days |
Days of the week to run updates (default: Mon, Wed, Fri) |
When enabled, mempaper checks for new releases at the configured time and day, installs the update, and restarts the service automatically.
The app lives in the service account's home directory and every file is owned by
mempaper, so run the update steps as that user — otherwise pip writes
root-owned files into the virtualenv and the service fails to start afterwards.
ssh pi@<pi-ip>
cd /home/mempaper/btc-mempaper
sudo -u mempaper git fetch --tags
sudo -u mempaper git checkout <tag> # e.g. git checkout v2.1.0
sudo -u mempaper .venv/bin/pip install -r requirements.txt --quiet
# Re-minify only if you opted into minification at install time.
# This mirrors what the web updater does: it re-minifies when dist/ has content.
[ -n "$(ls -A static/js/dist 2>/dev/null)" ] \
&& sudo -u mempaper .venv/bin/python tools/minify.py
sudo systemctl restart mempaper.serviceSkipping the minify step on a device that does use minified assets leaves
static/js/dist/holding the previous release's JavaScript — the UI then runs stale code against a new backend.
The installer adds the pi user to the mempaper group and sets static/memes/ to group-writable (chmod 2775), so you can copy memes directly from another machine without switching users:
# Copy a local memes folder to the Pi
scp -r ~/memes/* pi@<pi-ip>:/home/mempaper/btc-mempaper/static/memes/Note: You must log out and back in (or run
newgrp mempaper) on the Pi after installation for the group membership to take effect for any already-running SSH session.
A meme is reachable by holiday and tag matching only when the image is in static/memes/ and a record for it exists in index.jsonl or _state_memes.jsonl carrying tags or descriptive text. A meme without one still appears in the ordinary rotation, but is never chosen for a holiday or a keyword — and nothing on the device says so. The audit reports the state of every file in the directory:
cd /home/mempaper/btc-mempaper
sudo -u mempaper .venv/bin/python tools/meme_index_audit.py
sudo -u mempaper .venv/bin/python tools/meme_index_audit.py --list # every filename, not a sample
sudo -u mempaper .venv/bin/python tools/meme_index_audit.py --csv # to pipe somewhereThree cases are counted apart, because each has a different answer:
| Reported as | What it means | What to do |
|---|---|---|
| from the API, no record | A uuid-named file whose metadata never got written | --repair fetches the records back from einundzwanzig-memes.space and appends them |
| added by hand, no record | Uploaded or copied in, so nothing exists upstream to fetch | Tag it under Meme Management, or leave it in the ordinary rotation |
| record, nothing to match on | Indexed, but carries neither tags nor text | Tag it under Meme Management |
It also finds one meme sitting in the directory as several files — the .webp the API served plus a .jpg or .png converted from it. The renderer draws whatever is in the directory, so a meme with two files comes up twice as often as every other one.
To find the untagged ones from the web UI instead, tick Only memes without tags above the gallery in Meme Management.
The audit is read-only unless --repair is given, which appends to index.jsonl and touches nothing else.
If your git remote points to a private repository (e.g. self-hosted GitLab), the updater falls back to local git tags — updates still work, but release notes won't be shown in the web UI.
To enable full release notes, create a .env file with an API token:
cp .env.example .env
nano .env# GitHub: Personal Access Token with "repo" scope
# GitLab: Personal Access Token with "read_api" scope
GIT_API_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxxThe web UI update requires a sudoers entry for passwordless service restart. install.sh sets this up for you; to install it manually:
echo "$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart mempaper.service" | sudo tee /etc/sudoers.d/mempaper-update
sudo chmod 0440 /etc/sudoers.d/mempaper-update- Architecture -- Diagrams: deployment topologies, what data leaves the device, codebase map, block-to-image data flow
- Configuration Reference -- Complete guide to all settings
- Manual Installation -- Every command
install.shruns, step by step - Security Guide -- Hardening guide: installation, SSH, firewalls, threat model, audit checklist
- Maintenance Guide -- Safe apt upgrades, Python version management
- Self-Hosting Guide -- Expose mempaper to the internet via Traefik, OIDC login, and TLS; run a Tang server so wallet data cannot be decrypted off your network
- Cache System Documentation -- Technical cache implementation details
The codebase is organised into four layers — adapters bring data in, one orchestrator schedules, one renderer produces both images, two sinks deliver them. See the component map and directory layout in the Architecture doc.
This project is 100% Open Source under GPL-3.0 license. Clone it, build your own mempaper, or contribute to the project!
If you find this project useful and want to support its development, you can send a Lightning tip:
Every sat helps keep the project maintained and adds new features!
mempaper · GPL-3.0 · satcat21 Built with love for the Bitcoin community















