Compare commits

..

No commits in common. "master" and "2025-taxes-recap" have entirely different histories.

51 changed files with 20 additions and 2865 deletions

14
.gitignore vendored
View file

@ -1,14 +0,0 @@
# Deployment configuration (contains sensitive server details)
deploy.config
# OS files
.DS_Store
Thumbs.db
# Editor files
.vscode/
.idea/
*.swp
*.swo
*~

View file

@ -8,6 +8,4 @@ The `index.html` is ready in the `public` folder.
## How to deploy ## How to deploy
1. Copy `deploy.config.example` to `deploy.config` Somehow get the `public` folder behind a webserver manually and sort out DNS.
2. Fill in your server details in `deploy.config` (host, user, remote path)
3. Run `./deploy.sh` to sync the `public` folder to your remote webserver

View file

@ -1,30 +0,0 @@
# Beware of those who don't speak plainly
When I was young, I would frequently not understand what someone was trying to explain to me. This happened frequently when listening to engineers, economists, journalits, business leaders, scientists, and other sorts of "experts". As a young and humble padawan, I often assumed I was dumb and whatever was being presented was just above my pay grade.
As life progressed, I kept on expanding my brain, which led me to become knowledgeable in areas I previously wasn't and were the explanations of "experts" were flying over my head. This lead me to observe that, often times, the blame was not on dumb young me when I didn't understand the allegedly experts. Instead, they were either explaining stupid gibberish that sounded great but was wrong, or they were lying, bullshiting or pretending.
On the other hand, sometimes I stumbled upon people who explained important, brilliant and relevant truths in extremely simple and understandable ways. Young dumb me used to not appreciate this people, because being young and dumb, I mistakenly assumed if something was simple and easy to understand, surely it wouldn't merit attention nor would it be relevant. School taught us that learning is hard and boring, so the harder and the more boring something is, the more we should focus on it. Because otherwise... why would through the ordeal of traditional education?
Now I'm old enough to appreciate the value of simplicity. It took many spins around the sun to come to this, but I'm grateful I eventually did it. I've learned that most things can be explained if done with care and time, and thus whenever I encounter someone explaining something convoluted, I immediately become skeptical and try to read the situation to understand on whose end is the fault sitting. I obviously still consider that the fault may be on my end, and I also think that many things to be known will forever be above my paygrade. But often times, the problem is on the other end.
Here's a few situations that showcase this in specific examples.
## Confusing business models
It shouldn't be hard to explain how a business makes money. Typically it's as simple as explaining the pains and needs of your customers and how you solve them. Sometimes it needs a long-ish explanation if the audience is unfamiliar with the context of your customers, but once a bit of context is provided, the value should be easy to explain.
My experience when someone has a hard time explaining how their business makes money is that one of these is likely happening:
- It is not making money but there is a need to pretend it does.
- It is making money in a dishonest way and they want to put make up on it.
-
## Economics
## Management gurus
## Academia
## Politicians

View file

@ -1,21 +0,0 @@
# Deployment Configuration
# Copy this file to deploy.config and fill in your server details
# deploy.config is gitignored to keep your credentials safe
# Remote server hostname or IP address
REMOTE_HOST="example.com"
# SSH username for the remote server
REMOTE_USER="username"
# Remote path where the website should be deployed
# This should be the directory served by your webserver (e.g., /var/www/html, /home/username/public_html)
REMOTE_PATH="/var/www/html"
# Optional: Path to SSH private key (if not using default ~/.ssh/id_rsa)
# Leave empty to use default SSH key
SSH_KEY=""
# Optional: SSH port (defaults to 22 if not specified)
# SSH_PORT="22"

View file

@ -1,34 +0,0 @@
#!/bin/bash
# Deployment script for pablohere website
# This script syncs the public folder to a remote webserver
set -e # Exit on error
# Load deployment configuration
if [ ! -f "deploy.config" ]; then
echo "Error: deploy.config file not found!"
echo "Please copy deploy.config.example to deploy.config and fill in your server details."
exit 1
fi
source deploy.config
# Validate required variables
if [ -z "$REMOTE_HOST" ] || [ -z "$REMOTE_USER" ] || [ -z "$REMOTE_PATH" ]; then
echo "Error: Required variables not set in deploy.config"
echo "Please ensure REMOTE_HOST, REMOTE_USER, and REMOTE_PATH are set."
exit 1
fi
# Use rsync to sync files
echo "Deploying public folder to $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH"
rsync -avz --delete \
--exclude='.git' \
--exclude='.DS_Store' \
$SSH_OPTS \
public/ \
$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH
echo "Deployment complete!"

View file

@ -1,325 +0,0 @@
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calculadora de compravenda de bitcoin</title>
<style>
:root {
font-family: system-ui, sans-serif;
color-scheme: light dark;
}
body {
max-width: 420px;
margin: 4rem auto;
padding: 0 1rem;
line-height: 1.5;
}
h1 {
text-align: center;
font-weight: 500;
font-size: 1.15rem;
margin-bottom: 0.25rem;
}
.sub {
text-align: center;
font-size: 0.85rem;
color: #888;
margin: 0 0 2rem;
}
.field {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
label {
flex: 0 0 5.5rem;
font-size: 0.9rem;
color: #888;
}
.input-wrap {
flex: 1;
position: relative;
display: flex;
align-items: center;
}
input {
flex: 1;
width: 100%;
box-sizing: border-box;
font-size: 1.25rem;
font-variant-numeric: tabular-nums;
text-align: right;
padding: 0.5rem 3rem 0.5rem 0.6rem;
border: 1px solid rgba(128, 128, 128, 0.4);
border-radius: 8px;
background: transparent;
color: inherit;
}
input:focus {
outline: none;
border-color: #f7931a;
}
.unit {
position: absolute;
right: 0.7rem;
font-size: 0.8rem;
color: #888;
pointer-events: none;
}
input.computed {
border-style: dashed;
color: #f7931a;
}
.rates {
display: grid;
grid-template-columns: auto auto;
align-items: baseline;
justify-content: center;
column-gap: 0.4rem;
row-gap: 0.15rem;
margin: -1rem 0 2rem;
font-size: 0.8rem;
color: #999;
cursor: default;
user-select: none;
}
.rates .lbl {
text-align: right;
}
.rates .value {
text-align: left;
font-variant-numeric: tabular-nums;
}
.rates .value.trade-val {
color: #f7931a;
}
.rates .cue {
grid-column: 1 / -1;
text-align: center;
font-size: 0.7rem;
font-style: italic;
color: #999;
opacity: 0;
transition: opacity 0.15s;
}
.rates.is-same .value.trade-val {
color: #999;
}
.rates.is-same .cue {
opacity: 1;
}
.tag {
flex: 0 0 2.5rem;
font-size: 0.7rem;
color: #f7931a;
text-transform: uppercase;
letter-spacing: 0.05em;
visibility: hidden;
}
.field.is-computed .tag {
visibility: visible;
}
</style>
</head>
<body>
<h1>Calculadora de compravenda de bitcoin</h1>
<p class="sub">EUR ⇄ BTC, amb el premium inclòs</p>
<div class="rates">
<span class="lbl" title="Preu de mercat (mediana) — s'usa pel càlcul, no és editable">mercat</span>
<span class="value" id="rate"></span>
<span class="cue" id="same">a preu de mercat</span>
<span class="lbl" title="Preu de mercat ajustat amb el teu premium — el preu real de la compravenda">operació</span>
<span class="value trade-val" id="trade"></span>
</div>
<div class="field" id="field-eur">
<label for="eur">EUR</label>
<span class="input-wrap">
<input id="eur" type="text" inputmode="decimal" autocomplete="off" placeholder="0.00" value="1000">
<span class="unit"></span>
</span>
<span class="tag">auto</span>
</div>
<div class="field" id="field-sats">
<label for="sats">Bitcoin</label>
<span class="input-wrap">
<input id="sats" type="text" inputmode="numeric" autocomplete="off" placeholder="0">
<span class="unit">sats</span>
</span>
<span class="tag">auto</span>
</div>
<div class="field" id="field-premium">
<label for="premium">Premium</label>
<span class="input-wrap">
<input id="premium" type="text" inputmode="decimal" autocomplete="off" placeholder="0" value="5">
<span class="unit">%</span>
</span>
<span class="tag">auto</span>
</div>
<script>
const SATS_PER_BTC = 100_000_000;
const REFRESH_MS = 5 * 60 * 1000;
// --- hidden price engine: median BTC/EUR across several exchanges ---
// The median holds as long as at least MIN_SOURCES quotes come back valid;
// any exchange that errors, times out, or returns junk is simply dropped.
const MIN_SOURCES = 3;
const SOURCES = [
{
url: "https://api.kraken.com/0/public/Ticker?pair=XBTEUR",
parse: (d) => parseFloat(d.result.XXBTZEUR.c[0]),
},
{
url: "https://api.coinbase.com/v2/prices/BTC-EUR/spot",
parse: (d) => parseFloat(d.data.amount),
},
{
url: "https://www.bitstamp.net/api/v2/ticker/btceur/",
parse: (d) => parseFloat(d.last),
},
{
url: "https://api-pub.bitfinex.com/v2/ticker/tBTCEUR",
parse: (d) => parseFloat(d[6]), // LAST_PRICE
},
{
url: "https://api.bitvavo.com/v2/ticker/price?market=BTC-EUR",
parse: (d) => parseFloat(d.price),
},
];
let price = null; // EUR per BTC, median
function median(values) {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
}
// fetch one exchange, resolving to a valid positive number or null
async function fetchQuote(source) {
try {
const data = await (await fetch(source.url)).json();
const value = source.parse(data);
return Number.isFinite(value) && value > 0 ? value : null;
} catch {
return null;
}
}
async function refreshPrice() {
const quotes = (await Promise.all(SOURCES.map(fetchQuote))).filter(
(v) => v !== null
);
if (quotes.length < MIN_SOURCES) return; // too few sources; keep last price
price = median(quotes);
document.getElementById("rate").textContent =
"€" + Math.round(price).toLocaleString("en-US") + " / BTC";
recompute(null); // keep the derived field in sync with the new price
}
// --- calculator ---
const els = {
eur: document.getElementById("eur"),
sats: document.getElementById("sats"),
premium: document.getElementById("premium"),
};
const fields = {
eur: document.getElementById("field-eur"),
sats: document.getElementById("field-sats"),
premium: document.getElementById("field-premium"),
};
// recency of user edits; last entry is the field we derive.
// defaults: EUR (1000) and premium (5%) given, so sats is derived.
let touchOrder = ["premium", "eur", "sats"];
const parse = (el) => {
const v = parseFloat(el.value.replace(/[^0-9.\-]/g, ""));
return isFinite(v) ? v : null;
};
const fmt = {
eur: (v) => v.toFixed(2),
sats: (v) => Math.round(v).toLocaleString("en-US"),
premium: (v) => parseFloat(v.toFixed(2)).toString(),
};
function recompute(edited) {
if (edited) {
touchOrder = [edited, ...touchOrder.filter((f) => f !== edited)];
}
const target = touchOrder[touchOrder.length - 1];
// highlight which field is currently derived
for (const f in fields) {
fields[f].classList.toggle("is-computed", f === target);
els[f].classList.toggle("computed", f === target);
}
if (price === null) return;
const eur = parse(els.eur);
const sats = parse(els.sats);
const premium = parse(els.premium);
let result = null;
if (target === "eur" && sats !== null && premium !== null) {
result = (sats / SATS_PER_BTC) * price * (1 + premium / 100);
} else if (target === "sats" && eur !== null && premium !== null) {
const effective = price * (1 + premium / 100);
if (effective > 0) result = (eur / effective) * SATS_PER_BTC;
} else if (target === "premium" && eur !== null && sats !== null) {
const base = (sats / SATS_PER_BTC) * price;
if (base > 0) result = (eur / base - 1) * 100;
}
els[target].value = result === null ? "" : fmt[target](result);
// effective trade price = market adjusted by the current premium
// (use the just-derived premium when premium is the computed field)
const effPremium = target === "premium" ? result : premium;
const effective = price * (1 + (effPremium ?? 0) / 100);
const tradeLine = document.querySelector(".rates");
document.getElementById("trade").textContent =
"€" + Math.round(effective).toLocaleString("en-US") + " / BTC";
tradeLine.classList.toggle(
"is-same",
Math.round(effective) === Math.round(price)
);
}
// reformat a field once the user is done entering it
const formatField = (f) => {
const v = parse(els[f]);
if (v !== null) els[f].value = fmt[f](v);
};
for (const f in els) {
els[f].addEventListener("input", () => {
// sats are whole numbers only — drop anything but digits as typed
if (f === "sats") els.sats.value = els.sats.value.replace(/[^0-9]/g, "");
recompute(f);
});
els[f].addEventListener("blur", () => formatField(f));
}
// normalize the pre-filled defaults (e.g. "1000" -> "1000.00")
formatField("eur");
formatField("premium");
recompute(null);
refreshPrice();
setInterval(refreshPrice, REFRESH_MS);
</script>
</body>
</html>

View file

@ -22,7 +22,6 @@
<li><a href="#contact-header">Contact</a></li> <li><a href="#contact-header">Contact</a></li>
<li><a href="#my-projects-header">My projects</a></li> <li><a href="#my-projects-header">My projects</a></li>
<li><a href="#writings-header">Writings</a></li> <li><a href="#writings-header">Writings</a></li>
<li><a href="#talks-header">Talks</a></li>
</ul> </ul>
<hr /> <hr />
<section> <section>
@ -70,16 +69,18 @@
<p>You can contact me on:</p> <p>You can contact me on:</p>
<ul> <ul>
<li> <li>
<p>
<a href="https://www.linkedin.com/in/pablomartincalvo/">On LinkedIn</a> <a href="https://www.linkedin.com/in/pablomartincalvo/">On LinkedIn</a>
for professional matters. for professional matters.
</li> </p>
<li>
On keybase: <a href="https://keybase.io/pablomartincalvo">https://keybase.io/pablomartincalvo</a>.
</li> </li>
<li> <li>
On Nostr. My npub is: On Nostr. My npub is:
npub1a29gdc6p7c05az2ka3qwwpl9kfcqmws3xlwmjefmtkulfhgd7u6shuqatg npub1a29gdc6p7c05az2ka3qwwpl9kfcqmws3xlwmjefmtkulfhgd7u6shuqatg
</li> </li>
<li>
<p>At this stage I'm not open to other contacts.</p>
</li>
</ul> </ul>
<p> <p>
If you are looking for my CV, no need to reach out, If you are looking for my CV, no need to reach out,
@ -111,24 +112,14 @@
<h2 id="my-projects-header">My projects</h2> <h2 id="my-projects-header">My projects</h2>
<p>Some of the projects I've shared publicly:</p> <p>Some of the projects I've shared publicly:</p>
<ul> <ul>
<li>
<a href="https://www.meetup.com/bitcoin-barcelona" target="_blank" rel="noopener noreferrer">Barcelona Bitcoin
Only, a local Bitcoin meetup and community I've
helped organize and run</a>
</li>
<li>
<a href="https://github.com/pmartincalvo/ntfy-emergency-app" target="_blank" rel="noopener noreferrer">A micro
webapp to let your loved ones grab your attention via ntfy</a>
</li>
<li> <li>
<a href="https://github.com/pmartincalvo/dni" target="_blank" rel="noopener noreferrer">My Python package to <a href="https://github.com/pmartincalvo/dni" target="_blank" rel="noopener noreferrer">My Python package to
handle Spanish DNIs better</a> handle Spanish DNIs better</a>
</li> </li>
<li> <li>
<a href="https://bitcoininfra.contrapeso.xyz" target="_blank" rel="noopener noreferrer">My open access Bitcoin infrastructure that you can use freely.</a> It includes access to the peer port of my Bitcoin node, an Electrum server and a mempool.space instance. <a href="https://www.meetup.com/bitcoin-barcelona" target="_blank" rel="noopener noreferrer">Barcelona Bitcoin
</li> Only, a local Bitcoin meetup and community I've
<li> helped organize and run</a>
<a href="https://github.com/counterweightoperator/hashbidder" target="_blank" rel="noopener noreferrer">hashbidder</a>, a CLI tool to automatically manage your bids on Braiins Hashpower to maintain a target hashrate at minimal cost.
</li> </li>
</ul> </ul>
<p> <p>
@ -151,54 +142,6 @@
<h2 id="writings-header">Writings</h2> <h2 id="writings-header">Writings</h2>
<p>Sometimes I like to jot down ideas and drop them here.</p> <p>Sometimes I like to jot down ideas and drop them here.</p>
<ul> <ul>
<li>
<a href="writings/thoughts-after-departing-galoy.html" target="_blank"
rel="noopener noreferrer">Thoughts after departing Galoy</a>
</li>
<li>
<a href="writings/one-property-where-gold-beats-bitcoin.html" target="_blank"
rel="noopener noreferrer">One property where gold beats Bitcoin</a>
</li>
<li>
<a href="writings/10-million-sats-into-mining.html" target="_blank"
rel="noopener noreferrer">10 million sats into mining</a>
</li>
<li>
<a href="writings/tech-fascination-as-a-function-of-your-age.html" target="_blank"
rel="noopener noreferrer">Tech fascination as a function of your age</a>
</li>
<li>
<a href="writings/my-first-petahash.html" target="_blank"
rel="noopener noreferrer">My first petahash</a>
</li>
<li>
<a href="writings/my-fitness-journey.html" target="_blank"
rel="noopener noreferrer">My fitness journey</a>
</li>
<li>
<a href="writings/how-i-write-some-articles-im-lazy-about.html" target="_blank"
rel="noopener noreferrer">How I write some articles I have a hard time getting started with</a>
</li>
<li>
<a href="writings/replacing-a-failed-disk-in-a-zfs-mirror.html" target="_blank"
rel="noopener noreferrer">Replacing a Failed Disk in a ZFS Mirror</a>
</li>
<li>
<a href="writings/busy-mans-guide-to-optimizing-dbt-models-performance.html" target="_blank"
rel="noopener noreferrer">Busy man's guide to optimizing dbt models performance</a>
</li>
<li>
<a href="writings/fixing-a-degraded-zfs-mirror.html" target="_blank"
rel="noopener noreferrer">Fixing a Degraded ZFS Mirror: Reseat, Resilver, and Scrub</a>
</li>
<li>
<a href="writings/a-degraded-pool-with-a-healthy-disk.html" target="_blank"
rel="noopener noreferrer">A degraded pool with a healthy disk</a>
</li>
<li>
<a href="writings/why-i-put-my-vms-on-a-zfs-mirror.html" target="_blank"
rel="noopener noreferrer">Why I Put My VMs on a ZFS Mirror</a>
</li>
<li> <li>
<a href="writings/a-note-for-the-future-the-tax-bleeding-in-2025.html" target="_blank" <a href="writings/a-note-for-the-future-the-tax-bleeding-in-2025.html" target="_blank"
rel="noopener noreferrer">A note for the future: the tax bleeding in 2025</a> rel="noopener noreferrer">A note for the future: the tax bleeding in 2025</a>
@ -261,33 +204,10 @@
</li> </li>
</ul> </ul>
</section> </section>
<hr />
<section>
<h2 id="talks-header">Talks</h2>
<p>Some talks I've given:</p>
<ul>
<li>
<a href="https://www.youtube.com/watch?v=kjJ2qto8cjg" target="_blank"
rel="noopener noreferrer">¿Sabes cómo se conecta tu wallet a la red?: Introducción a la infrastructura pública de Barcelona Bitcoin Only</a>
</li>
<li>
<a href="https://www.youtube.com/watch?v=-kQT2-C-Pgs" target="_blank"
rel="noopener noreferrer">¿Es posible la libertad sin desobediencia?</a>
</li>
<li>
<a href="https://www.youtube.com/watch?v=G9sk8fHrZ3Y&t=1360" target="_blank"
rel="noopener noreferrer">Lanzamiento BISQ 2.0</a>
</li>
<li>
<a href="https://youtu.be/3GfkptA8Dgc?t=1629" target="_blank"
rel="noopener noreferrer">Lo que mi nodo (Lightning) me enseñó</a>
</li>
</ul>
</section>
</main> </main>
<footer> <footer>
<p>Pablo Martín Calvo</p> <p>Pablo Martín Calvo</p>
</footer> </footer>
</body> </body>
</html> </html>

View file

@ -20,13 +20,13 @@ To do so, I am signing this object:
"username": "pablomartincalvo" "username": "pablomartincalvo"
}, },
"merkle_root": { "merkle_root": {
"ctime": 1755635067, "ctime": 1753193114,
"hash": "4f91af0b9c674e0f1d74a7cfad7abd15a7065cded92b96ac8a6abeb5c8553318599aa1bf7b065a3312e303506256b729b8b60b3a5dd06b68694423f4341a6a14", "hash": "30476b9dd587e65241454c447b71ef3f393f88b579350bd89bf5b9e443e6ba4d8ba99216710c622429c4fea48f95001effd4b4d8ec33decf6a7591c98f114460",
"hash_meta": "6472dbf2ed33341fb30b6a0c5c5c7fb39c219dd0ffd03c6e08b68c788e0de60a", "hash_meta": "cdcde70dfa48dad33277cf144e2a9dc0c55916d315685b913efc887b7c51247e",
"seqno": 27031070 "seqno": 26961345
}, },
"service": { "service": {
"entropy": "LEFJJ4FMmlJQWPPFEO4xHE5y", "entropy": "+5f0nk/+mBs8GItHh7xtyv3J",
"hostname": "pablohere.contrapeso.xyz", "hostname": "pablohere.contrapeso.xyz",
"protocol": "https:" "protocol": "https:"
}, },
@ -37,16 +37,16 @@ To do so, I am signing this object:
"name": "keybase.io go client", "name": "keybase.io go client",
"version": "6.5.1" "version": "6.5.1"
}, },
"ctime": 1755635082, "ctime": 1753193129,
"expire_in": 504576000, "expire_in": 504576000,
"prev": "37f12270050ab037897ccf6ef9451b1911cb505eca7c3842993b0b8925bc79b8", "prev": "17f8a85c13ee480621129b8e320fe83d45bfcfea7d5f956bf1adbf5def17d39b",
"seqno": 31, "seqno": 25,
"tag": "signature" "tag": "signature"
} }
which yields the signature: which yields the signature:
hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEg4B8c7Q5VP0eIY8W5Dttfq0uCL9YAAWtwT2/wH3+2jQIKp3BheWxvYWTESpcCH8QgN/EicAUKsDeJfM9u+UUbGRHLUF7KfDhCmTsLiSW8ebjEIAnIWTmufZ017e9WLdI1LhKBPaZ3HzmTrgyASDvY3PwoAgHCo3NpZ8RA9a3xgkSTU6Ht7M7DCsy4ClMmoWFtDEqzX9/dqskeoH2DrJUZYVymBQE1nyB0p1GuXiZA1cP5WY5SDURWZ5bBC6hzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEIEJZ4g4HC5qXcqbFf6sJ8XuZyMtoppazFqr1zPu0LH5co3RhZ80CAqd2ZXJzaW9uAQ== hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEg4B8c7Q5VP0eIY8W5Dttfq0uCL9YAAWtwT2/wH3+2jQIKp3BheWxvYWTESpcCGcQgF/ioXBPuSAYhEpuOMg/oPUW/z+p9X5Vr8a2/Xe8X05vEIDSxwrsQKywdTQC4/Z9ff1hsg9jf+HPjtzdFmUX6h/8oAgHCo3NpZ8RAwcxkmlMFZ2cJ4T638SWLTUnlWhDrJgDn18SM+CvvYOo60INaUdD/ou5jX62LIN7OOqQfdw5MoYEQTgMt5zGJD6hzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEIKg7r57boivDmPxyU3ai8XN+JeIf6Ct2Kz+LjJt7VPvVo3RhZ80CAqd2ZXJzaW9uAQ==
And finally, I am proving ownership of this host by posting or And finally, I am proving ownership of this host by posting or
appending to this document. appending to this document.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

View file

@ -7,8 +7,7 @@ body {
h1, h1,
h2, h2,
h3, h3 {
h4 {
text-align: center; text-align: center;
} }
@ -26,12 +25,4 @@ figcaption {
font-style: italic; font-style: italic;
font-size: small; font-size: small;
text-align: center; text-align: center;
}
blockquote {
border-left: 3px solid #888;
margin-left: 0;
padding-left: 1em;
font-style: italic;
color: #555;
} }

View file

@ -1,295 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
<style>
table.ledger {
border-collapse: collapse;
width: fit-content;
min-width: 60%;
margin: 16px auto;
font-family: "Times New Roman", Times, serif;
font-size: 1.05em;
}
table.ledger th,
table.ledger td {
padding: 4px 16px;
border-bottom: 1px solid #ddd;
}
table.ledger .title {
text-align: center;
font-weight: bold;
font-size: 1.15em;
}
table.ledger .subtitle {
text-align: center;
font-style: italic;
font-weight: bold;
border-bottom: 2px solid #000;
padding-bottom: 4px;
}
table.ledger .amount {
text-align: right;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
table.ledger .section th {
text-align: left;
font-weight: bold;
padding-top: 12px;
}
table.ledger .item td:first-child {
padding-left: 30px;
font-weight: normal;
}
table.ledger .subtotal td {
font-weight: normal;
}
table.ledger .subtotal .amount {
border-top: 1px solid #000;
}
table.ledger .total th {
text-align: left;
font-weight: bold;
border-bottom: none;
}
table.ledger .total .amount {
text-align: right;
border-top: 1px solid #000;
border-bottom: 3px double #000;
}
</style>
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<hr>
<section>
<h2>10 million sats into mining</h2>
<p><em>Published: 2026-05-25</em></p>
<p>In my previous article <a href="my-first-petahash.html">My first petahash</a> I discussed my beginnings
with mining via rented hashrate from
<a href="https://hashpower.braiins.com">Braiins' Hashpower market</a> plus my own
<a href="https://github.com/OCEAN-xyz/datum_gateway">DATUM</a> gateway pointing to
<a href="https://ocean.xyz/">OCEAN</a>, with
<a href="https://github.com/counterweightoperator/hashbidder">hashbidder</a> to automate my bidding in
the hashmarket. I also mentioned that my starting plan was to put a total of 10 million sats into this
experiment then checkpoint how things were going. Well, the 10 million sats have gone through Hashpower
(and <em>partially</em> come back to me), so I'll deliver the checkpoint as I promised myself.</p>
<p>Before going into it, I want to share my rationale for writing and sharing this. Most stuff I fiddle and
tinker with, I don't write about publicly. But this experiment is different. Although simply setting this
up and mining is plenty of fun and pretty much justifies doing it by its entertaining nature, the reality
is that my interest in controlling hashrate goes beyond sweet weekend hacking fun. Truth is that the
<a href="https://bip110.org/">BIP110 softfork</a> is on the table, and its success will depend on people
adopting it and hashrate being deployed to support it. My node has been running with the BIP110 patch for
quite a bit, but I didn't control more than 1TH/s until I started this experiment.</p>
<p>I think just learning how to set up this system and pointing a few petahashes to support the softfork is
enough of a contribution for my paygrade. It's been long since I thought I should save the world.
Nowadays I'm happy doing what a pleb must do, which is tending to his garden and doing his humble
part.</p>
<figure style="width: 75%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/peaceful-life-meme.jpeg"
alt="It's a peaceful life meme">
</figure>
<p>Nevertheless: I wrote my previous article because I found there seems to be quite a bit of interest among
the plebs around renting hashrate, and so far I had not found much written about it beyond shit slinging
between knotzis and coretards. I'm writing this second part for the same reason, plus another specific
gap I've noticed. I see many plebs ask questions about how much money will you spend doing this, or what
amount of hashrate can you secure if you are willing to dedicate X sats per month to mining. And the
answers that come back are typically long rants on the different convoluted factors that influence the
answer. Not that those answers are wrong, for it is really a convoluted topic and it's hard to talk
specifics a priori. Hence why I want to write this article where I provide specific numbers on my
experience: so that it serves others as a very relatable and simple to understand report on how much
someone actually spent mining in a certain way. Hopefully it makes for a better data point to those who
find themselves on the fences of trying out.</p>
<p>The ultimate goal of it all is to spread knowledge, in the hopes people will be enlightened and motivated
by this and will take part instead of letting Foundry mine everything. My part will end here, and then
it's your turn to act.</p>
<p>I hope you find it interesting and useful. And I hope that, if you support BIP110, you'll roll your
sleeves and replicate this, adding the hashrate you can afford to the cause.</p>
<p>Now let's jump into the mud.</p>
<h3>Quick review of the system + how it performed</h3>
<p>I'm running Knots with the BIP110 patch, DATUM and hashbidder in a server I physically host myself. All
three services are hosted in a single VM. The DATUM endpoint is publicly reachable via a VPS I rent
(with sats), which then gets redirected to the DATUM process running in my server.</p>
<p>The uptime of this has been perfect. Any bit of downtime has been related to blips in my residential
networking or host downtime due to maintenance that is not related to the mining bit. None of the
Bitcoin/mining services have had any operational hiccup (hashbidder did stop working here and there, but
that was simply because I was always using the bleeding edge version and some bugs made it to
production). Resource consumption for this stack is a non-issue: stable and predictable, so the VM this
has been running on has had 99.86% uptime during the last couple of months.</p>
<p>I encountered a few small issues with Hashpower, but they were all trivial and didn't affect my
experience. The service has had a few outages of the API and web, yet as far as I can tell the actual
delivery of hashrate has been working constantly. If that failed at any time, it was brief enough to get
camouflaged as your usual hash delivery spikeyness.</p>
<p>I did face some issues with <a href="https://hashpower.braiins.com/api/">Hashpower's API docs</a> being
incorrect: some endpoints behaved differently than what they advertised in the documentation. I got in
touch with their support in mid April and they told me they would address the issue. I checked while
writing the article and the issue is still unsolved.</p>
<p>Regarding rejected shares, I think the setup has worked out great. I just checked, and the datum dashboard
indicates I'm sitting at 0.06% (0.0006). I wouldn't be able to judge the quality of this rate myself, but
others more experienced in mining have assured me it's great, so I'll parrot back that it's great.</p>
<p>When it comes to bidding, I can assure you it has been quite optimal. I've taken whatever prices existed in Hashpower and never said "no, I won't mine now because XYZ price is too high". But I did a pretty decent job at staying as close as possible to the cheapest price being served. It is common to see bids that are 10%/20%/50% above market price in Hashpower. In my case, I'm pretty confident I must have stayed constantly between 0-5% of the cheapest bid. I can't show you the data to back it up, and won't go into the details, but hashbidder just does a good job of doing that. You can try for yourself and verify.</p>
<h3>Facts, figures, performance</h3>
<p>Okay, now let's show some serious data. Let's begin with how much I mined:</p>
<figure style="width: 97.5%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/chart1_accumulated_hashrate.png"
alt="Accumulated hashes computed">
<figcaption>Total accumulated hashes computed over the experiment</figcaption>
</figure>
<figure style="width: 97.5%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/chart2_daily_hashrate.png"
alt="Daily average hashrate against the 5 PH/s target">
<figcaption>Daily average hashrate against the 5 PH/s target</figcaption>
</figure>
<p>After some initial fiddling, I decided I would settle for a goal of mining at ~5PH/s. I estimated this
would allow me to spend my 10 million sats over 6-8 weeks. I wanted to mine at least that long to have a
decent statistical chance to getting close to non-extreme luck streaks with OCEAN (either good or
bad).</p>
<p>You can see in the chart the first days were more unstable due to this. It also took me a bit of time to
get hashbidder properly implemented and operational, hence why I didn't really start tracking the target
hashrate great until early May.</p>
<p>Is this enough to amount to anything significant? Well, if I had solo mined at this pace, there is less
than 2.5% probability that I would have found a block in less than a year. There is also around a 2.5%
probability that if I had mined with the same weight in the network all the way to 2140, I would still
have not found a block. Thank God for pooled mining and for OCEAN to make it fair, simple and
transparent.</p>
<p>How much did I pay for this? Well, the actual exact amount of money I've spent is not exactly 10,000,000
sats, but rather 9,929,972 sats. The difference is mixed between mining fees I spent sending my money to
Braiins and some leftover sats I have still sitting on Hashpower because they're not enough to place an
order.</p>
<figure style="width: 97.5%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/chart3_cumulative_spend.png"
alt="Cumulative sats spent on rented hashrate">
<figcaption>Cumulative sats spent on rented hashrate</figcaption>
</figure>
<p>This is a chart showing how my expense accumulated slowly by the day. Unfortunately, it only shows some
days because the Braiins API doesn't give you back the full history, hence why you see that interpolated
bit at the start. Pretty boring. Stable hashrate, stable expense.</p>
<p>Although it's not a perfectly straight line! Prices do change quite a bit overtime in hashpower. The chart
of average sats per PH/s/d shows how the average price fluctuates over different days:</p>
<figure style="width: 97.5%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/chart4_price_per_ph.png"
alt="Average price per PH/s per day">
<figcaption>Average price per PH/s per day</figcaption>
</figure>
<p>As you can see, prices change significantly measured in sats. Differences of +-20% in a few days do
happen.</p>
<p>On the income side: this chart shows the earned rewards day by day (not to be confused with payouts).</p>
<figure style="width: 97.5%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/chart5_daily_rewards.png"
alt="Daily mining rewards earned">
<figcaption>Daily mining rewards earned, with the cumulative average</figcaption>
</figure>
<p>Extremely volatile, as you would expect due to the nature of
<a href="https://ocean.xyz/docs/tides">TIDES</a>, the protocol that OCEAN follows. Having said that, the
volatility of this also relates to the size of OCEAN: as they grow their share of the total difficulty,
they hit blocks most frequently and wild spikes of luck become less frequent.</p>
<p>This is the chart that should make it clear that mining with OCEAN nowadays for a brief window of time is
russian roulette. There are entire days where NOTHING is paid. If you have any hopes of having a stable
experience, plan to mine for months at least. The cumulative average line in the chart shows you how, as
you stay longer and longer, you start stabilizing on the average.</p>
<p>Finally, this chart sums up the financial performance, showing expense and reward day by day along with
the cumulative net loss:</p>
<figure style="width: 97.5%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/chart6_daily_pnl.png"
alt="Estimated daily P&L: expense vs rewards">
<figcaption>Estimated daily P&amp;L: expense vs rewards, with accumulated net profit</figcaption>
</figure>
<p>So what does the final P&amp;L look like then?</p>
<table class="ledger">
<thead>
<tr>
<th class="title" colspan="2">Mining experiment P&amp;L</th>
</tr>
<tr>
<th class="subtitle" colspan="2">(in sats)</th>
</tr>
</thead>
<tbody>
<tr class="section">
<th colspan="2">Expenses:</th>
</tr>
<tr class="item">
<td>Transaction fees</td>
<td class="amount">817</td>
</tr>
<tr class="item">
<td>OCEAN fees</td>
<td class="amount">93,218</td>
</tr>
<tr class="item">
<td>Rented hashrate</td>
<td class="amount">9,929,972</td>
</tr>
<tr class="subtotal">
<td>Total expense</td>
<td class="amount">10,024,007</td>
</tr>
<tr class="section">
<th colspan="2">Income:</th>
</tr>
<tr class="item">
<td>Mining rewards</td>
<td class="amount">9,321,406</td>
</tr>
<tr class="total">
<th>Net</th>
<th class="amount">702,601</th>
</tr>
</tbody>
</table>
<h3>What comes next</h3>
<p>So, I poured 10 million sats into this and got roughly 9.3 million sats out, thus spending 700K sats.</p>
<p>Surely it's time to stop, right?</p>
<figure style="width: 75%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/padme-meme.jpg" alt="And then you will stop, right? meme">
</figure>
<p>Unhosted Marcellus made
<a href="https://xcancel.com/oomahq/status/2023476291136037182#m">this reasoning I find great</a> that if
your bags are in Bitcoin, it is in your best interest to keep hash raining on the blockchain tip. He
changes the way of looking at mining as a "business" for miners, and rather as a maintenance expense for
users (and fees and miners are just a way to organize so that each user doesn't need to run an S21 in
their kitchen and risk divorce).</p>
<p>I think it's a good POV. And I'm going to stick to it. I think putting 0.1% of my network, on a yearly
basis, towards adding hash is valuable for me. Now that I've run this couple months experiment, I have a
good feel for what is the "loss" factor of cycling sats through hashpower, so I can crunch the numbers
and find out what is the hashrate I should aim for to spend 0.1% of my bags on a yearly basis.</p>
<p>Some fellow Bitcoiners are aware of my plan and have pointed out to me that my actions are pointless. That
I'm too small to make a difference in the mining scene. It must suck to have such a terrible low-agency
mentality. Pleasure should be found in doing the right things, even if they amount to little.</p>
<figure style="width: 75%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/achilles-meme.jpeg"
alt="That's why no one will remember your nym meme">
</figure>
<p>And you? Will you mine? Why not?</p>
<hr>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -1,134 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<section>
<h2>A degraded pool with a healthy disk</h2>
<p><em>Published: 2026-01-04</em></p>
<p><em>Part 2 of 3 in my "First ZFS Degradation" series. See also <a href="why-i-put-my-vms-on-a-zfs-mirror.html">Part 1: The Setup</a> and <a href="fixing-a-degraded-zfs-mirror.html">Part 3: The Fix</a>.</em></p>
<h3>The "Oh Shit" Moment</h3>
<p>I wasn't even looking for trouble. I was clicking around the Proxmox web UI, exploring some storage views I hadn't noticed before, when I saw it: my ZFS pool was in <strong>DEGRADED</strong> state.</p>
<p>I opened the details. One of my two mirrored drives was listed as <strong>FAULTED</strong>.</p>
<p>I was very surprised. This box and disks were brand new and didn't even have three months of running on them. I was not expecting HW issues to come at me that fast. I SSH'd into the server and ran the command that would become my best friend over the next 24 hours:</p>
<pre><code>zpool status -v proxmox-tank-1</code></pre>
<p>No glitch. The pool was degraded. The drive had racked up over 100 read errors, 600+ write errors, and 129 checksum errors. ZFS had given up on it.</p>
<pre><code> NAME STATE READ WRITE CKSUM
proxmox-tank-1 DEGRADED 0 0 0
mirror-0 DEGRADED 0 0 0
ata-ST4000NT001-3M2101_WX11TN0Z FAULTED 108 639 129 too many errors
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0</code></pre>
<p>The good news: <code>errors: No known data errors</code>. ZFS was serving all my data from the healthy drive. Nothing was lost yet.</p>
<p>The bad news: I was running on a single point of failure. If AGAPITO2 decided to have a bad day too, I'd be in real trouble.</p>
<p>I tried the classic IT move: rebooting. The system came back up and ZFS immediately started trying to resilver (rebuild) the degraded drive. But within minutes, the errors started piling up again and the resilver stalled.</p>
<p>Time to actually figure out what was wrong.</p>
<h3>The Diagnostic Toolbox</h3>
<p>When a ZFS drive acts up, you have two main sources of truth: what the <strong>kernel</strong> sees happening at the hardware level, and what the <strong>drive itself</strong> reports about its health. This can be looked up with <code>dmesg</code> and <code>smartctl</code>.</p>
<h4>dmesg: The Kernel's Diary</h4>
<p>The Linux kernel maintains a ring buffer of messages about hardware events, driver activities, and system operations. The <code>dmesg</code> command lets you read it. For disk issues, you want to grep for SATA-related keywords:</p>
<pre><code>dmesg -T | egrep -i 'ata[0-9]|sata|reset|link|i/o error' | tail -100</code></pre>
<p>The <code>-T</code> flag gives you human-readable timestamps instead of seconds-since-boot.</p>
<p>What I saw was... weird. Here's an excerpt:</p>
<pre><code>[Fri Jan 2 22:25:13 2026] ata4.00: exception Emask 0x50 SAct 0x70220001 SErr 0xe0802 action 0x6 frozen
[Fri Jan 2 22:25:13 2026] ata4.00: irq_stat 0x08000000, interface fatal error
[Fri Jan 2 22:25:13 2026] ata4.00: failed command: READ FPDMA QUEUED
[Fri Jan 2 22:25:13 2026] ata4: hard resetting link
[Fri Jan 2 22:25:14 2026] ata4: SATA link down (SStatus 0 SControl 300)</code></pre>
<p>Let me translate: the kernel tried to read from the drive on <code>ata4</code>, got a "fatal error," and responded by doing a hard reset of the SATA link. Then the link went down entirely. The drive just... disappeared.</p>
<p>But it didn't stay gone. A few seconds later:</p>
<pre><code>[Fri Jan 2 22:25:24 2026] ata4: link is slow to respond, please be patient (ready=0)
[Fri Jan 2 22:25:24 2026] ata4: SATA link up 6.0 Gbps (SStatus 133 SControl 300)</code></pre>
<p>The drive came back! At full speed! But then...</p>
<pre><code>[Fri Jan 2 22:25:29 2026] ata4.00: qc timeout after 5000 msecs (cmd 0xec)
[Fri Jan 2 22:25:29 2026] ata4.00: failed to IDENTIFY (I/O error, err_mask=0x4)
[Fri Jan 2 22:25:29 2026] ata4: limiting SATA link speed to 3.0 Gbps</code></pre>
<p>It failed again. The kernel, trying to be helpful, dropped the link speed from 6.0 Gbps to 3.0 Gbps. Maybe a slower speed would be more stable?</p>
<p>It wasn't. The pattern repeated: connect, fail, reset, reconnect at a slower speed. 6.0 Gbps, then 3.0 Gbps, then 1.5 Gbps. Eventually:</p>
<pre><code>[Fri Jan 2 22:27:06 2026] ata4.00: disable device</code></pre>
<p>The kernel gave up entirely.</p>
<p>This wasn't what a dying drive looks like. A dying drive throws read errors on specific bad sectors. This drive was connecting and disconnecting like someone was jiggling the cable. The kernel was calling it "interface fatal error", emphasis on <em>interface</em>.</p>
<h4>smartctl: Asking the Drive Directly</h4>
<p>Every modern hard drive has S.M.A.R.T. (Self-Monitoring, Analysis, and Reporting Technology) — basically a built-in health monitor. The <code>smartctl</code> command lets you get info out of it.</p>
<p>First, the overall health check:</p>
<pre><code>smartctl -H /dev/sdb</code></pre>
<pre><code>SMART overall-health self-assessment test result: PASSED</code></pre>
<p>Okay, that looks great. But if the disk is healthy, what the hell is going on, and where are all those errors that ZFS was spotting coming from?</p>
<p>Let's dig deeper with the extended info:</p>
<pre><code>smartctl -x /dev/sdb</code></pre>
<p>The key attributes I was looking for:</p>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Value</th>
<th>What it means</th>
</tr>
</thead>
<tbody>
<tr>
<td>Reallocated_Sector_Ct</td>
<td>0</td>
<td>Bad sectors the drive has swapped out. Zero is good.</td>
</tr>
<tr>
<td>Current_Pending_Sector</td>
<td>0</td>
<td>Sectors waiting to be checked. Zero is good.</td>
</tr>
<tr>
<td>UDMA_CRC_Error_Count</td>
<td>0</td>
<td>Data corruption during transfer. Zero is good.</td>
</tr>
<tr>
<td>Number of Hardware Resets</td>
<td>39</td>
<td>Times the connection has been reset. Uh...</td>
</tr>
</tbody>
</table>
<p>All the sector-level health metrics looked perfect. No bad blocks, no pending errors, no CRC errors. The drive's magnetic platters and read/write heads were fine.</p>
<p>But 39 hardware resets? That's not normal. That's the drive (or its connection) getting reset nearly 40 times.</p>
<p>I ran the short self-test to be sure:</p>
<pre><code>smartctl -t short /dev/sdb
# Wait a minute...
smartctl -l selftest /dev/sdb</code></pre>
<pre><code># 1 Short offline Completed without error 00%</code></pre>
<p>The drive passed its own self-test. The platters spin, the heads move, the firmware works, and it can read its own data just fine.</p>
<h3>Hypothesis</h3>
<p>At this point, the evidence was pointing clearly away from "the drive is dying" and toward "something is wrong with the connection."</p>
<p>What the kernel logs told me: the drive keeps connecting and disconnecting. Each time it reconnects, the kernel tries slower speeds. Eventually it gives up entirely. This is what you see with an unstable physical connection.</p>
<p>What SMART told me: the drive itself is healthy. No bad sectors, no media errors, no signs of wear. But there have been dozens of hardware resets — the connection keeps getting interrupted.</p>
<p>The suspects, in order of likelihood:</p>
<ol>
<li><strong>SATA data cable</strong>: the most common culprit for intermittent connection issues. Cables go bad, or weren't seated properly in the first place.</li>
<li><strong>Power connection</strong>: if the drive isn't getting stable power, it might brown out intermittently.</li>
<li><strong>SATA port on the motherboard</strong>: less likely, but possible.</li>
<li><strong>PSU</strong>: power supply issues could affect the power rail feeding the drive. Unlikely, since both disks where feeding from the same cable tread, but still an option.</li>
</ol>
<p>Given that I had just built this server a few weeks earlier, and a good part of that happened after midnight... I was beginning to suspect that perhaps I simply might not have plugged in the disk properly.</p>
<h3>The Verdict</h3>
<p>I was pretty confident now: the drive was fine, but the connection was bad. Most likely the SATA data cable, and most probably simply not connected properly.</p>
<p>The fix would require shutting down the server, opening the case, and reseating (or replacing) cables. Before doing that, I wanted to take the drive offline cleanly and document everything.</p>
<p>In <a href="fixing-a-degraded-zfs-mirror.html">Part 3</a>, I'll walk through exactly how I fixed it: the ZFS commands, the physical work, and the validation to make sure everything was actually okay afterward.</p>
<p><em>Continue to <a href="fixing-a-degraded-zfs-mirror.html">Part 3: The Fix</a></em></p>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -14,7 +14,6 @@
<hr /> <hr />
<section> <section>
<h2>A note for the future: the tax bleeding in 2025</h2> <h2>A note for the future: the tax bleeding in 2025</h2>
<p><em>Published: 2025-08-18</em></p>
<p> <p>
I hate taxes deeply. I fell through the rabbit hole of libertarian and I hate taxes deeply. I fell through the rabbit hole of libertarian and
anarcocapitalist ideas some years ago, and taxes have been repulsive anarcocapitalist ideas some years ago, and taxes have been repulsive

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>A simple solution to spam</h2> <h2>A simple solution to spam</h2>
<p><em>Published: 2025-01-14</em></p>
<p>Spam is a problem that has plagued the Internet for decades already. Given a negligible cost to reach <p>Spam is a problem that has plagued the Internet for decades already. Given a negligible cost to reach
out, and easy access to some contact method, many annoying people will choose to mass bother all of us out, and easy access to some contact method, many annoying people will choose to mass bother all of us
in hopes of striking a profit. in hopes of striking a profit.

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>Bitcoin mining is like adding the final piece to a puzzle</h2> <h2>Bitcoin mining is like adding the final piece to a puzzle</h2>
<p><em>Published: 2025-02-01</em></p>
<p> <p>
Bitcoin mining is one of those terribly explained topics that everyone and their mother has kind of Bitcoin mining is one of those terribly explained topics that everyone and their mother has kind of
heard about at some point, but no one really feels comfortable explaining. heard about at some point, but no one really feels comfortable explaining.

View file

@ -1,359 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<hr>
<section>
<h2>Busy man's guide to optimizing dbt models performance</h2>
<p><em>Published: 2026-02-10</em></p>
<p>The below guide is a copy-paste of an internal doc file I created while working in Superhog. My team of
analysts were very smart guys, but had little knowledge on Postgres internals (we used Postgres for our
DWH) and low-level query optimization. That was understandable: they were analysts, busy with answering
business questions. Their value was not in fixing pipeline performance.</p>
<p>Nevertheless, giving them some degree of freedom in fixing performance was both great to avoid me
becoming a bottleneck and to also expand their knowledge, which they were eager to. This guide was
targeted to them, and the goal was to give them as many tools as possible without having to go into the
rabbit hole of <code>EXPLAIN ANALYZE</code>.</p>
<hr>
<p>You have a <code>dbt</code> model that takes ages to run in production. For some very valid reason, this
is a problem.</p>
<p>This is a small reference guide on things you can try. I suggest you try them from start to end, since
they are sorted in a descending way by value/complexity ratio.</p>
<p>Before you start working on a model, you might want to check <a href="#bonus">the bonus guide at the
bottom</a> to learn how to make sure you don't change the outputs of a model while refactoring it.
</p>
<p>If you've tried everything you could here and things still don't work, don't hesitate to call Pablo.</p>
<h3>1. Is your model <em>really</em> taking too long?</h3>
<blockquote>Before you optimize a model that is taking too long, make sure it actually takes too long.
</blockquote>
<p>The very first step is to really assess if you do have a problem.</p>
<p>We run our DWH in a Postgres server, and Postgres is a complex system. Postgres is doing many things at
all times and it's very stateful, which means you will pretty much never see <em>exactly</em> the same
performance twice for some given query.</p>
<p>Before going crazy optimizing, I would advise running the model or the entire project a few times and
observing the behaviour. It might be that <em>some day</em> it took very long for some reason, but
usually, it runs just fine.</p>
<p>You also might want to do this in a moment where there's little activity in the DWH, like very early or
late in the day, so that other users' activity in the DWH don't pollute your observations.</p>
<p>If this is a model that is already being run regularly, we can also leverage the statistics
collected by the <code>pg_stat_statements</code> Postgres extension to check what are the min, avg, and
max run times for it. Ask Pablo to get this.</p>
<h3>2. Reducing the amount of data</h3>
<blockquote>Make your query only bring in the data it needs, and not more. Reduce the amount of data as
early as possible.</blockquote>
<p>This option is a simple optimization trick that can be used in many areas and it's easy to pull off.</p>
<p>The two holy devils of slow queries are large amounts of data and monster lookups/sorts. Both can be
drastically reduced by simply reducing the amount of data that goes into the query, typically by applying
some smart <code>WHERE</code> or creative conditions on a <code>JOIN</code> clause. This can be either
done in your basic CTEs where you read from other models, or in the main <code>SELECT</code> of your
model.</p>
<p>Typically, try to make this as <em>early</em> as possible in the model. Early here refers to the steps
of your query. In your queries, you will typically:</p>
<ul>
<li>read a few tables,</li>
<li>do some <code>SELECTs</code></li>
<li>then do more crazy logic downstream with more <code>SELECTs</code></li>
<li>and the party goes on for as long and complex your case is</li>
</ul>
<p>Reducing the amount of data at the end is pointless. You will still need to read a lot of stuff early and
have monster <code>JOIN</code>s , window functions, <code>DISTINCT</code>s, etc. Ideally, you want to do
it when you first access an upstream table. If not there, then as early as possible within the logic.
</p>
<p>The specifics of how to apply this are absolutely query dependent, so I can't give you magic instructions
for the query you have at hand. But let me illustrate the concept with an example:</p>
<h4>Only hosts? Then only hosts</h4>
<p>You have a table <code>stg_my_table</code> with a lot of data, let's say 100 million records, and each
record has the id of a host. In your model, you need to join these records with the host user data to
get some columns from there. So right now your query looks something like this (tables fictional, this
is not how things look in DWH):</p>
<pre><code>with
stg_my_table as (select * from {{ ref("stg_my_table") }}),
stg_users as (select * from {{ ref("stg_users")}})
select
...
from stg_my_table t
left join
stg_users u
on t.id_host_user = id_user</code></pre>
<p>At the time I'm writing this, the real user table in our DWH has like 600,000 records. This means
that:</p>
<ul>
<li>The CTE <code>stg_users</code> will need to fetch 600,000 records, with all their data, and store
them.</li>
<li>Then the left join will have to join 100 million records from <code>my_table</code> with the 600,000
user records.</li>
</ul>
<p>Now, this is not working for you because it takes ages. We can easily improve the situation by applying
the principle of this section: reducing the amount of data.</p>
<p>Our user table in the DWH has both hosts and guests. Actually, it has a ~1,000 hosts and everything else
is just guests. This means that:</p>
<ul>
<li>We're fetching around 599,000 guest details that we don't care about at all.</li>
<li>Every time we join a record from <code>my_table</code>, we do so against 600,000 user records when
we only truly care about 1,000 of them.</li>
</ul>
<p>Stupid, isn't it?</p>
<p>Well, imagining that our fictional <code>stg_users</code> tables had a field called
<code>is_host</code>, we can rewrite the query this way to get exactly the same result in only a
fraction of the time:
</p>
<pre><code>with
stg_my_table as (select * from {{ ref("stg_my_table") }}),
stg_users as (
select *
from {{ ref("stg_users")}}
where is_host = true
)
select
...
from stg_my_table t
left join
stg_users u
on t.id_host_user = id_user</code></pre>
<p>It's simple to understand: the CTE will now only get the 1,000 records related to hosts, which means we
save performance in both fetching that data and having a much smaller join operation downstream against
<code>stg_my_table</code>.
</p>
<h3>3. Controlling CTE materialization</h3>
<blockquote>Tell Postgres when to cache intermediate results and when to optimize through them.</blockquote>
<p>This one requires a tiny bit of understanding of what happens under the hood, but the payoff is big and
the fix is easy to apply.</p>
<h4>What Postgres does with your CTEs</h4>
<p>When Postgres runs a CTE, it has two strategies:</p>
<ul>
<li><strong>Materialized</strong>: Postgres runs the CTE query, stores the full result in a temporary
buffer, and every downstream reference reads from that buffer. Think of it as Postgres creating a
temporary, index-less table with the CTE's output.</li>
<li><strong>Not materialized</strong>: Postgres treats the CTE as if it were a view. It doesn't store
anything — instead, it folds the CTE's logic into the rest of the query and optimizes everything
together. This means it can push filters down, use indexes from the original tables, and skip
reading rows it doesn't need.</li>
</ul>
<p>By default, Postgres decides for you: if a CTE is referenced once, it inlines it. If it's referenced
more than once, it materializes it.</p>
<p>The problem is that this default isn't always ideal, especially with how we write dbt models.</p>
<h4>Why this matters for our dbt models</h4>
<p>Following our conventions, we always import upstream refs as CTEs at the top of the file:</p>
<pre><code>with
stg_users as (select * from {{ ref("stg_users") }}),
stg_bookings as (select * from {{ ref("stg_bookings") }}),
some_intermediate_logic as (
select ...
from stg_users
join stg_bookings on ...
where ...
),
some_other_logic as (
select ...
from stg_users
where ...
)
select ...
from some_intermediate_logic
join some_other_logic on ...</code></pre>
<p>Notice that <code>stg_users</code> is referenced twice — once in <code>some_intermediate_logic</code>
and once in <code>some_other_logic</code>. This means Postgres will materialize it by default. What
happens then is:</p>
<ol>
<li>Postgres scans the entire <code>stg_users</code> table and copies all 600,000 rows into a temporary
buffer.</li>
<li>If the buffer exceeds available memory, it spills to disk.</li>
<li>Every downstream CTE that reads from <code>stg_users</code> does a sequential scan of that buffer.
Note this means indices can't be used, even if the original table had them.</li>
<li>Any filters that downstream CTEs apply to <code>stg_users</code> (like
<code>where is_host = true</code>) can't be pushed down to the original table scan. Postgres reads
all 600,000 rows first, stores them, and only then filters.
</li>
</ol>
<p>All of that, for a <code>select *</code> that does absolutely no computation worth caching.</p>
<h4>The fix</h4>
<p>You can explicitly control this behaviour by adding <code>MATERIALIZED</code> or
<code>NOT MATERIALIZED</code> to any CTE:
</p>
<pre><code>with
stg_users as not materialized (select * from {{ ref("stg_users") }}),
stg_bookings as not materialized (select * from {{ ref("stg_bookings") }}),
some_intermediate_logic as (
...
),
some_other_logic as (
...
)
select ...</code></pre>
<p>With <code>NOT MATERIALIZED</code>, Postgres treats those import CTEs as transparent aliases. It can see
straight through to the original table, use its indexes, and push filters down.</p>
<h4>When to use which</h4>
<p>The rule of thumb is simple:</p>
<ul>
<li><strong>Cheap CTE, referenced multiple times</strong><code>NOT MATERIALIZED</code>. This is the
typical case for our import CTEs at the top of the file. There's no computation to cache, so
materializing just wastes resources.</li>
<li><strong>Expensive CTE, referenced multiple times</strong> → leave it alone (or explicit
<code>MATERIALIZED</code>). If a CTE does heavy aggregations, complex joins, or window functions,
materializing means that work happens once. Without it, Postgres would repeat the expensive query
every time the CTE is referenced.
</li>
<li><strong>Any CTE referenced only once</strong> → doesn't matter. Postgres inlines it automatically.
</li>
</ul>
<p>If you're unsure whether a CTE is "expensive enough" to warrant materialization, just try both and
measure. There's no shame in that.</p>
<h3>4. Change upstream materializations</h3>
<blockquote>Materialize upstream models as tables instead of views to reduce computation on the model at
hand.</blockquote>
<p>Going back to basics, dbt offers <a href="https://docs.getdbt.com/docs/build/materializations"
target="_blank" rel="noopener noreferrer">multiple materializations strategies for our models</a>.
</p>
<p>Typically, for reasons that we won't cover here, the preferred starting point is to use views. We only go
for tables or incremental materializations if there are good reasons for this.</p>
<p>If you have a model that is having terrible performance, it's possible that the fault doesn't sit at the
model itself, but rather at an upstream model. Let me make an example.</p>
<p>Imagine we have a situation with three models:</p>
<ul>
<li><code>stg_my_simple_model</code>: a model with super simple logic and small data</li>
<li><code>stg_my_crazy_model</code>: a model with a crazy complex query and lots of data</li>
<li><code>int_my_dependant_model</code>: an int model that reads from both previous models.</li>
<li>Where the staging models are set to materialize as views and the int model is set to materialize as
a table.</li>
</ul>
<p>Because the two staging models are set to materialize as views, this means that every time you run
<code>int_my_dependant_model</code>, you will also have to execute the queries of
<code>stg_my_simple_model</code> and <code>stg_my_crazy_model</code>. If the upstream views model are
fast, this is not an issue of any kind. But if a model is a heavy query, this could be an issue.
</p>
<p>The point is, you might notice that <code>int_my_dependant_model</code> takes 600 seconds to run and
think there's something wrong with it, when actually the fault sits at <code>stg_my_crazy_model</code>,
which perhaps is taking 590 seconds out of the 600.</p>
<p>How can materializations solve this? Well, if <code>stg_my_crazy_model</code> was materialized as a table
instead of as view, whenever you ran <code>int_my_dependant_model</code> you would simply read from a
table with pre-populated results, instead of having to run the <code>stg_my_crazy_model</code> query
each time. Typically, reading the results will be much faster than running the whole query. So, in
summary, by making <code>stg_my_crazy_model</code> materialize as a table, you can fix your performance
issue in <code>int_my_dependant_model</code>.</p>
<h3>5. Switch the model to materialization to <code>incremental</code></h3>
<blockquote>Make the processing of the table happen in small batches instead of on all data to make it more
manageable.</blockquote>
<p>Imagine we want to count how many bookings were created each month.</p>
<p>As time passes, more and more months and more and more bookings appear in our history, making the size of
this problem ever increasing. But then again, once a month has finished, we shouldn't need to go back
and revisit history: what's done is done, and only the ongoing month is relevant, right?</p>
<p><a href="https://docs.getdbt.com/docs/build/incremental-models" target="_blank"
rel="noopener noreferrer">dbt offers a materialization strategy named <code>incremental</code></a>,
which allows you to only work on a subset of data. This means that every time you run
<code>dbt run</code> , your model only works on a certain part of the data, and not all of it. If the
nature of your data and your needs allows isolating each run to a small part of all upstream data, this
strategy can help wildly improve the performance.
</p>
<p>Explaining the inner details of <code>incremental</code> goes beyond the scope of this page. You can
check the official docs from <code>dbt</code> (<a
href="https://docs.getdbt.com/docs/build/incremental-models" target="_blank"
rel="noopener noreferrer">here</a>), ask the team for support or check some of the incremental
models that we already have in our project and use them as references.</p>
<p>Note that using <code>incremental</code> strategies makes life way harder than simple <code>view</code>
or <code>table</code> ones, so only pick this up if it's truly necessary. Don't make models incremental
without trying other optimizations first, or simply because you realise that you <em>could</em> use it
in a specific model.</p>
<h3>6. End of the line: general optimization</h3>
<p>The final tip is not really a tip. The above five things are the easy-peasy, low hanging fruit stuff that
you can try. This doesn't mean that there isn't more than you can do, just that I don't know of more
simple stuff that you can try without deep knowledge of how Postgres works beneath and a willingness to
get your hands <em>real</em> dirty.</p>
<p>If you've reached this point and your model is still performing poorly, you either need to put your Data
Engineer hat on and really deepen your knowledge… or call Pablo.</p>
<h3 id="bonus">Bonus: how to make sure you didn't screw up and change the output of the model</h3>
<p>The topic we are discussing in this guide is making refactors purely for the sake of performance, without
changing the output of the given model. We simply want to make the model faster, not change what data it
generates.</p>
<p>That being the case, and considering the complexity of the strategies we've presented here, being afraid
that you messed up and accidentally changed the output of the model is a very reasonable fear to have.
That's a kind of mistake that we definitely want to avoid.</p>
<p>Doing this manually can be a PITA and very time consuming, which doesn't help at all.</p>
<p>To make your life easier, I'm going to show you a new little trick.</p>
<h4>Hashing tables and comparing them</h4>
<p>I'll post a snippet of code here that you can run to compare if any pair of tables has <em>exactly</em>
the same contents. Emphasis on exactly. Changing the slightest bit of content will be detected.</p>
<pre><code>SELECT md5(array_agg(md5((t1.*)::varchar))::varchar)
FROM (
SELECT *
FROM my_first_table
ORDER BY &lt;whatever field is unique&gt;
) AS t1
SELECT md5(array_agg(md5((t2.*)::varchar))::varchar)
FROM (
SELECT *
FROM my_second_table
ORDER BY &lt;whatever field is unique&gt;
) AS t2</code></pre>
<p>How this works is: you execute the two queries, which will return a single value each. Some hexadecimal
gibberish.</p>
<p>If the output of the two queries is identical, it means their contents are identical. If they are
different, it means there's something different across both.</p>
<p>If you don't understand how this works, and you don't care, that's fine. Just use it.</p>
<p>If not knowing does bother, you should go down the rabbit holes of hash functions and deterministic
serialization.</p>
<h4>Including this in your refactoring workflow</h4>
<p>Right, now you know how to make sure that two tables are identical.</p>
<p>This is dramatically useful for your optimization workflow. You can know simply:</p>
<ul>
<li>Keep the original model</li>
<li>Create a copy of it, which is the one you will be working on (the working copy)</li>
<li>Prepare the magic query to check their contents are identical</li>
<li>From this point on, you can enter in this loop for as long as you want/need:
<ul>
<li>Run the magic query to ensure you start from same-output-state</li>
<li>Modify the working copy model to attempt whatever optimization thingie you wanna try</li>
<li>Once you are done, run the magic query again.</li>
<li>If the output is not the same anymore, you screwed up. Start again and avoid whatever
mistake you made.</li>
<li>If the output is still the same, you didn't cause a change in the model output. Either keep
on optimizing or call it day.</li>
</ul>
</li>
<li>Finally, just copy over the working copy model code into the old one and remove the working copy.
</li>
</ul>
<p>I hope that helps. I also recommend doing the loop as frequently as possible. The less things you change
between executions of the magic query, the easier is to realize what caused errors if they appear.</p>
<hr>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>Credit cards, affairs and ChatGPT</h2> <h2>Credit cards, affairs and ChatGPT</h2>
<p><em>Published: 2025-01-31</em></p>
<p> <p>
Many years ago, when I was doing my Bachelor's thesis, I had a fun conversation Many years ago, when I was doing my Bachelor's thesis, I had a fun conversation
with Dolors, my tutor back then. She was a smart woman, and so the interesting chats with Dolors, my tutor back then. She was a smart woman, and so the interesting chats

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>Don't hide it, make it beautiful</h2> <h2>Don't hide it, make it beautiful</h2>
<p><em>Published: 2025-03-17</em></p>
<p>I'm currently living in a flat, and my internet connection physically comes in through my living room. That's where <p>I'm currently living in a flat, and my internet connection physically comes in through my living room. That's where
my home router is placed. However, my main workspace is not in my living room but in my working room, my home router is placed. However, my main workspace is not in my living room but in my working room,
which is a few meters away. I would love to have a wired internet connection for my laptop, but unfortunately, with which is a few meters away. I would love to have a wired internet connection for my laptop, but unfortunately, with

View file

@ -1,189 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<section>
<h2>Fixing a Degraded ZFS Mirror: Reseat, Resilver, and Scrub</h2>
<p><em>Published: 2026-01-04</em></p>
<p><em>Part 3 of 3 in my "First ZFS Degradation" series. See also <a href="why-i-put-my-vms-on-a-zfs-mirror.html">Part 1: The Setup</a> and <a href="a-degraded-pool-with-a-healthy-disk.html">Part 2: Diagnosing the Problem</a>.</em></p>
<h3>The Game Plan</h3>
<p>By now I was pretty confident about what was wrong: not a dying drive, but a flaky SATA connection. The fix should be straightforward. Just take the drive offline, shut down, reseat the cables, bring it back up, and let ZFS heal itself.</p>
<p>But I wanted to do this methodically. ZFS is forgiving, but I didn't want to make things worse by rushing.</p>
<p>Here was my plan:</p>
<ol>
<li>Take the faulty drive offline in ZFS (tell ZFS "stop trying to use this drive")</li>
<li>Power down the server</li>
<li>Open the case, inspect and reseat cables</li>
<li>Boot up, verify the drive is detected</li>
<li>Bring the drive back online in ZFS</li>
<li>Let the resilver complete</li>
<li>Run a scrub to verify data integrity</li>
<li>Check SMART one more time</li>
</ol>
<p>Let's walk through each step.</p>
<h3>Step 1: Taking the Drive Offline</h3>
<p>Before touching hardware, I wanted ZFS to stop trying to use the problematic drive.</p>
<p>First, I set up some variables to avoid typos with that long disk ID:</p>
<pre><code>DISKID="ata-ST4000NT001-3M2101_WX11TN0Z"
DISKPATH="/dev/disk/by-id/$DISKID"</code></pre>
<p>Then I took it offline:</p>
<pre><code>zpool offline proxmox-tank-1 "$DISKID"</code></pre>
<p>Checking the status afterward:</p>
<pre><code>zpool status -v proxmox-tank-1</code></pre>
<pre><code> NAME STATE READ WRITE CKSUM
proxmox-tank-1 DEGRADED 0 0 0
mirror-0 DEGRADED 0 0 0
ata-ST4000NT001-3M2101_WX11TN0Z OFFLINE 108 639 129
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0</code></pre>
<p>The state changed from FAULTED to OFFLINE. ZFS knows I intentionally took it offline rather than it failing on its own. The error counts are still there as a historical record, but ZFS isn't actively trying to use the drive anymore.</p>
<p>Time to shut down and get my hands dirty.</p>
<h3>Step 2: Opening the Case</h3>
<p>I powered down the server and opened up the Fractal Node 804. This case has a lovely design with drive bays accessible from the side, which I love. No reaching out into weird corners in the case, just unscrew a couple screws, slide the drive bay out and there they are, handy and reachable.</p>
<p>I located AGAPITO1 (I had handwritten labels on the drives, lesson learned after many sessions of playing "which drive is which") and inspected the connections.</p>
<p>Here's the honest truth: everything looked fine. The SATA data cable was plugged in. The power connector was plugged in. Nothing was obviously loose or damaged. There was a bit of tension in the cable as it moved from one area of the case (where the motherboard is) to the drives area, but I really didn't think that was affecting the connection to either the drive or the motherboard itself.</p>
<p>But "looks fine" doesn't mean "is fine". So I did a full reseat:</p>
<ul>
<li>Unplugged and firmly replugged the SATA data cable at both ends (drive and motherboard).</li>
<li>Unplugged and firmly replugged the power connector.</li>
<li>While I was in there, checked the connections on the other disk of the mirror as well.</li>
</ul>
<p>I made sure each connector clicked in solidly. Then I closed up the case and hit the power button.</p>
<h3>Step 3: Verifying Detection</h3>
<p>The server booted up. Would Linux see the drive?</p>
<pre><code>ls -l /dev/disk/by-id/ | grep WX11TN0Z</code></pre>
<pre><code>lrwxrwxrwx 1 root root 9 Jan 2 23:15 ata-ST4000NT001-3M2101_WX11TN0Z -> ../../sdb</code></pre>
<p>The drive was there, mapped to <code>/dev/sdb</code>.</p>
<p>I opened a second terminal and started watching the kernel log in real time:</p>
<pre><code>dmesg -Tw</code></pre>
<p>This would show me immediately if the connection started acting flaky again. For now, it was quiet, showing just normal boot messages, the drive being detected successfully, etc. Nothing alarming.</p>
<h3>Step 4: Bringing It Back Online</h3>
<p>Moment of truth. I told ZFS to start using the drive again:</p>
<pre><code>zpool online proxmox-tank-1 "$DISKID"</code></pre>
<p>Immediately checked the status:</p>
<pre><code>zpool status -v proxmox-tank-1</code></pre>
<pre><code> pool: proxmox-tank-1
state: DEGRADED
status: One or more devices is currently being resilvered.
action: Wait for the resilver to complete.
scan: resilver in progress since Fri Jan 2 23:17:35 2026
0B resilvered, 0.00% done, no estimated completion time
NAME STATE READ WRITE CKSUM
proxmox-tank-1 DEGRADED 0 0 0
mirror-0 DEGRADED 0 0 0
ata-ST4000NT001-3M2101_WX11TN0Z DEGRADED 0 0 0 too many errors
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0</code></pre>
<p>Two things to notice: the drive's error counters are now at zero (we're starting fresh), and ZFS immediately started resilvering. It shows "too many errors" as the reason for the degraded state, which is historical, it remembers why the drive was marked bad before.</p>
<p>I kept watching both the status and the kernel log. No errors, no link resets.</p>
<h3>Step 5: The Resilver</h3>
<p>Resilvering is ZFS's term for rebuilding redundancy. Copying data from the healthy drive to the one that fell behind. In my case, the drive had been desynchronized for who knows how long (the pool had drifted 524GB out of sync before I noticed), so there was a lot to copy.</p>
<p>I shut down my VMs to reduce I/O contention and let the resilver have the disk bandwidth. Progress:</p>
<pre><code>scan: resilver in progress since Fri Jan 2 23:17:35 2026
495G / 618G scanned, 320G / 618G issued at 100M/s
320G resilvered, 51.78% done, 00:50:12 to go</code></pre>
<p>The kernel log stayed quiet the whole time. Everything was indicating the cable reseat had worked.</p>
<p>I went to bed and let it run overnight. The next morning:</p>
<pre><code>scan: resilvered 495G in 01:07:58 with 0 errors on Sat Jan 3 00:25:33 2026</code></pre>
<p>495 gigabytes resilvered in about an hour, zero errors. But the pool still showed DEGRADED with a warning about "unrecoverable error." I was very confused about this, but I solved that with some research. Apparently, ZFS is cautious and wants human acknowledgement before declaring everything healthy again.</p>
<pre><code>zpool clear proxmox-tank-1 ata-ST4000NT001-3M2101_WX11TN0Z</code></pre>
<p>This command clears the error flags. Immediately:</p>
<pre><code> pool: proxmox-tank-1
state: ONLINE
scan: resilvered 495G in 01:07:58 with 0 errors on Sat Jan 3 00:25:33 2026
NAME STATE READ WRITE CKSUM
proxmox-tank-1 ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
ata-ST4000NT001-3M2101_WX11TN0Z ONLINE 0 0 0
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0</code></pre>
<p>Damn, seeing this felt nice.</p>
<h3>Step 6: The Scrub</h3>
<p>A resilver copies data to bring the drives back in sync, but it doesn't verify that all the existing data is still good. For that, you run a scrub. ZFS reads every block on the pool, verifies checksums, and repairs anything that doesn't match.</p>
<pre><code>zpool scrub proxmox-tank-1</code></pre>
<p>I let this run while I brought my VMs back up (scrubs can run in the background without blocking normal operations, though performance takes a hit). A few hours later:</p>
<pre><code>scan: scrub repaired 13.0M in 02:14:22 with 0 errors on Sat Jan 3 11:03:54 2026
NAME STATE READ WRITE CKSUM
proxmox-tank-1 ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
ata-ST4000NT001-3M2101_WX11TN0Z ONLINE 0 0 992
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0</code></pre>
<p>Interesting. The scrub repaired 13MB of data and found 992 checksum mismatches on AGAPITO1. From what I read, checksum errors are typically a sign of the disk being in terrible shape and needing a replacement ASAP. That sounds scary, but I took the risk and assumed those were blocks that had been written incorrectly (or not at all) during the period when the connection was flaky, and not an issue with the disk itself. ZFS detected the bad checksums and healed them using the good copies from AGAPITO2.</p>
<p>I cleared the errors again and the pool was clean:</p>
<pre><code>zpool clear proxmox-tank-1 ata-ST4000NT001-3M2101_WX11TN0Z</code></pre>
<h3>Step 7: Final Validation with SMART</h3>
<p>One more check. I wanted to see if SMART had anything new to say about the drive after all that activity:</p>
<pre><code>smartctl -x /dev/sdb | egrep -i 'overall|Reallocated|Pending|CRC|Hardware Resets'</code></pre>
<pre><code>SMART overall-health self-assessment test result: PASSED
5 Reallocated_Sector_Ct PO--CK 100 100 010 - 0
197 Current_Pending_Sector -O--C- 100 100 000 - 0
199 UDMA_CRC_Error_Count -OSRCK 200 200 000 - 0
0x06 0x008 4 41 --- Number of Hardware Resets</code></pre>
<p>Still passing. The hardware reset count went from 39 to 41 — just the reboots I did during this process.</p>
<p>For completeness, I ran the long self-test. The short test only takes a minute and does basic checks, the long test actually reads every sector on the disk, which for a 4TB drive takes... a while.</p>
<pre><code>smartctl -t long /dev/sdb</code></pre>
<p>The estimated time was about 6 hours. In practice, it took closer to 12. Running VMs in parallel probably didn't help.</p>
<p>But eventually:</p>
<pre><code>SMART Self-test log structure revision number 1
Num Test_Description Status Remaining LifeTime(hours) LBA_of_first_error
# 1 Extended offline Completed without error 00% 1563 -
# 2 Short offline Completed without error 00% 1551 -
# 3 Short offline Completed without error 00% 1462 -</code></pre>
<p>The extended test passed. Every sector on the disk is readable. The drive is genuinely healthy — it was just the connection that was bad.</p>
<h3>Lessons Learned</h3>
<ul>
<li><strong>ZFS did exactly what it's supposed to do:</strong> Despite 524+ gigabytes of desync and nearly a thousand checksum errors, I lost zero data and was back on action while keeping my VMs running. The healthy drive kept serving everything while the flaky drive was acting up, and once the connection was fixed, ZFS healed itself automatically. Also, I was operating for an unknown amount of time with only one drive. In this case it seems it was due to stupid me messing up cable management, but I'm very happy knowing if the disk had been genuinely faulty, services would have continued just fine.</li>
<li><strong>Physical connections matter:</strong> It's easy to not pay that much attention when building a new box. Well, it bites back.</li>
<li><strong>Monitor your pools.</strong> I only found this issue by accident, clicking around in the Proxmox UI. The pool had been degraded for who knows how long before I noticed. I'm already working in setting up a monitor to my Uptime Kuma instance so that next time the pool status stops being ONLINE I get notified immediately.</li>
</ul>
<p>I'm happy I was able to test out recoverying from a faulty disk with such a tiny issue. I learned a lot fixing it, and now I'm even more happy than before having decided to go for this ZFS pool setup.</p>
<h3>Quick Reference: The Commands</h3>
<p>For future me (and anyone else who ends up here with a degraded pool):</p>
<pre><code># Check pool status
zpool status -v <pool>
# Watch kernel logs in real time
dmesg -Tw
# Check SMART health
smartctl -H /dev/sdX
smartctl -x /dev/sdX
# Take a drive offline before physical work
zpool offline <pool> <device-id>
# Bring a drive back online
zpool online <pool> <device-id>
# Clear error flags after recovery
zpool clear <pool> <device-id>
# Run a scrub to verify all data
zpool scrub <pool>
# Run SMART self-tests
smartctl -t short /dev/sdX # Quick test (~1 min)
smartctl -t long /dev/sdX # Full surface scan (hours)
smartctl -l selftest /dev/sdX # Check test results</code></pre>
<p><em>Thanks for reading! This was <a href="fixing-a-degraded-zfs-mirror.html">Part 3: The Fix</a>. You might also enjoy <a href="why-i-put-my-vms-on-a-zfs-mirror.html">Part 1: The Setup</a> and <a href="a-degraded-pool-with-a-healthy-disk.html">Part 2: Diagnosing the Problem</a>.</em></p>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -15,7 +15,6 @@
<hr /> <hr />
<section> <section>
<h2>Gresham's Law has nothing to do with Bitcoin</h2> <h2>Gresham's Law has nothing to do with Bitcoin</h2>
<p><em>Published: 2025-05-26</em></p>
<p> <p>
This is going to be a thorough explanation for a simple thing, but we This is going to be a thorough explanation for a simple thing, but we
will take it slow since this topic somehow causes loads of confusion. will take it slow since this topic somehow causes loads of confusion.

View file

@ -1,107 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<hr>
<section>
<h2>How I write some articles I have a hard time getting started with</h2>
<p><em>Published: 2026-03-02</em></p>
<p>
I have a lot of shower thoughts. Way more than I can handle. Many times I feel like some of them
would make for a nice piece on my webpage, or I should make a personal note out of. But often times
they won't catch me in the right place and time to sit down, focus, and type them out. You have to
figure out the key points, lay out a plan, think about what you want to say and how, type it all
out, work on the text... It burns some calories. And sometimes it's enough friction to kill the idea
entirely.
</p>
<p>
Recently I found a set of AI tools that allow me to capture some of these ideas and lower the
friction to get them on ink. My goal is not to have the AI write things for me, but just to quickly
drop a pretty decent first draft with very little effort, on the spot. With this, I've happily
rescued many good ideas from getting by falling through the cracks before I even hit my desk (or
getting ignored because I don't have the energy to go through it all).
</p>
<h3>Step one: recording myself</h3>
<p>
I'm self-hosting an open source audio transcription app called
<a href="https://github.com/rishikanthc/Scriberr" target="_blank" rel="noopener noreferrer">scriberr</a>.
It's just a fancy little webpage where you can drop audio and get a transcript. The neat bit is that
it also allows you to simply record on the spot. I used to host
<a href="https://github.com/pluja/whishper" target="_blank" rel="noopener noreferrer">Whishper</a>,
but you had to first record then upload a file. I really liked it, but sadly I found that was
enough friction when rushing on the phone to not use it. I can access scriberr from my laptop, my
desktop, my phone, from anywhere basically.
</p>
<p>
So anytime I feel like it, I can pull out my phone and start ranting about whatever topic is on my
mind. The recording gets sent to my server back home, and scriberr transcribes the whole thing into
text automatically. Transcription itself takes a bit because I'm not using a GPU, but I'm not in a
rush usually since the important thing is just to get the ideas out of my brain and into text easily.
</p>
<p>
The transcript that comes out is usually quite decent in terms of accuracy. I'd say around 95% of
what I say gets picked up perfectly. The whisper models also do pretty decent in different
languages, so I can record in whatever I feel like at each moment.
</p>
<p>
What's not great, in case you've never used transcription tools, is that the output is just a long
stream of words. The model tries its best at punctuation, but it's rather crappy. And obviously, no
paragraphs. Plus, with unprepared, spontaneous ranting, structure tends not to be top notch either.
Definitely not article-grade text out of the box.
</p>
<h3>Letting an LLM clean it up</h3>
<p>
But hey, those are not issues now that we have LLMs! I have a little script that fetches the
transcript results from my server to my laptop. Once I have my raw transcript locally, I just pass
it on to Claude, with a little prompt saying something like: "This is a transcript of me talking
about this and that. Process it for me, I want the output to be like XYZ."
</p>
<p>
Sometimes I really just ask to have the transcript nicely formatted into paragraphs and proper
sentences, with the actual sentences being totally respected. Other times I already ask for some
restructuring of the ideas, so it's not just cleaning up the writing but actually shaping it into
something that resembles an article. It depends on how clear I was during my rant or what I'm really
planning to get out of it.
</p>
<p>
After this bit, I'll either abandon the idea altogether because it wasn't as interesting as I felt
initially, or I'll really work properly in the text when I can and come up with something I'm happy
with. It will still take some desk time to get to a final result, but then the article is really my
writing and not a mix of my own slop with a layer of LLM slopification on top of it. I can't help
but think people who just copy paste LLM output and put their signature under it have little respect
for themselves and their reputation, and little love for the act of thinking and writing.
</p>
<h3>A great starter</h3>
<p>
And there you go. In just a few minutes, without having to focus deeply in front of a blank file, I
end up with a first version of my thoughts that I can already start polishing and thinking from. Not
all transcripts make it into clean texts in the end, but at least I ensure some of the ones which
would otherwise get lost do survive. The stuff that comes out of the LLM is not always great, to be
honest. Sometimes I'll change a lot of things from what I said in the audio. Sometimes I'll end up
adding a lot of stuff that I wasn't covering in my original rant. But the first draft helps me get
my thoughts out there and gives me something to begin with.
</p>
<p>
And that's often all I need.
</p>
<hr>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>I want code defined dashboards so badly</h2> <h2>I want code defined dashboards so badly</h2>
<p><em>Published: 2025-01-14</em></p>
<p>Analysts build dashboards. These are also called reports, data tools, data products, and another <p>Analysts build dashboards. These are also called reports, data tools, data products, and another
gazillion funny names.</p> gazillion funny names.</p>

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>If I started a Data team again</h2> <h2>If I started a Data team again</h2>
<p><em>Published: 2025-02-04</em></p>
<p> <p>
In November 2023, I joined <a href="https://truvi.com/">Truvi</a> (<a In November 2023, I joined <a href="https://truvi.com/">Truvi</a> (<a
href="https://truvi.com/blog/superhog-becomes-truvi/">back then called Superhog</a>) as the first href="https://truvi.com/blog/superhog-becomes-truvi/">back then called Superhog</a>) as the first

View file

@ -15,7 +15,6 @@
<hr /> <hr />
<section> <section>
<h2>Is your drug dealer a homophobic socialist?</h2> <h2>Is your drug dealer a homophobic socialist?</h2>
<p><em>Published: 2025-06-03</em></p>
<p> <p>
Lately, I've noticed a branch of Lately, I've noticed a branch of
<a href="https://en.wikipedia.org/wiki/Cancel_culture" target="_blank" rel="noopener noreferrer">cancel <a href="https://en.wikipedia.org/wiki/Cancel_culture" target="_blank" rel="noopener noreferrer">cancel

View file

@ -1,128 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<hr>
<section>
<h2>My first petahash</h2>
<p><em>Published: 2026-04-16</em></p>
<p>I've recently started mining Bitcoin at a scale I never had before, so I thought it would be interesting
to jot down a few observations on my recent errands.</p>
<p>My friend Unhosted Marcellus has been following closely the evolution of the
<a href="https://ocean.xyz">OCEAN mining pool</a> since its launch. I hadn't used it personally until
recently, so for years all the info I had on it was second hand. What he was most excited about was the
<a href="https://ocean.xyz/docs/datum-setup">DATUM Gateway</a>: the great innovation is that you are
building your own block templates, which is something no other pool does. By using OCEAN with DATUM, you
enjoy the benefit of more stable mining rewards as opposed to lotto mining, while still being a sovereign
miner in the sense that you rely on your own node and you do your own templating. Great news for
decentralization.</p>
<p>The reason I had not bothered with setting all of this up so far was... that I really don't mine much. I
got gifted a cute <a href="https://bitronics.store/collections/bitaxe/products/bitaxe-supra">Bitaxe
Supra</a> from the <a href="https://bitronics.store/">Bitronics Shop</a> that produces some humble
600GH/s, so it felt pointless to do all the setup for such a tiny hashrate.</p>
<p>But then, Unhosted Marcellus started to tell me about these new markets started by Braiins called
<a href="https://hashpower.braiins.com/">Hashpower</a>. Other articles explain the market better, so
I'll leave it up to you to find those to learn about it. Although I must say, if learning is what you
want, nothing beats using it. The TLDR is that you can sign up, send sats, and rent hashrate that you
can point to your own DATUM gateway. And the surprise (at least for me) is how you can literally rent
petahashes for peanuts, when you account for the fact that most of the sats you put towards buying
hashrate will come back as mining rewards.</p>
<p>Unhosted finally triggered me with <a href="https://x.com/oomahq/status/2038703912572571997">this tweet</a>. Cheeky bastard.</p>
<p>I started toying around with a few PH/s, eventually trying out double digit petahashes. There is this
funny feeling to suddenly be controlling the equivalent of tens of thousands of little bitaxes.</p>
<figure style="width: 75%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/many-phs.png" alt="Double digit petahashes on OCEAN">
<figcaption>Double digit petahashes on OCEAN</figcaption>
</figure>
<p>The economics around it are interesting. The bidding prices in Hashpower are usually (not always!) above
hashvalue. It's common to pay a 1%-5% premium over hashvalue. So, the most probable thing is that you
end up operating at a small loss. This is not strictly guaranteed if you mine with OCEAN, since the luck
factor is important and can easily swing rewards +-10%. So unless you mine at a stable rate with a
months-long time horizon, luck is going to play a more important role than the premium on the
hashrate.</p>
<p>To optimize your outcome, it is important to constantly update your bids in Hashpower. Bids are set at a
fixed price in sats, so as the market auction moves every few seconds, you will be either overpaying or
end up unserviced.</p>
<figure style="width: 75%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/order-book-bids-here.png" alt="Hashpower order book">
<figcaption>Hashpower order book</figcaption>
</figure>
<p>On the first days I was using Hashpower, I would log into it multiple times a day to adjust my bids to
stay at the right height of the order book. At first it was fun, then it felt tedious, and it started to
generate this Twitter-esque addiction feeling I didn't like. I quickly concluded I wanted to automate
this out so my only task was to contemplate how pretty my OCEAN hashrate dashboard looked like, and I
could leave behind pulling levers in Hashpower's webpage like a financial monkey.</p>
<figure style="width: 75%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/dreaming-bids.jpeg" alt="Dreaming of automated bids">
<figcaption>Dreaming of automated bids</figcaption>
</figure>
<p>I solved this problem for myself with
<a href="https://github.com/counterweightoperator/hashbidder">hashbidder</a>. It's a small CLI tool
that I run every couple minutes with cron. The TLDR is you can give it a config file that reads "I want
to mine at 5PH/s" and the tool will set your bids with two goals in mind:</p>
<ul>
<li>To bring your hashrate in line with your goal (e.g. if you want to be at 5PH/s, and you're
currently averaging 3PH/s in the last 24 hours, it will drive your bids to a total of 7PH/s. If
instead, you're scoring an average of 10PH/s, it will stop your bids completely to let your average
go lower).</li>
<li>To pay as little as possible, but guarantee you get served. The logic here is to set the price right
above the cheapest bid that is being served currently.</li>
</ul>
<p>The result is quite pleasant. Delivery is choppy because, even with frequent updates, trying to be cheap
means you often get dragged into being overbid by others and you stay there for some minutes. But the
self-adjusting hashrate compensates for it: if you've been falling behind a lot recently, hashbidder
will just hash at a higher hashrate to make up for it. I'm currently targeting 5PH/s, and this is what
my OCEAN hashrate timeline looks like.</p>
<figure style="width: 75%; margin: 10px auto;">
<img width="100%" height="auto" src="../static/recent-hashrate.png" alt="OCEAN hashrate timeline">
<figcaption>OCEAN hashrate timeline at ~5PH/s target</figcaption>
</figure>
<p>There are still a few more optimizations I'll add to hashbidder to reduce cost and decrease the
volatility of delivery, but they're just marginal improvements. The gist of it is already there and it's
doing its work fine.</p>
<p>My next steps are simply to sit and watch. I've decided I will pour 10 million sats during a few months
into this setup and then stop to measure what my rewards have totalled to, so I can provide people
interested in this with a real-life report of how everything turned out.</p>
<p>Overall, I'm having lots of fun. Setting this up made me excited in a way that felt oddly similar to the
first time I was setting up lightning nodes. The night I started out my DATUM gateway and pointed some
hashrate to it felt like the night I spun up an LND and started doing some lightning triangles in
<a href="https://lightningnetwork.plus/">Lightning Network+</a>.</p>
<p>Some interesting links in case you want to learn more or give it a shot at mining with rented hash
yourself:</p>
<ul>
<li><a href="https://rentsomehash.com/">rentsomehash.com</a>, guides on how to set up your DATUM
gateway and start mining with rented hash</li>
<li>A video guide from Matthew Kratter:
<a href="https://x.com/mattkratter/status/2043692900190753089">on X</a></li>
<li>You can check what Unhosted tweets here, since he's pretty much obsessed with this and doesn't pay
attention to anything else: <a href="https://x.com/oomahq">https://x.com/oomahq</a>. Also, some
podcasts and articles from him. Many kudos for starting this fire:
<ul>
<li><a href="https://x.com/oomahq/status/2042692591469367692">Interesting tweet #1</a></li>
<li><a href="https://fountain.fm/episode/zwmdkwdhy0jQT5dVkhah">Once Bitten! episode</a></li>
<li><a href="https://www.youtube.com/watch?v=k8ZRNyr3ofA">The Bitcoin Libertarian episode (in
Spanish)</a></li>
</ul>
</li>
</ul>
<hr>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -1,203 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<hr>
<section>
<h2>My fitness journey</h2>
<p><em>Published: 2026-03-03</em></p>
<p>Nowadays I find myself in the best physical shape I've ever been at. I'm stronger than I've ever been.
Pretty decent cardio-wise, not at my peak but close. I'm overall very active, and I can jump into any
physical activity without having second thoughts. I sleep like a baby, perform like a machine, eat like a
pig and it's all good. I'm at a very good weight. And there are so many other things in my life that work
nicely because my body is working nicely.</p>
<p>I'm very happy about this because, for a long time, I really didn't think I would find myself in this
situation. Historically I was not an active person at all. It took many years of back and forth, making
mistakes and trying things, to get my groove in and settle for the habits I have now. And those habits
are great because it's literally costing me no effort to maintain myself in this state.</p>
<p>So I thought I could look back a little, reflect on how I got here. Perhaps you can learn from some of
the mistakes I made and some of the things that worked for me.</p>
<h3>Growing up</h3>
<p>When I was a kid, I was a normal kid. I liked to play and have fun, but I was pretty terrible at sports.
Not a cripple, but surely a rather clumsy kid. I wasn't really talented for most sports, usually falling
in the bottom 25% of the distribution in terms of overall skill and performance.</p>
<p>I guess part of that was just genetics and natural talent, and part of it was simply that my parents were
not the most physically active people. They never engaged in any sports, never did any physical exercise
or went to the gym. So I had no input from them in terms of physical activity or motivation around that
area. I would just play because I was a kid, not because of my parents pushing me to go for it. As I
grew up and playing slowly became less of a thing due to age, sports also slowly faded from my life (and
I was quite happy about it: it's not fun to be the loser systematically).</p>
<p>The only exception in this path is that I did do a couple of years of martial arts when I was around 11
or 12. That was nice, it was a lot of fun and it helped me boost my confidence. And surprisingly, I got
kind of decent at it. Being used to the bottom 25%, just being average at something felt like a massive
success. But then I went into my teenage years and my head was quickly filled with other stuff. I
eventually dropped out of it.</p>
<h3>The dark ages</h3>
<p>During my teenage years, I didn't do shit. I played no sports nor trained my body in any way. I don't
think I ever tried to do anything until I was 18 or 19, already in university. I signed up for the gym a
couple of times, checked online for routines, downloaded them, tried to follow them. Would usually last
two or three months and then drop out. It was boring, not motivating, and I also had no clue what I was
doing. When I look back at how I was training at those times, I can see so many mistakes. I would try to
go too hard, have overly ambitious routines that were completely not sustainable. Nutrition wasn't in
place, technique wasn't in place, exercise choices were probably extremely poor.</p>
<p>I also tried jogging for a bit, but similar experience. Would do it for a while, then eventually lose
consistency. On and off for years.</p>
<p>When I was around 20 or 21, I was living in Germany and really short on money, so signing up for a gym
was not an option. But near where I was living there was a park with some bars, and some days there was
this German fella, I think his name was Leo. He was insane good: super ripped, in extremely good
condition, did all sorts of complicated exercises like it was nothing. The kind of guy you see training
and go "oh wow, that's amazing." He would give me some tips and I really enjoyed training around there
with him. Still, I wasn't extremely consistent. But it was a good intro to doing calisthenics in a bar
park, since the closest I had done was bodyweight training when I did martial arts in my younger years.
</p>
<p>And in that park I fucked up my ribcage doing dips on the parallel bars. One day I went too hard, and I
would guess the technique must have been absolutely terrible as well. That day, after finishing a set, I
just felt this little stab in my chest, which I didn't give much importance to on the spot. Then the next
morning I had super intense pain in my chest every time I tried to move or breathe. I'm pretty confident
I searched online whether I was having a heart attack. It slowly faded but not completely, and it stuck
around for at least a couple of years. That was my first big fuck-up with training and injuring myself.
After that, I learned not to exceed myself with risky exercise like dips, to respect my natural ranges of
motion, and to just take it easy for the most part.</p>
<p>That chest pain got me away from exercise for a long time because any chest exercise would hurt quite a
bit. Push-ups were a no-no. And how's a man supposed to train if he can't do push-ups, right?</p>
<h3>Some progress, then rock bottom</h3>
<p>Around 23 or 24, I went a bit harder with running. Started jogging more regularly, signed up for a 10k,
did a decent job, ran a couple more races, and eventually finished a half marathon in under two hours. A
year or two where I was jogging regularly. That was nice. I eventually lost my habit, but it was a good
experience and there I learned how to do resistance training decently. Also proved myself I was capable
of training for a feat like a half marathon, which was nice since I would have never identified myself as
a runner before.</p>
<p>Then, by the time Covid hit, I was not active at all. Absolutely zero gym, calisthenics, no jogging. And
then we were thrown in our cages with the curfews. Being locked down all day was absolutely terrible. I
wouldn't move around. I was working quite hard at my desk, long days of sitting. My back was hurting like
I was a grandpa. I was smoking a pack of cigarettes a day. Probably drinking more than I should. For a
few months it just spiralled down into some mornings where I would wake up and go "god, I feel like shit
every single day."</p>
<p>Actually, let me backtrace a little. A few months before Covid hit, I had a traffic accident and broke my
leg. Had to get surgery on my knee. I was bedridden for a couple of months, then had to slowly learn how
to walk again. It's hard to explain how bad it is to be bedridden for that long.</p>
<p>So Covid hit on top of that, and I just went worse and worse. Maybe one year, one year and a half into it,
I was in absolute shit shape. At that point, it started to click in my brain that this was not normal, not
good, and not something I wanted. I guess sometimes you really need to lack something to truly appreciate
it and feel the need for it. Greener grasses, yadayada.</p>
<h3>The turnaround</h3>
<p>First thing, I quit smoking. One morning I woke up with my usual chest congestion, you know, where you
have to go to the bathroom and spit the hell out of yourself for five or ten minutes. All morning I was
just feeling it and I was like "I'm so done with this." I quit cold turkey that day, never went back.
Took no effort. Since then, I always advise smokers who want to quit to consider as a possible strategy
to just smoke themselves out. Smoke every single minute, as much as you want. Heck, smoke even when you
don't want to. Maybe that way you'll just get sudden rejection for it like I did.</p>
<p>Then I started training a little at home with a few dumbbells. Basic stuff: rows, squats, floor presses.
And I took it really easy because I was in such terrible shape and I had learned from my leg surgery
rehab that when you're at the bottom, you really have to take it easy coming up. That was one of the most
enlightening things I figured out. When you're in a really bad shape, don't rush it. You will go up
eventually and things will work out, but don't force yourself to do stuff you're not ready to do. Do
something that feels comfy, even trivial I would say, and start doing it regularly. From that point on,
you can start pumping the numbers.</p>
<p>I was extremely busy with work, working from home, so what I would do is train during my lunch break. 15,
20 minutes. Four exercises, a few rows, a few presses, a few squats, maybe just nine sets, done. Cook my
lunch, continue with life. Very humble training for a busy guy.</p>
<p>Precisely because what I was doing was not ambitious, it was pretty convenient. We're talking about
working out for 15 to 20 minutes in a lightweight way at home. I didn't change clothes. I didn't get in
my car and drive to a gym. I would sometimes cook as I was training, leveraging the rest time between
sets. It was extremely convenient and humble, and because of that, I was actually able to make a habit
out of it. One month, two months, three months. Eventually it just grew into a habit. I stayed with this
attitude for at least a year. That was the first time in my life I was actually consistent with strength
training.</p>
<p>The habit stuck and slowly, I increased the intensity, but with an extremely relaxed attitude. I wasn't
tracking myself heavily. I wasn't pushing hard to pump the numbers. I was just happily trying to feel
well and then every now and then I'd throw another half a kilo here, another half a kilo there. Do 4
sets where I used to do 3. Do 8 pushups instead of 6.</p>
<p>I also started jogging casually, doing 5k's here and there. Same principle: very humble jogs with no
intense pace goals, trivial to include in daily life.</p>
<h3>Calisthenics and the bars</h3>
<p>Eventually I started working for a company with a nice office close to the beach, literally a few meters
from the sand. Right in front of the office there was an absolutely amazing calisthenics bar park, with
all the pull-up bars you could figure out, all the grips and heights, parallel bars, row bars, everything
you can imagine. Training in the sand was really nice.</p>
<p>I made a habit of training whenever I went to the office. Sometimes in the morning, sometimes at the end
of the day. I would train at the bars regularly and some days go for a 5k near the beach. I just kept my
sneakers at the office and grabbed them whenever I felt I had the energy.</p>
<p>At this stage I had clearly recovered all of my shitty physical condition from the curfew, smoking and
drinking. By the way, by the beach stage I had already stopped drinking as well. I've been smoke-free
and alcohol-free for a few years now.</p>
<p>Things kept improving. I was feeling so great that I could actually be a bit more ambitious with my
training. And it just worked. It didn't feel like I was pushing myself too hard and I didn't have weird
pains. Also, because I was in decent condition and not smoking or drinking, I was getting incredibly good
sleep, so recovery was much better. I also lost quite a bit of weight due to it all (though most of the
merit I think goes to quitting alcohol completely).</p>
<p>Eventually, I kind of got addicted to the bars. The habit had completely stuck with me. Since then, I
train at different bar parks depending on where I'm working or where life takes me, but I'm always
finding some bar park and going there 2-3 times a week. I even got a weighted vest, so some training
sessions I'll go quite hard with heavy pull-ups and dips.</p>
<p>And that's where I sit today. I'm stronger than I've ever been. I can crank out 10 pull-ups any day of
the week when I'm fresh, or squat cleanly with the missus on my back. And what I today call an easy day
would have been outright impossible for me to perform in 2020. That's pretty cool.</p>
<p>I'm not doing great with jogging right now because I stopped for a few months, tried to pump the numbers
too quickly when I came back (lesson learned), and hurt my Achilles tendon. Working on recovering that,
and then I'll just be very humble again with my 5k's. But right before that, I could run you a sub-hour
10K on any normal day, even if I had not been prepping intensely for it the weeks before.</p>
<p>The only thing I might be missing is some stretch work for flexibility and mobility. I've been thinking I
should probably try some yoga classes. Not to become a master at it, but to have my joints pushed to the
extreme positions every now and then. I'll probably do that at some point.</p>
<h3>Lessons</h3>
<p>If I look back, here's what I would tell my past self, or anyone who's starting out or struggling to make
exercise stick.</p>
<p>The most important thing is to start ridiculously small. So little volume, so little frequency, that it
almost feels pointless. A couple of days a week, five minutes of something that doesn't even feel like a
challenge. You might think that's not getting you anywhere. You're so wrong. Listen: first, you need to
prove yourself that you're capable of doing something trivial consistently, because doing something
consistently is all that matters. Once you prove you can do that, slowly increase, but very slowly.
Don't be in a rush. Don't try to make every session a challenge. You have a lifetime of exercising ahead
of you. Get to a certain level, stay there for a few weeks until you're bored, and then bump it up a
little bit. So little you have a hard time noticing any difference. And then repeat. Same for cardio.
Never jogged? Just go out and do two kilometres. You should be able to pull that in under 15 minutes. Do
that for a month, a couple of times a week, then do 2.5 kilometres. Take years to get to something
significant. But eventually you will get there, which is the important thing. And you'll make it because
you took it easy, you didn't injure yourself, and you didn't give up because your training plan was too
hard and completely incompatible with living a normal life. It feels counterintuitive, but by being kind
of lazy and doing very little, you eventually grow into doing a lot.</p>
<p>Along the same lines, make it trivial to start exercising in your daily life. Remove every bit of
friction you can. If you can afford to have equipment at home, have it ready and accessible, not buried
in some inconvenient closet. If you just have to reach for it and start, there's no excuse. The moment
it takes a couple of minutes to prep for exercise, you can talk yourself out of it. Same for jogging:
have your sneakers and clothes ready to grab, have one route you always do, don't plan it, don't think
about it, just go. Calisthenics bar parks have worked nicely for me precisely because they need zero
prep. You show up and the bars are there, ready for you to jump to them. And don't shy away from short
workouts either. In 15 minutes at a bar park you can do a lot. You're not going to break any records,
but it's way better than doing nothing. I never do long strength training sessions. I'm pretty confident
80-90% of my workouts sit somewhere between 15 and 30 minutes. You can get quite decent results with
that. And if someday you have the time and energy to punish yourself for an hour, be my guest. But don't
feel you have to.</p>
<p>Finally, get your weight sorted. Being at a good weight is insanely convenient for exercise. Running,
lifting, all of it. Being overweight is a fucking pain in the ass, messes with your health in many ways
and just doesn't let your body work properly. If you're clearly overweight, getting your weight down
before you start training hard will make your life a lot easier. Things like running are terribly tolling
when you're heavy, whereas at a healthy weight it feels so much more pleasant, which helps you stick
with it.</p>
<p>That's it. It took me many years to get here, but I'm rolling with it. I'm having fun with the bars,
I'll be having fun jogging with no pressure very soon, might throw the yoga in. I keep it easy. Whenever
I feel like I have some extra energy and bandwidth, I push myself a bit harder for fun. Not on a
schedule, not with any pressure, just because I feel like doing it. Whenever I seem to have no energy or
time, I just do what I can afford to. But the key is: I always do a bit, no matter what.</p>
<hr>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>My tips and tricks when using Postgres as a DWH</h2> <h2>My tips and tricks when using Postgres as a DWH</h2>
<p><em>Published: 2025-04-24</em></p>
<p>In November 2023, I joined Superhog (now called Truvi) to start out the Data team. As part of that, I <p>In November 2023, I joined Superhog (now called Truvi) to start out the Data team. As part of that, I
also drafted and deployed the first version of its data platform. also drafted and deployed the first version of its data platform.
</p> </p>

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>Notes for myself during my departure from Superhog</h2> <h2>Notes for myself during my departure from Superhog</h2>
<p><em>Published: 2025-07-07</em></p>
<p>I'm writing this a few days before my last day at Superhog (now called Truvi). Having a few company <p>I'm writing this a few days before my last day at Superhog (now called Truvi). Having a few company
departures under my belt already, I know a bit on what will come next. I know one part of the drill is departures under my belt already, I know a bit on what will come next. I know one part of the drill is
that 99% of the details of what happened during my tenure at the company will completely disappear from that 99% of the details of what happened during my tenure at the company will completely disappear from

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>One efective but risky way to find the top budget for the vacancy</h2> <h2>One efective but risky way to find the top budget for the vacancy</h2>
<p><em>Published: 2025-02-03</em></p>
<p> <p>
I've seen a lot of people frustrated with not knowing how much a company is willing to pay for a I've seen a lot of people frustrated with not knowing how much a company is willing to pay for a
vacancy. There are multiple ways to find out, which you probably are familiar with if you've been job vacancy. There are multiple ways to find out, which you probably are familiar with if you've been job

View file

@ -1,317 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8" />
<meta viewport="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="../styles.css" />
</head>
<body>
<main>
<h1>Hi, Pablo here</h1>
<p><a href="../index.html">back to home</a></p>
<hr />
<section>
<h2>One property where gold beats Bitcoin</h2>
<p><em>Published: 2026-06-25</em></p>
<p>
Gold is typically mocked by Bitcoiners (along with Peter Schiff). The
honorable grandpa pet rock of sound money was the best thing we ever
had, up until Bitcoin, and now Bitcoiners find it hilarious. No
practical way to verify it, insanely hard to transact with, consistent
inflation (even if not rampant like fiat), must be stored physically...
Once you get used to Bitcoin, there are certainly many properties of
gold that make you wonder why people still hold on to it. I must admit I
have regularly taken part in the memetic "Ha-ha" aggressions towards
gold and had my fair share of laughing when listening to Peter Schiff.
</p>
<p>
But in my mother tongue we say that wise men change their minds when
they're wrong. And recently, as I've become more knowledgable about
Bitcoin (does the rabbit hole ever end?), I've come to realize there is
one category in which gold woops Bitcoin's ass. And I think in the long
term, this will become significant.
</p>
<h3>Wars</h3>
<p>
I'm not an OG. It took me many years to pay any attention to Bitcoin,
and a few more to actually become enlightened and start to understand
what I was looking at. This means I was not around when the Blocksize
wars happened, and thus all I know about it comes from posts, books and
grandpa Simpson styled ramblings of Bitcoiners with more years of
service than me. And as much as the story makes sense, it always felt
more like history than a story. I mean to say, I can understand the
facts I'm told, but I can't imagine going through it. I can't imagine
how it felt. It's just a bunch of cold, timeline facts.
</p>
<p>
But now, time passed since I fell down the rabbit hole. And it seems
we're heading for new wars, and this time it is my turn to live through
them, not just be aware of a few journalistic-styled facts.
</p>
<p>
A bunch of Core developers have been acting stupid for years. I won't go
into details because others have documented this issue much better than
me. Now, theoretically, in the astral
plane of "bitcoin is the invincible honey badger because muh game theory
will fix everything no matter what", a bunch of devs being corrupted by
VC money and flashes of prestige shouldn't be a threat because "the rest
of the network" will go and do what needs be done: fork the code, ignore
whatever proposals which are harmful to Bitcoin they bring, and just
continue to build good code, run it, point hashrate to it.
</p>
<!-- TODO: ADD LINKS TO JON ATTACK INTERVIEW | ADD LINKS TO HODLNAUT PIECES | ADD WHATEVER ELSE MARCELLUS SENDS ME -->
<p>
And it's true: this immune system like reaction is kind of happening.
The recent increase in the adoption of Bitcoin Knots over Bitcoin Core
is testament to it. And now we have BIP110, which keeps on getting more
nodes and more hashrate. I don't know if the softfork will be adopted
widely and become the chain with most work, but surely it seems there is
a reaction to the fuckery that a clique of big pockets with their teams
of devs tried to pull.
</p>
<p>
But even if BIP110 goes through, and we keep preventing and perhaps even
undoing malicious changes and mistakes in the protocol, I'm left
concerned. Because living through this conflict in detail, unlike the
Blocksize Wars, has allowed me to see the trenches with my own eyes: how
plebs think. How they react to the actions of each side. How they form
their opinions. How they come to decide. What they do, and what they
don't.
</p>
<p>And it makes me worry.</p>
<h3>Crowds</h3>
<p>
Most Bitcoiners deeply hate collectivism and instead root for
individualism. Powerful states that take aggression on the individual
with the pretext of favoring the group are not seen with good eyes by
Bitcoiners. I certainly share the feeling. Following that rabbit hole has
led many Bitcoiners to actually find Democracy itself a flawed model:
they see how the crowd is short-sighted, stupidly greedy, aggressive.
How it votes for evil taxes to support unfair wealth redistribution. How
it allows and applauds authoritarian measures promoted for the sake of
some supposed security. The crowd is seen as a dangerous animal, and
representative democracy is a way for powerful evil men to unleash the
crowd on all individuals who won't bend the knee.
</p>
<p>
The view on this problem, I think, is widely shared. The solutions, on
the other hand, tend to be more varied. I find some Bitcoiners don't see
any solution, or think any alternative is worse. Others have fallen in
love with the strong man in charge rethoric, monarchical or in other
flavors. Others fall in between, proposing various forms of Aristocracy.
The common thread is: most people don't have enough virtue to rule over
others and make the world better (or at least not make it a hellhole) in
the process. So either it's only a few, or none. The common thread is the
view that the crowd can't be left in charge, for they will mess everything
up.
</p>
<h3>Meritocracy</h3>
<p>
Bitcoiner is an overloaded word. I find myself using it every day,
meaning different things each time.
</p>
<p>
If the definition of being a Bitcoiner was to have exposure to price,
things would look great: I would guess that group must be at least in
the double digit million size worldwide already.
</p>
<p>
If the definition of being a Bitcoiner was to hold your own keys... that
would narrow things down quite a bit. Single digit million? Perhaps even
less than a million?
</p>
<p>
Notch it up to only people who also run their own node and now we're
certainly under a million. I would be surprised if it was more than a
hundred thousand.
</p>
<p>
If we dive deeper into people who also mine, I don't think there is more
than ten thousand.
</p>
<p>
And I'm picturing the final stage of this peculiar cypherpunk ladder to
Olympus would be... writing their own code. How many can actually fork
Bitcoin Core and work on adjusting the code to their wishes, without
forking themselves off or introducing deadly bugs accidentally?
</p>
<p>A thousand? A hundred?</p>
<p>
What's the picture? There's millions of people who "use" Bitcoin, at
varying heights of the rabbit hole. But probably barely a few thousands
have anything resembling sovereignty in the network. Are you even
sovereign if you don't have the knowledge to fork off on your own,
should someone push a soft fork you don't support into Core, and no one
else write the right URSF you need to run to oppose it?
</p>
<h3>Email and the healthbar that doesn't refill</h3>
<p>
The death of email as a decentralized protocol has been documented by
others thoroughly. I won't try to replicate that here.
</p>
<p>
I'll just emphasize one ugly fact: the capture of email did not happen
suddenly. It happen slowly over decades. It happened as many individual
people took decisions that they probably didn't feel were terrible, even
important, when they were doing so. We would have trouble having someone
point himself in the mirror and yell: "It was me! I killed email".
</p>
<p>
The decentralization of email disappeared because mistakes piled up and
they were not reverted. Why weren't they?
</p>
<p>
Well, that's a question we could ask ourselves today. Why can't we save
email right fucking now?
</p>
<p>
Email died due to many decisions related to spam. There are solutions to
spam that could be embedded into the protocol. But none of those would
be backwards compatible, and that generates a massive issue: if you
apply the solution yourself, you cut yourself out of email since others
didn't follow along. So you need to bring everyone along with you and
have them all apply the same backwards incompatible change with you. The
only issue is that is impossible. We're all too busy doing things to pay
attention and coordinate and trust each other to do such a thing. Does
it feel somewhat familiar to Bitcoin?
</p>
<p>
To me, it does. Millions of people out there holding IOUs. Some of them
puting their life savings into it. But most have no time or willingness
to run a node. And barely a fraction of them will spend any calories
judging what side is in their best interest when a fork war, hard or
soft, comes around. And then only a sliver of a fraction of a minority
would even be able to escape the duality of such a situation and say
"fuck you guys, it's not red or blue, I'll go write my own color and
show you".
</p>
<p>
And so, small bad decisions piled up with no way to undo them, since the
crowd will just accept whatever is on the table, until they had grown
into the cancer that killed the patient. Any time you manage to sneak
any tiny damaging into the protocol, it's as if you've soldered a
leeched onto it. And it's so scary: we already fucked up with Segwit.
Now Core is a bunch of amateurs salaried westerners that sit in a couple
of offices, and they do whatever shit they want on each release and
people just upgrade. And then you go to Twitter and read on the BIP110
discussions and most of it is just ignorants on either side that
understand nothing, slinging noise at each other. And then you go to
reddit and it's just normies memeing about dips and pumps and asking if
DCA-ing 100$ a week will retire them at 32.
</p>
<p>
This all made me think that a decentralized protocol is like a character
in a shooter videogame where there are no health potions, no medkits, no
bandaids. And you only get to play once, no second tries. You can be as
skilled as you want, but it's still a fact someday you'll get hit. With
no way to recover your health, over a long enough time horizon, you will
die.
</p>
<h3>Mix it all up</h3>
<p>
All of this combined is what I find concerning.
</p>
<p>
Most people in this world are not fully fledged participants in Bitcoin. They don't have the willingness to actively engage in the maintenance of the protocol. They don't care. Most don't even know what they don't know. Their "usage" of Bitcoin is like second-hand smoke: the only way they can interact with the network is via tooling that others build and run. The only knowledge they hold on its inner workings come from metaphors brought by third parties, which many times are wrong, biased or outright maliciously crafted to confuse them.
</p>
<p>Instead, only a minuscule minority can play the game properly. People intelligent and educated enough. With filled bags that both give them the freedom and the incentive to engage.</p>
<p>It wouldn't be that terrible if Bitcoin ended up being an aristocratic monetary network. It would be orders of magnitude less useful than if it was global money, surely. But it would surely help, not hurt, the world.
</p>
<p>
But the problem is that there are no meritocratic barriers to participate in Bitcoin. Both brilliant people and absolute brutes are welcome alike. The crowd comes in. And the crowd becomes the stone foundation that makes the protocol resistant to change: you have to convince them all to push changes. </p>
<p>This ought to be good, right? If you want to change Bitcoin, you have to convince the participants that your change is in their best interest, hence nobody can take over maliciously the protocol.
</p>
<p>But the reality is that the crowd is ignorant and lives in an incredibly dense fog of war. They know jackshit of the protocol and how it works. They don't know how software works. They don't understand economics. Their opinions on protocol changes are either non-existant ("Oh, a new version of Core! I'll update right away! Have I read the changelog? What do you mean? What is a changelog?"), or instead formed from the result of online propagandistic crossfire.
</p>
<p>And so, I come to this observation: that the decentralization of Bitcoin surely makes protocol changes expensive. But it hardly guarantees that bad changes are less likely than good changes. With enough resources and patience, I'm confident you have a decent chance at psy-oping the network into crazy shit like supporting tail emission. And once you fuck the protocol, it is very hard to unfuck. After all, it is the crowd who turned email in to Gmail.
</p>
<p>And the healthbar doesn't refill.</p>
<h3>Gold's latest fork war</h3>
<p>
I started this rant heretically proclaiming there is some aspect of gold
that is superior to Bitcoin.
</p>
<p>
Well, do you remember gold's last fork war? Of course not, because there
wasn't one. There's nothing to discuss.
</p>
<p>
I'm not saying gold is superior. Hard to custody, hard to transport,
hard to verify, forever inflationary, tends to be centralized.
</p>
<p>
But at least it escapes the crowd: it is what it is, and no human can
change that. There's no signaling, no forking, no socratic seminar of
people discussing the greatest and latest update proposal from Pieter
Wuille to make TheMempool&trade; 0.7% more schgaboozing, no ugly VCs
pouring money into young devs who still get yelled at by mom when they
don't drop their undies in the dirty clothes bag. No greedy masses who
completely ignore the nature of the technology their betting their
financial future on. No hashrate competitions, no threats of changing
the PoW algorithm. No Tone Vays.
</p>
<p>
I can shit on gold, but the truth is it's done its thing for millenia
because we can't change it. It might be a shit money in the 21st money, but that's because the world has changed, not because gold has enshittified.
</p>
<p>
It makes me wonder: will we ever truly find good money? Is it even
posible?
</p>
<p>
It also makes me think how materially effective it is to indebted in
money: you get to buy real hard things today, only in exchange of
promises of imperfect money in the future. With so many ways for the
unit your liability is denominated to go upside down, letting you run
free with the asset while you pee on the debt.
</p>
<hr />
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -1,243 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<section>
<h2>Replacing a Failed Disk in a ZFS Mirror</h2>
<p><em>Published: 2026-02-21</em></p>
<p>If you've been following along, you know the story: I set up a <a href="why-i-put-my-vms-on-a-zfs-mirror.html">ZFS mirror for my Proxmox VMs</a>, then one of the drives <a href="a-degraded-pool-with-a-healthy-disk.html">started acting flaky</a>, and I <a href="fixing-a-degraded-zfs-mirror.html">diagnosed and fixed what turned out to be a bad SATA connection</a>.</p>
<p>Well, the connection wasn't the whole story. A few weeks after that fix, the same drive, AGAPITO1, started dropping off again. Same symptoms: link resets, speed downgrades, kernel giving up on the connection. I went through the cable swap dance again, tried different SATA ports on the motherboard, tried different cables. Nothing helped. The SATA PHY on the drive itself was failing.</p>
<p>I contacted PcComponentes (where I bought it), RMA'd the drive, and ran degraded on AGAPITO2 alone for about two weeks. Then the replacement arrived. This article covers the process of physically installing a new drive and getting it into the ZFS mirror, from "box on the desk" to "pool healthy, mirror whole."</p>
<h3>The starting point</h3>
<p>Before doing anything, this is what the pool looked like:</p>
<pre><code> pool: proxmox-tank-1
state: DEGRADED
status: One or more devices have been removed.
Sufficient replicas exist for the pool to continue functioning in a
degraded state.
action: Online the device using zpool online' or replace the device with
'zpool replace'.
scan: scrub repaired 0B in 06:55:06 with 0 errors on Tue Feb 17 20:40:50 2026
config:
NAME STATE READ WRITE CKSUM
proxmox-tank-1 DEGRADED 0 0 0
mirror-0 DEGRADED 0 0 0
ata-ST4000NT001-3M2101_WX11TN0Z REMOVED 0 0 0
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0
errors: No known data errors</code></pre>
<p><code>DEGRADED</code> with one drive <code>REMOVED</code>. The old drive (WX11TN0Z) was physically gone, shipped back to PcComponentes. AGAPITO2 (WX11TN2P) was holding down the fort alone.</p>
<p>This is the beauty and the terror of a degraded mirror: everything works fine. Your VMs keep running, your data is intact, reads and writes happen normally. But you have zero redundancy. If that surviving drive has a bad day, you lose everything. Two weeks of running like this was two weeks of hoping AGAPITO2 stayed healthy.</p>
<h3>Before you touch hardware</h3>
<p>Before doing anything physical, I wanted to capture the current state. When things go wrong during maintenance, you want to be able to compare "before" and "after."</p>
<p>Three things to record while the server is still running:</p>
<p><strong>Pool status</strong>, the <code>zpool status</code> output above. You want to know exactly what ZFS thinks the world looks like right now.</p>
<p><strong>SATA layout</strong>, which drive is on which port:</p>
<pre><code>dmesg -T | grep -E 'ata[0-9]+\.[0-9]+: ATA-|ata[0-9]+: SATA link up'</code></pre>
<p>In my case, AGAPITO2 was on ata4 and ata3 was empty (the old drive's port). This matters because after you install the new drive, you want to confirm it shows up on the expected port.</p>
<p><strong>Surviving drive health</strong>, to make sure the drive you're depending on is actually healthy before you start:</p>
<pre><code>smartctl -H /dev/disk/by-id/ata-ST4000NT001-3M2101_WX11TN2P</code></pre>
<pre><code>SMART overall-health self-assessment test result: PASSED</code></pre>
<p>If this says anything other than <code>PASSED</code>, stop and deal with that first. You don't want to discover your only remaining copy of data is on a failing drive while you're in the middle of hardware work.</p>
<p>Once you've got your reference snapshots, shut down the server gracefully:</p>
<pre><code>shutdown now</code></pre>
<h3>Physical installation</h3>
<p>I won't write a hardware installation tutorial, every case and drive bay is different. But a few practical tips for homelabbers doing this for the first time:</p>
<ul>
<li><strong>Inspect your cables before connecting them.</strong> If the SATA data cable has been sitting disconnected in the case, check the connector pins. Bent pins or dust can cause exactly the kind of intermittent issues that started this whole saga.</li>
<li><strong>Label the new drive.</strong> I labeled mine "TOMMY" with its serial number (WX120LHQ) written on a sticker. Yes, I name my drives. It makes debugging much easier than squinting at serial numbers.</li>
<li><strong>Push connectors until they click.</strong> Both SATA data and power. Then do the wiggle test: grab the connector gently and try to move it. If it shifts at all, it's not fully seated.</li>
</ul>
<p>Seat the drive, connect both cables, close the case, and power on.</p>
<h3>Boot and verify detection</h3>
<p>First thing after boot: did the kernel see the new drive?</p>
<pre><code>dmesg -T | grep -E 'ata[0-9]+\.[0-9]+: ATA-|ata[0-9]+: SATA link up'</code></pre>
<pre><code>[Fri Feb 20 22:57:06 2026] ata3: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
[Fri Feb 20 22:57:06 2026] ata3.00: ATA-11: ST4000NT001-3M2101, EN01, max UDMA/133
[Fri Feb 20 22:57:07 2026] ata4: SATA link up 6.0 Gbps (SStatus 133 SControl 300)
[Fri Feb 20 22:57:07 2026] ata4.00: ATA-11: ST4000NT001-3M2101, EN01, max UDMA/133</code></pre>
<p>Both drives detected at full 6.0 Gbps: TOMMY on ata3, AGAPITO2 on ata4.</p>
<p>Next, verify it shows up with its expected serial in <code>/dev/disk/by-id/</code>:</p>
<pre><code>ls -l /dev/disk/by-id/ | grep WX120LHQ</code></pre>
<pre><code>ata-ST4000NT001-3M2101_WX120LHQ -> ../../sda</code></pre>
<p>And confirm identity with SMART:</p>
<pre><code>smartctl -i /dev/disk/by-id/ata-ST4000NT001-3M2101_WX120LHQ</code></pre>
<pre><code>Device Model: ST4000NT001-3M2101
Serial Number: WX120LHQ
Firmware Version: EN01
User Capacity: 4,000,787,030,016 bytes [4.00 TB]
SATA Version is: SATA 3.3, 6.0 Gb/s (current: 6.0 Gb/s)</code></pre>
<p>Correct model, serial, firmware, and running at full speed.</p>
<p>One more critical check: look for SATA errors in the kernel log.</p>
<pre><code>dmesg -T | grep -E 'ata[0-9]' | grep -iE 'error|fatal|reset|link down|slow|limiting'</code></pre>
<p>I saw <code>ata1: SATA link down</code> and <code>ata2: SATA link down</code>, which are just unused ports. Nothing on ata3 or ata4. If you see errors on the port your new drive is on, <strong>stop</strong>. A brand new drive throwing SATA errors on a known-good cable is likely dead on arrival.</p>
<h3>Health-check before trusting it</h3>
<p>A drive can be detected and still be dead on arrival. Before resilvering 1.3 terabytes of data onto it, I wanted to know it was actually healthy.</p>
<p><strong>SMART overall health:</strong></p>
<pre><code>smartctl -H /dev/disk/by-id/ata-ST4000NT001-3M2101_WX120LHQ</code></pre>
<pre><code>SMART overall-health self-assessment test result: PASSED</code></pre>
<p><strong>Baseline SMART attributes</strong>, the important ones to check on a new drive:</p>
<pre><code>smartctl -A /dev/disk/by-id/ata-ST4000NT001-3M2101_WX120LHQ | grep -E 'Reallocated|Pending|Offline_Uncorrect|CRC'</code></pre>
<pre><code> 5 Reallocated_Sector_Ct ... - 0
197 Current_Pending_Sector ... - 0
198 Offline_Uncorrectable ... - 0
199 UDMA_CRC_Error_Count ... - 0</code></pre>
<p>All zeros. Reallocated sectors would mean the drive has already had to remap bad spots. Pending sectors are blocks the drive suspects are bad but hasn't confirmed yet. CRC errors indicate data corruption during transfer. On a new or refurbished drive, all of these should be zero.</p>
<p><strong>Short self-test:</strong></p>
<pre><code>smartctl -t short /dev/disk/by-id/ata-ST4000NT001-3M2101_WX120LHQ
# Wait ~2 minutes...
smartctl -l selftest /dev/disk/by-id/ata-ST4000NT001-3M2101_WX120LHQ</code></pre>
<pre><code># 1 Short offline Completed without error 00% 0 -</code></pre>
<p>Passed with 0 power-on hours, a fresh drive. If any of these checks fail, don't proceed. Contact the seller and get another replacement.</p>
<h3>The replacement: <code>zpool replace</code></h3>
<p>This is the moment. One command:</p>
<pre><code>zpool replace proxmox-tank-1 ata-ST4000NT001-3M2101_WX11TN0Z ata-ST4000NT001-3M2101_WX120LHQ</code></pre>
<p>This tells ZFS "the drive identified as WX11TN0Z (currently <code>REMOVED</code>) is being replaced by WX120LHQ." ZFS starts resilvering immediately, copying all data from the surviving drive (AGAPITO2) onto the new one (TOMMY).</p>
<p>Checking status right after:</p>
<pre><code> pool: proxmox-tank-1
state: DEGRADED
scan: resilver in progress since Fri Feb 20 23:10:58 2026
config:
NAME STATE READ WRITE CKSUM
proxmox-tank-1 DEGRADED 0 0 0
mirror-0 DEGRADED 0 0 0
replacing-0 DEGRADED 0 0 0
ata-ST4000NT001-3M2101_WX11TN0Z REMOVED 0 0 0
ata-ST4000NT001-3M2101_WX120LHQ ONLINE 0 0 7.73K
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0</code></pre>
<p>Notice the <code>replacing-0</code> vdev. That's a temporary structure ZFS creates during the replacement, showing both the old (<code>REMOVED</code>) and new (<code>ONLINE</code>) drive while the resilver is in progress.</p>
<p>The 7.73K cksum count on the new drive might look alarming, but it's expected during a resilver. Those are blocks that haven't been written yet. ZFS is aware of them and they'll clear up as the resilver progresses.</p>
<p>I monitored progress with:</p>
<pre><code>watch -n 30 "zpool status -v proxmox-tank-1"</code></pre>
<p>I also kept <code>dmesg -Tw</code> running in another terminal, watching for any SATA errors. The kernel log stayed quiet the entire time.</p>
<p>In my case, the VMs had auto-started on boot, so the resilver was competing with production I/O. It completed in about 3.5 hours: 1.34 terabytes resilvered with 0 errors. Not bad for a pair of 4TB IronWolf drives running alongside active workloads.</p>
<h3>Post-resilver verification</h3>
<p>The resilver finished. Time to verify everything is actually good.</p>
<p><strong>Pool status:</strong></p>
<pre><code> pool: proxmox-tank-1
state: ONLINE
scan: resilvered 1.34T in 03:32:55 with 0 errors on Sat Feb 21 02:43:53 2026
config:
NAME STATE READ WRITE CKSUM
proxmox-tank-1 ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
ata-ST4000NT001-3M2101_WX120LHQ ONLINE 0 0 7.73K
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0
errors: No known data errors</code></pre>
<p><code>ONLINE</code>. The <code>replacing-0</code> vdev is gone and the mirror now has the new drive in place. The 7.73K cksum on TOMMY is a residual counter from the resilver, so let's clear it:</p>
<pre><code>zpool clear proxmox-tank-1</code></pre>
<p>Now for the real test. A resilver copies data to rebuild the mirror, but a <strong>scrub</strong> reads every block on the pool, verifies all checksums, and repairs any mismatches. This is the definitive integrity check:</p>
<pre><code>zpool scrub proxmox-tank-1</code></pre>
<p>This ran for about 3.5 hours across 1.34T of data:</p>
<pre><code> scan: scrub repaired 0B in 03:27:50 with 0 errors on Sat Feb 21 11:38:02 2026
NAME STATE READ WRITE CKSUM
proxmox-tank-1 ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
ata-ST4000NT001-3M2101_WX120LHQ ONLINE 0 0 0
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0
errors: No known data errors</code></pre>
<p>Zero bytes repaired, zero errors, both drives at 0/0/0. Clean.</p>
<p>One last thing: a post-I/O SMART check on the new drive. After hours of heavy writes during the resilver and reads during the scrub, any hardware weakness should have surfaced:</p>
<pre><code>smartctl -x /dev/disk/by-id/ata-ST4000NT001-3M2101_WX120LHQ | grep -E 'Reallocated|Pending|Offline_Uncorrect|CRC|Hardware Resets|COMRESET|Interface'</code></pre>
<pre><code>Reallocated_Sector_Ct ... 0
Current_Pending_Sector ... 0
Offline_Uncorrectable ... 0
UDMA_CRC_Error_Count ... 0
Number of Hardware Resets ... 2
Number of Interface CRC Errors ... 0
COMRESET ... 2</code></pre>
<p>All clean. The 2 hardware resets and 2 COMRESETs are just from the server booting, perfectly normal.</p>
<h3>The commands, all in one place</h3>
<p>For future me and anyone else replacing a disk in a ZFS mirror:</p>
<pre><code># --- Before shutdown ---
# Record pool status
zpool status -v &lt;pool&gt;
# Record SATA layout
dmesg -T | grep -E 'ata[0-9]+\.[0-9]+: ATA-|ata[0-9]+: SATA link up'
# Check surviving drive health
smartctl -H /dev/disk/by-id/&lt;surviving-disk-id&gt;
# Shut down
shutdown now
# --- After boot with new drive ---
# Verify detection
dmesg -T | grep -E 'ata[0-9]+\.[0-9]+: ATA-|ata[0-9]+: SATA link up'
ls -l /dev/disk/by-id/ | grep &lt;new-serial&gt;
smartctl -i /dev/disk/by-id/&lt;new-disk-id&gt;
# Check for SATA errors
dmesg -T | grep -E 'ata[0-9]' | grep -iE 'error|fatal|reset|link down'
# Health-check the new drive
smartctl -H /dev/disk/by-id/&lt;new-disk-id&gt;
smartctl -A /dev/disk/by-id/&lt;new-disk-id&gt; | grep -E 'Reallocated|Pending|Offline_Uncorrect|CRC'
smartctl -t short /dev/disk/by-id/&lt;new-disk-id&gt;
smartctl -l selftest /dev/disk/by-id/&lt;new-disk-id&gt;
# --- Replace and resilver ---
# Replace old drive with new
zpool replace &lt;pool&gt; &lt;old-disk-id&gt; &lt;new-disk-id&gt;
# Monitor resilver progress
watch -n 30 "zpool status -v &lt;pool&gt;"
# Watch kernel log for SATA errors during resilver
dmesg -Tw
# --- Post-resilver verification ---
# Check final status
zpool status -v &lt;pool&gt;
# Clear residual cksum counters
zpool clear &lt;pool&gt;
# Run a full scrub
zpool scrub &lt;pool&gt;
# Post-I/O SMART check
smartctl -x /dev/disk/by-id/&lt;new-disk-id&gt; | grep -E 'Reallocated|Pending|Offline_Uncorrect|CRC'</code></pre>
<p>The mirror degradation that started on February 8th is resolved. Two weeks of running on a single drive, an RMA, and one evening of work later, the pool is whole again. Full redundancy restored, zero data lost throughout the entire saga. ZFS did exactly what it was designed to do.</p>
<p><em>This is the fourth and final article in this series. If you're just arriving, start with <a href="why-i-put-my-vms-on-a-zfs-mirror.html">Part 1: Why I Put My VMs on a ZFS Mirror</a>, then <a href="a-degraded-pool-with-a-healthy-disk.html">Part 2: A Degraded Pool with a Healthy Disk</a>, and <a href="fixing-a-degraded-zfs-mirror.html">Part 3: Fixing a Degraded ZFS Mirror</a>.</em></p>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -1,42 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<hr>
<section>
<h2>Tech fascination as a function of your age</h2>
<p><em>Published: 2026-05-11</em></p>
<p>A long time ago I read a quote that I find hilariously accurate. At first it stuck with me because it was
funny. Then as time passed, I observed it was actually very accurate. I mistakenly thought the quote was
commonly known in the world, and today I realised apparently it isn't, and I can't find it posted
anywhere. So I'm storing it here for the future. The quote roughly looked like this:</p>
<blockquote>
<p>Any technology that existed before you turned 18 is just boring, regular old stuff that has always
been there since the world was born and doesn't matter much.</p>
<p>Any technology that appears while you are between 18 and 30 years old is a revolution like the world
has never seen before, will change the universe in ways mankind can't even fathom and deserves your
entire devotion.</p>
<p>Any technology that appears after you've hit 30 is black magic and heresy, and you will never truly
understand it.</p>
</blockquote>
<hr>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>The ROI of toilets</h2> <h2>The ROI of toilets</h2>
<p><em>Published: 2025-02-19</em></p>
<p>Years ago I worked under the organizational umbrella of this COO. He was my boss' boss. Sometimes we <p>Years ago I worked under the organizational umbrella of this COO. He was my boss' boss. Sometimes we
bumped into each other for big meetings and presentations.</p> bumped into each other for big meetings and presentations.</p>
<p>The COO had a background in finance and audit, which gave him certain management quirks that coupled in <p>The COO had a background in finance and audit, which gave him certain management quirks that coupled in

View file

@ -1,148 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8" />
<meta viewport="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="../styles.css" />
</head>
<body>
<main>
<h1>Hi, Pablo here</h1>
<p><a href="../index.html">back to home</a></p>
<hr />
<section>
<h2>Thoughts after departing Galoy</h2>
<p><em>Published: 2026-08-30</em></p>
<p>
I recently left my position as a Data Engineer at Galoy to pursue some other opportunities. I thought it would
be a good time to reflect a bit and jot down some notes for my future self. I did that for the first time when
leaving Superhog and it proved useful, so here it goes.
</p>
<ul>
<li>
Having a technical CEO saves you from many pains that a non-technical CEO will bring to your door. But it
will introduce other ones. It is definitely not heaven on earth compared to non-technical CEOs, but rather a
different set of trade-offs. If you care about being surrounded by a culture of great engineering, though, a
technical CEO really helps.
</li>
<li>
The team stayed away from anything that resembled a production environment for my full tenure. I could see how
it rots away the spirit. There are issues with having a bunch of engineers crank code against a future
hypothetical usage that never comes. Eventually, care degrades. You do not build things the same way when the
quality bar is being able to call the task done, even with very good CI and a good culture of testing, as you
do when your stuff will actually be used in the real world. Operations also take a hit: when you can always
reset any environment, because you do not live under the terrible threat of at least one true production
environment that needs to be treated like a jewel, you tolerate many deployment and operation quirks. Finally,
without clear guidance provided by business needs, the team will sometimes deviate into funky technical nerd
snipes that, after weeks or months, might end up providing nothing useful to the product or business.
</li>
<li>
Working with a globally distributed remote team can be a challenge. You can get the feeling that things slow
down dramatically because communication sometimes ends up working with one working day of latency: you ask
your coworker, who is sleeping, something; then he replies back when you are AFK. Now I can see why some
companies make it strict to only hire people within certain timezone boundaries. Also, executing an offsite is
a logistics nightmare. No matter which place you pick to meet, someone will have visa nightmares to get there.
</li>
<li>
It is insanely productive to have a strong CI setup and to heavily automate all things related to builds,
testing, deployment, and so on. I was a bit hesitant about going all the way with certain automations, but this
team really showed me tricks and patterns to take CI to the limit and I loved it.
</li>
<li>
Compiling Rust is a pain in the ass.
</li>
<li>
I have seen many places where you always have one engineer owning one task, and the idea of putting two people
under the same task would be considered unproductive. In Galoy, it was quite the opposite: having multiple
engineers on any non-trivial task was considered to be faster in overall team velocity, because multiple
people being hands-on with the same task would speed up design and review. I fell in love with the concept,
and now I find a lot of value in mobbing. It is also great for knowledge sharing. Galoy implemented some rules
to motivate this, like limiting PRs in flight and having the convention for engineers to always try to join a
WIP task instead of getting a brand new one started.
</li>
<li>
Incident reviews were always done collectively: whoever might have tackled the incident started a few notes,
but there were large chunks of the report that would be filled in collectively before the review meeting. Then
many details would be added during the meeting itself. The bit I found most interesting about this shared
ownership of the review was that, during the meeting, each related engineer was expected to walk everyone else
through their own subjective timeline of events, uninterrupted. This practice, as opposed to someone playing
detective and trying to craft one global sequence of events, was really helpful in making evident the
misalignments in how different colleagues collaborated together.
</li>
<li>
If you are the kind of person who likes to get shit done, traditional banking is a terrible place to be. It is
the most regulated, risk-averse environment I have ever lived in. Innovating there is almost impossible. This
is the reason you see fintechs do cool stuff and banks not: the fintech is working outside of financial
regulations, and only touching the industry in the most minimal way possible.
</li>
<li>
Another related thought: running a tradfi bank is so utterly complex due to regulation that bootstrapping one
from scratch somehow feels impossible. You are going to need double-digit millions just to get ready for
licensing and launch, with no guarantee that you will actually take off successfully. It feels like a system
made to ensure no new players enter, which is obviously in the interest of both current players and regulators.
One specifically tough part here is that licensing is a bit all or nothing: if you want to start the bank, you
must prove to the regulator that you can properly operate up to his standards everything the license offers,
even if you only intend to offer a very limited set of products. You cannot get an MVP license: you are either
a bank or not, and you cannot lean-launch yourself into existence. It reminds me of the famous John Gall quote:
<em>A complex system that works is invariably found to have evolved from a simple system that worked.</em>
Here, regulators have doomed startups by forcing everyone to somehow miraculously start being complex out the
door.
</li>
<li>
I got the chance to visit El Salvador. It is a really poor country, but at the time of my visit the spirits
were high because things had been trending upwards for years already. The environment created by such a winning
streak was awesome. I found many people were very happy because, even if they were still in deep shit, things
had been improving significantly for them for quite some time, and they expected more good things to come.
</li>
<li>
I have doubted in the past about whether scheduled 1:1s, retrospectives, and performance reviews are good or
not. The good old debate between "schedule it and do it with some frequency" versus "let us just raise the
conversation when needed". After being in Galoy, I have finally come to the personal conclusion that it pays
off to have those meetings scheduled on a recurrent basis. If you feel you have nothing to say, just schedule
them less frequently.
</li>
<li>
Event sourcing is a beautiful pattern. It comes with its own complexity, but if you are in a system with true
playback and audit needs, nothing beats it. The only time I found it can give you a headache is when you deal
with requests to delete sensitive personal data.
</li>
<li>
As you try to scale throughput, strong consistency increasingly becomes an obstacle. I now understand why so
many other systems I dealt with in the past force you through async "fill a request and poll the state"
patterns: they are shoving you into an async background queue to take weight away from the overloaded main
server.
</li>
<li>
Having all the tools you usually interact with when working, both development and operations related, exposing a
CLI is awesome. The ability to mix and match information from different spots in a single terminal is very
productive. Both as a human, but also when working with LLM agents.
</li>
<li>
Testing with BATS is great. I now realize there are so many other projects that could have had great testing
if they had stepped out one more layer of the onion and run their tests at the terminal layer.
</li>
<li>
Having things that you can check in CI but not locally inevitably leads to headaches. It pays off to make all
the CI tasks runnable locally. Instead of relying on complicated multistep workflows in the CI YAMLs, perhaps
just try to have one bash script per task that runs the same locally and in the CI VM.
</li>
<li>
If you are trying to sell critical products or services to big corps, in the sense that their business will
strongly depend on what you are providing, having weak financials is a serious liability. They may love your
proposal, but they may also get really scared from the fact that it looks like you are going bankrupt in a few
months.
</li>
</ul>
<hr />
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>When new is not better</h2> <h2>When new is not better</h2>
<p><em>Published: 2025-01-19</em></p>
<p> <p>
One of the hobbies that has stuck with me for decades is photography. I've shot a few hundreds of One of the hobbies that has stuck with me for decades is photography. I've shot a few hundreds of
film rolls, and some non-recorded amount of digital pics. film rolls, and some non-recorded amount of digital pics.

View file

@ -1,121 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Pablo here</title>
<meta charset="utf-8">
<meta viewport="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<main>
<h1>
Hi, Pablo here
</h1>
<p><a href="../index.html">back to home</a></p>
<section>
<h2>Why I Put My VMs on a ZFS Mirror</h2>
<p><em>Published: 2026-01-04</em></p>
<p><em>Part 1 of 3 in my "First ZFS Degradation" series. Also read <a href="a-degraded-pool-with-a-healthy-disk.html">Part 2: Diagnosing the Problem</a> and <a href="fixing-a-degraded-zfs-mirror.html">Part 3: The Fix</a>.</em></p>
<h3>Why This Series Exists</h3>
<p>A few weeks into running my new homelab server, I stumbled upon something I wasn't expecting to see that early: my ZFS pool was in "DEGRADED" state. One of my two mirrored drives had gone FAULTED.</p>
<p>This was the first machine I had set up with a ZFS mirror, precisely to be able to deal with disk issues smoothly, without losing data and having downtime. Although it felt like a pain in the ass to spot the problem, I was also happy because it gave me a chance to drill the kind of disk maintenance I was hoping to do in this new server.</p>
<p>But here's the thing: when I was in the middle of it, I couldn't find a single resource that walked through the whole experience in detail. Plenty of docs explain what ZFS <em>is</em>. Plenty of forum posts have people asking "help my pool is degraded." But nothing that said "here's what it actually feels like to go through this, step by step, with all the commands and logs and reasoning behind the decisions."</p>
<p>So I wrote it down. I took a lot of notes during the process and crafted a more or less organized story from them. This three-part series is for fellow amateur homelabbers who are curious about ZFS, maybe a little intimidated by it, and want to know what happens when things go sideways. I wish I had found a very detailed log like this when I was researching ZFS initially. Hope it helps you.</p>
<h3>The server and disks</h3>
<p>My homelab server is a modest but capable box I built in late 2025. It has decent consumer hardware, but nothing remarkable. I'll only specify that I have currently three disks on it:</p>
<ul>
<li><strong>OS Drive</strong>: Kingston KC3000 512GB NVMe. Proxmox lives here.</li>
<li><strong>Data Drives</strong>: Two Seagate IronWolf Pro 4TB drives (ST4000NT001). This is where my Proxmox VMs get their disks stored.</li>
</ul>
<p>The two IronWolf drives are where this story takes place. I labeled them AGAPITO1 and AGAPITO2 because... well, every pair of drives deserves a silly name. I have issues remembering serial numbers.</p>
<p>The server runs Proxmox and hosts most of my self-hosted life: personal services, testing VMs, and my Bitcoin infrastructure (which I share over at <a href="https://bitcoininfra.contrapeso.xyz" target="_blank" rel="noopener noreferrer">bitcoininfra.contrapeso.xyz</a>). If this pool goes down, everything goes down.</p>
<h3>Why ZFS?</h3>
<p>I'll be honest: I didn't overthink this decision. ZFS is the default storage recommendation for Proxmox, it has a reputation for being rock-solid, and I'd heard enough horror stories about silent data corruption to want something with checksumming built in.</p>
<p>What I was most interested in was the ability to define RAID setups in software and deal easily with disks going in and out of them. I had never gone beyond the naive "one disk for the OS, one disk for data" setup in previous servers. After having disks failing on me in previous boxes, I decided it was time to gear up and do it proper this time. My main concern initially was just saving time: it's messy when a "simple" host has disk issues, and I hoped mirroring would allow me to invest less time in cleaning up disasters.</p>
<h3>Why a Mirror?</h3>
<p>When I set up the pool, I had two 4TB drives. That gave me a few options:</p>
<ol>
<li><strong>Single disk</strong>: Maximum space (8TB usable), zero redundancy. One bad sector and you're crying.</li>
<li><strong>Mirror</strong>: Half the space (4TB usable from 8TB raw), but everything is written to both drives. One drive can completely die and you lose nothing.</li>
<li><strong>RAIDZ</strong>: Needs at least 3 drives, gives you parity-based redundancy. More space-efficient than mirrors at scale.</li>
</ol>
<p>I went with the mirror for a few reasons.</p>
<p>First, I only had two drives to start with, so RAIDZ wasn't even an option yet.</p>
<p>Second, mirrors are <em>simple</em>. Data goes to both drives. If one dies, the other has everything. No parity calculations, no write penalties, no complexity.</p>
<p>Third (and this is the one that sold me), <strong>mirrors let you expand incrementally</strong>. With ZFS, you can add more mirror pairs (called "vdevs") to your pool later. You can even mix sizes: start with two 4TB drives, add two 8TB drives later, and ZFS will use all of it. RAIDZ doesn't give you that flexibility; once you set your vdev width, you're stuck with it.</p>
<h4>When Would RAIDZ Make More Sense?</h4>
<p>If you're starting with 4+ drives and you want to maximize usable space, RAIDZ starts looking attractive:</p>
<table>
<thead>
<tr>
<th>Configuration</th>
<th>Drives</th>
<th>Usable Space</th>
<th>Fault Tolerance</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mirror</td>
<td>2</td>
<td>50%</td>
<td>1 drive</td>
</tr>
<tr>
<td>RAIDZ1</td>
<td>3</td>
<td>~67%</td>
<td>1 drive</td>
</tr>
<tr>
<td>RAIDZ1</td>
<td>4</td>
<td>75%</td>
<td>1 drive</td>
</tr>
<tr>
<td>RAIDZ2</td>
<td>4</td>
<td>50%</td>
<td>2 drives</td>
</tr>
<tr>
<td>RAIDZ2</td>
<td>6</td>
<td>~67%</td>
<td>2 drives</td>
</tr>
</tbody>
</table>
<p>RAIDZ2 is popular for larger arrays because it can survive <em>two</em> drive failures, which matters more as you add drives (more drives = higher chance of one failing during a resilver).</p>
<p>But for a two-drive homelab that might grow to four drives someday, I felt a mirror was the right call. I can always add another mirror pair later.</p>
<h3>The Pool: proxmox-tank-1</h3>
<p>My ZFS pool is called <code>proxmox-tank-1</code>. Here's what it looks like when everything is healthy:</p>
<pre><code> pool: proxmox-tank-1
state: ONLINE
config:
NAME STATE READ WRITE CKSUM
proxmox-tank-1 ONLINE 0 0 0
mirror-0 ONLINE 0 0 0
ata-ST4000NT001-3M2101_WX11TN0Z ONLINE 0 0 0
ata-ST4000NT001-3M2101_WX11TN2P ONLINE 0 0 0</code></pre>
<p>That's it. One pool, one mirror vdev, two drives. The drives are identified by their serial numbers (the <code>WX11TN0Z</code> and <code>WX11TN2P</code> parts), which is important — ZFS uses stable identifiers so it doesn't get confused if Linux decides to shuffle around <code>/dev/sda</code> and <code>/dev/sdb</code>.</p>
<p>All my Proxmox VMs store their virtual disks on this pool. When I create a new VM, I point its storage at <code>proxmox-tank-1</code> and ZFS handles the rest.</p>
<h3>What Could Possibly Go Wrong?</h3>
<p>Everything was humming along nicely. VMs were running fine and I was feeling pretty good about my setup.</p>
<p>Then, a few weeks in, I was poking around the Proxmox web UI and noticed something that caught my eye.</p>
<p>The ZFS pool was DEGRADED. One of my drives — AGAPITO1, serial <code>WX11TN0Z</code> — was FAULTED.</p>
<p>In <a href="a-degraded-pool-with-a-healthy-disk.html">Part 2</a>, I'll walk through how I diagnosed what was actually wrong. Spoiler: the drive itself was fine. The problem was much dumber than that.</p>
<p><em>Continue to <a href="a-degraded-pool-with-a-healthy-disk.html">Part 2: Diagnosing the Problem</a></em></p>
<p><a href="../index.html">back to home</a></p>
</section>
</main>
</body>
</html>

View file

@ -18,7 +18,6 @@
<hr> <hr>
<section> <section>
<h2>Your customers don't care that your bathroom is dirty</h2> <h2>Your customers don't care that your bathroom is dirty</h2>
<p><em>Published: 2025-02-09</em></p>
<p>The other night I went out with the missus and we went to a fancy pants restaurants, which is unusual for <p>The other night I went out with the missus and we went to a fancy pants restaurants, which is unusual for
us. We prefer neighbourhood, simple places.</p> us. We prefer neighbourhood, simple places.</p>
<p> <p>

View file

@ -1,29 +0,0 @@
Throughout most of my career, the standard choice of tooling to run local dev stacks has been Docker/Podman. You make a compose file and conveniently start/stop services as needed.
It surely works. But lately I've been slightly bothered by it. Many times I develop simple services that don't go through convoluted containerization and deployment patterns, such as Docker images + Helm + Kubernets. Instead, they simply end up as regular processes in a Debian box (typically a VM). What bothers me is that this made me end up with two quite different setups: I would use Docker for local dev, then bare metal deploys in production. This is annoying because you have relevant differences in how things run in each environment, which leaves you with no way to test locally certain deployment quirks.
You could thing for yourself: sure, but you can have a staging/testflight environment where you tests deploys as closely as possible as in production. That is certainly and option (and what I usually do nowadays), but why not do it already locally?
To scratch my itch, I decided to experiment with a toy project I was fooling around. I decided I would come up with a way to run it locally with a fully isolated VM instead of the existint docker compose driven approach.
## Tooling choice
I decided to use KVM + QEMU + libvirt + inqus
KVM (Kernel Virtual Machine) is a Linux kernel module. It's job is to expose CPU extensions to userspace, which allows the VMs you start to run CPU commands directly in the CPU, and not in an emulated way. Technically, you can run this stack without KVM, but then all CPU workload will be emulated and take many orders of magnitude longer to run, which sucks. One constraint to notice is that you can only leverage KVM if the VMs that you run have the same chip architecture as the host. Obviously, this is because you can't send, for instance, ARM operations to a RISC-V processor. QEMU can run (much slower) without KVM, and can also emulate other CPU architectures without it.
QEMU (Quick EMUlator) is the actual Virtual Machine emulator. Technically, the VM running on your computer is a QEMU process. The process is taking care of simulating all of the physical components of the VM (CPU, RAM, disk, etc). QEMU holds the VM memory in the host RAM memory, handles CPU commands being passed to the host CPU (or emulates them itself if no KVM), and maps a virtual disk to actual disk space in one of the hosts disks. Overall, what QEMU does is to create virtual interfaces that emulate the real interfaces offered by the parts of a computer and then, behind the curtains, map those interfaces to backend processes that either do the thing on the host (writing to a QEMU disk will actually write to the host disk) or on a completely in-process simulation of the part (BIOS/UEFI firmware).
Now, to start a QEMU VM you need some initial state. Your options resemble pretty much the same you would face when working with a real computer.
Your first option is to grab an OS installer image and install the OS from scratch. This works just like it would if you were setting up a real computer. QEMU can emulate a CD/DVD reader and pretend the ISO is being read from it. This is an option, but rather painful if you plan on frequently recreating VMs. And it rarely gets used because there is a much better alternative.
But if you had a computer, you could get up to speed much faster if you already had a drive with an OS installed and ready to run, and you simply plugged it to your motherboard and told it to boot from there, right? The same approach is what gets used the most when running QEMU VMs. There is this format called `qcow2`, which is basically a byte-per-byte clone of the disk of a VM. Large providers like Canonical will ship ready to use `qcow2`, which are pretty much the state of the OS after having been installed but before having done a first boot. Combined with `cloud-init`, these images let you spin a ready to use + minimally configured to your needs VMs fast without having to go through the OS install.
You can also build your own `qcow2` images from a running VM, although making generic images that are useful in many situations has certain complexity. It is nevertheless a good option for backups or for some spots where you need to start a VM in a reproducible state repeteadly. And, if you're willing to learn how to do it, you can actually also build your own custom (as in, with whatever setup you need) yet generic (as in, stripped of host specific details that would give you headaches if you spin it repeteadly) images.
Now, you can use QEMU directly in a standalone way, but the interfaces are rather raw. QEMU focuses heavily on doing its job (running a VM) well, and doesn't offer convenient tooling to make managing many VMs or automating VM management ergonomic.
To solve this, we add Incus to the stack. Incus is a management tool for Virtual Machines. It doesn't actually run the machines, that's QEMU's job. Incus runs a daemon on the host which can be interacted with via CLI.