Digital Currency · Distributed Systems · Open Source
A Global Currency. Five icons: peer-to-peer, decentralized, distributed, value from global economic metrics, deflationary. A chain of blocks winds around a glowing globe at sunset. On the desk, a notebook page headed The Idea lists the same six points: global currency, peer-to-peer, decentralized, distributed, value from global economic metrics, deflationary.
1995 · THE SEED  →  2002 · FIRST CODE  →  24 YEARS OF DETOURS & DISTRACTIONS  →  TODAY · A DOCUMENTARY

The 24-Year Journey to Speso:
A Currency Priced by the World Economy

📅 October 2026· 🔗 github.com/niyid/speso· ☕ Java 17+ · zero dependencies· ⏱ 16 min read

So back in April 2002, I had this idea of a global currency. Peer-to-peer. Decentralized. Distributed. The value deriving from global economic metrics. I did not give it a name, but I immediately identified the most complex issue I had to solve: double-spend. And I identified the overall data structure: a linked list of sorts. Speso is a recent development, even though at the time I had an interest in Esperanto. Somewhere along the line, distractions and detours came along, and after several pitstops and a grand total of 24 years from when I wrote the first code (the initial idea was in 1995, but I wrote no code), I finally conclude.

Now it is more of a documentary than a threat to the petro-dollar, and me being identified as a threat to the United States and thereby drawing attention from various goon-squad types.

April 2002

Six Lines in a Notebook

The seed of the idea goes back to 1995, but I wrote no code then. April 2002 is when it became concrete, and when I started writing code. The notebook in the banner above shows how I listed it: six points. Global currency. Peer-to-peer. Decentralized. Distributed. Deflationary. Value from global economic metrics. Then the problem I saw first: double-spend. And the shape of the data: boxes joined by arrows, with a question mark: a linked list, of sorts?

So back in April 2002, I had this idea of a Global currency. Peer-to-peer. Decentralized. Distributed. The value deriving from global economic metrics. I did not give it a name but I immediately identified the most complex issue I had to solve — double-spend. And I identified the overall data structure — a linked-list of sorts.

— The author, on the origin of the idea (the initial idea was in 1995; no code until 2002)

Notice what is in that list and what is not. Nothing about mining. Nothing about tokens or fundraising. The two ideas that matter are the ones everybody forgets when they say “cryptocurrency”: a money with no issuing authority, and a money whose value is anchored to something real instead of to a promise. Both are hard. The first needs a solution to double-spend. The second needs somebody to tell the chain what the world economy is doing, and that turns out to be the harder of the two. Most of this article is about that.

1995

The initial idea. No code is written.

APRIL 2002

The idea gets concrete: a list of bullet points (global, peer-to-peer, decentralized, distributed, deflationary, value from economic metrics), one named problem (double-spend), one data structure (a linked list of sorts). The first code is written. Still no name.

2002 → 2026

Distractions and detours. Several pitstops. The idea never stops being the one I come back to. Twenty-four years are counted from that first code.

2026

The name arrives: Speso, a nod to the Esperanto I was curious about then. The code is finished as plain Java, and the idea finally becomes a documentary of how it works.

The Name

Why “Speso”, and Why an Integer

Speso is the Esperanto word for a spending, an expenditure. The word came much later than the idea, but it fit for a second reason: in 1907 René de Saussure (brother of the linguist, and an Esperantist) proposed a monetary unit for the world called the spesmilo, symbol ₷, pinned to 0.733 grams of gold. In his scheme the speso was the tiny base unit, “purposely made very small to avoid fractions”, and the spesmilo was 1,000 spesoj.

That is exactly what a ledger wants. Floating-point money is a classic source of bugs, so the chain stores integer spesoj only, and people deal in ₷ with up to three decimals. Saussure pinned his spesmilo to gold. This one is pinned to the world.

1 ₷= 1,000 spesoj
16main source files
3,105lines of main code
0dependencies
Value

The Score: Pricing a Coin With the Planet

Every block carries five economic indicators, each stored as an integer number of hundredths of a percent: world GDP growth, global inflation, unemployment, trade growth and a financial stress index. From those five numbers the chain computes a single integer, the score. 10,000 means 100.00 and is the baseline.

value of one ₷, in Global Basket Units (GBU)
1 ₷ = score / 10,000 GBU

A healthy world makes a spesmilo worth more than 1 GBU, and a sick one less. The arithmetic is integer-only on purpose: every node on the network must compute exactly the same score from the same inputs, or the chain forks. This is the entire function:

EconomyIndex.javaJava
static long score(long[] i) { long s = SCORE_BASE; s += 8 * (i[0] - BASELINE[0]); // +1 pt GDP growth => +8.00% s -= 4 * Math.abs(i[1] - BASELINE[1]); // 1 pt off inflation target => -4.00% s -= 3 * (i[2] - BASELINE[2]); // +1 pt unemployment => -3.00% s += 2 * (i[3] - BASELINE[3]); // +1 pt trade growth => +2.00% s -= (i[4] - BASELINE[4]) / 2; // +10 stress points => -5.00% return Math.max(SCORE_MIN, Math.min(SCORE_MAX, s)); }

One point of GDP growth is worth +8%; a point of unemployment costs 3%; inflation is penalised in both directions around a 2% target. The weights are placeholders, and I say so in the source. They are the part you would argue about for years, and that is the point: the protocol decides how the economy is turned into a price, and the community decides the weights.

🧮
The price cannot lurch

A block may move the score by at most 2% from the previous block. A reading that would move it further is clamped, step by step, until it is legal. One bad report cannot teleport the price; at worst it drags it, and everyone watching sees it happen.

The Big Problem

Double-Spend: The Problem I Wrote Down in 2002

Digital cash is just data, and data can be copied. If I can send you the same ₷ 10 and also send it to someone else, the money is worthless. With no bank in the middle to say which payment came first, the network itself has to agree on an order. The linked list from my doodle is how: blocks of transactions, each carrying the hash of its parent, so that changing any old block breaks every block after it.

Block.java · wire formatSpec
index#prevHash#time#bits#nonce#miner#ind,ind,...#tx;tx;... tx = fromPub|kind|to|amount|aux|fee|seq|data|sig

The five defences in the code, in the order they bite:

Double-spend defences
1
Sequence numbers

Each account's transactions are numbered 0, 1, 2… Only the next number is valid. Two payments cannot share one, a replay is dead, and the signature covers the number.

2
Balance checked at the point of application

Transactions are applied in order against the account state; an overspend fails exactly where it occurs.

3
Mempool: first seen wins

Pending transactions are validated against confirmed state plus everything already pending. The second of two conflicting payments is rejected on arrival.

4
Blocks are verified by replay

A block is accepted only if replaying its transactions on a copy of the state succeeds and produces the numbers the block claims.

5
Fork choice by cumulative work

Not the longest chain, the chain with the most proof-of-work. To rewrite a confirmed payment you must out-work the rest of the network. On a reorg, still-valid transactions from abandoned blocks go back to the mempool.

Ledger.java · applying one transactionJava
if (!t.verify()) return false; String from = t.from(); if (t.seq != nextSeq(from)) return false; // replay / double-spend / gap long cost; switch (t.kind) { case Transaction.PAY -> cost = t.amount; case Transaction.PAY_GBU -> { cost = Params.mulDivCeil(t.amount, EconomyIndex.SCORE_BASE, rateScore); if (cost > t.aux) return false; // rate moved past sender's limit } case Transaction.REPORT, Transaction.ATTEST, Transaction.QUOTE -> cost = 0; default -> { return false; } } long total = cost + t.fee;

Defence 1 is the line if (t.seq != nextSeq(from)) return false. It is the whole idea in one comparison, and it is what the 2002 notebook was reaching for. The test suite attacks it directly: a conflicting payment with the same sequence number, an exact replay, an overspend with the next number, a block that reuses a sequence number, and a block that spends 80 from a balance of 50. All are refused.

The Oracle

The Hard Part: Who Tells the Chain What the World Is Doing?

Double-spend is a problem the network can solve for itself, because everything it needs is on the chain. Pricing the coin from the world economy is different: GDP growth is a fact about the outside world, and no blockchain can verify a fact about the outside world. All it can do is decide whose claim to believe.

My first version let any holder publish a signed report of the five indicators, and the chain took the stake-weighted median. A block with any other numbers was invalid, so miners no longer chose the price. I wrote down the weakness honestly: the oracle proves agreement, not truth. Anyone holding a majority of the reporting stake can set the number. That sentence was the first item on my list to fix.

The fix: two keys

The change is a rule that says neither group may set the number alone. Key 1 is a fixed set of named publishers who sign attestations of the data; the candidate value is their median, and it only counts if enough of them agree (a strict majority by default). Key 2 is the stake: holders still publish reports, but now their median can only ratify or veto the publishers' value. The price moves when both keys agree, and otherwise it freezes. A veto is not free: a stake report that lands in a block and clearly contradicts the publishers' value burns 2% of the reporter's balance, and when the publishers have no consensus nobody is slashed, so a stake majority can never punish the minority.

Ledger.java · the oracle's verdictJava
// Key 1: publishers. The candidate value is their median, but only if k of n agree with it. List<long[]> pubs = new ArrayList<>(); for (Report r : attests.values()) pubs.add(r.ind); o.pubLive = pubs.size(); long[] candidate = null; if (!pubs.isEmpty() && pubs.size() >= Params.PUBLISHER_THRESHOLD) { long[] med = plainMedian(pubs); for (long[] p : pubs) if (EconomyIndex.agrees(p, med)) o.pubAgree++; if (o.pubAgree >= Params.PUBLISHER_THRESHOLD) candidate = med; } if (candidate == null) { o.why = pubs.size() < Params.PUBLISHER_THRESHOLD ? "publishers below quorum" : "publishers disagree"; return o; } if (!stakeQuorum) { o.why = "no stake quorum to ratify"; return o; } if (!EconomyIndex.agrees(stakeMedian(stake, total), candidate)) { o.why = "stake vetoes the publishers"; return o; } o.quorum = true; o.median = candidate; for (Map.Entry<String, Long> e : stake.entrySet()) if (EconomyIndex.agrees(reports.get(e.getKey()).ind, candidate)) { o.agree.put(e.getKey(), e.getValue()); o.agreeTotal += e.getValue(); } return o;
Attacker
Before
After
Tested?
Stake majority
Sets any number
Can only freeze (veto), at 2% per contradicting reporter
Yes: no publisher quorum, and a contradicting majority (including the slash)
Publisher quorum
Did not exist
Can only freeze (no ratification)
Yes
1 publisher + 100% stake
Sets any number
Nothing (below quorum)
Yes
Publishers split
n/a
Score frozen
Yes
Publisher quorum and stake majority collude
Sets any number
Sets any number
This is the residual trust

Two design decisions are worth naming. First, it fails closed: with no publishers configured, no attestation can ever be valid and the score stays frozen at baseline. It never quietly falls back to the old stake-only oracle. Second, publishers are a role, not a bankroll: their transactions may carry a zero fee, so running a publisher costs no spesmiloj. Only the fixed publisher addresses can send them, and the list is hashed into the genesis block, so a node with a different list is on a different network and simply will not sync.

⚖️
This moves trust; it does not remove it

I want to be plain about this. The two-key rule means one party can no longer set the price. It does not make the price true. The number is now exactly as honest as the publishers, who are named parties chosen at genesis. If a publisher quorum and a stake majority collude, they set the number. Publishers should read independent sources: several publishers copying the same one are a single point of failure with extra steps.

One more detail from the tests, because it shows why stake is a living thing. When I wrote the “stake majority cannot override attested data” test, it failed. The ledger was right and my test was wrong: the two holders who disagreed with the majority were the ones mining the test blocks, and their rewards grew them into the majority. Stake is dynamic. The fix was to have a non-reporting wallet mine instead. A good sign for the rule, and a reminder to read the failure before blaming the code.

Making the Value Mean Something

Steering, Not Guaranteeing

A score on its own is only a number. Three mechanisms make it matter, and one thing is honestly out of reach.

Deflationary supply

Target supply is 21,000,000 ₷ × 100 / score, and never above 21,000,000 ₷. Below target, the block reward (max ₷ 50) is minted, never past the target. Above it nothing is minted and all fees are burned. A healthy world tightens supply; a sick one lowers the value of a ₷ instead of inflating the supply.

Peg feedback

Publishers can also sign a market quote. When a quorum agree within 5%, a market price below the peg shrinks the supply target and one above it can only lift it back toward 21,000,000 ₷, clamped to 50%–150%.

GBU-denominated payments

Sign “pay X GBU, at most M spesoj”. The chain converts at the previous block's rate, so a merchant can price in GBU and always receive the right number of ₷. This is the layer that lets the index be used.

The limit

None of this can make a market outside the chain pay the peg price. The protocol steers; it cannot compel.

Params.java · the market corrects the targetJava
static long adjustedTarget(long score, long market) { long t = supplyTarget(score); if (market <= 0) return t; long pct = Math.max(PEG_ADJ_MIN_PCT, Math.min(PEG_ADJ_MAX_PCT, mulDiv(market, 100, score))); long adj = mulDiv(t, pct, 100); return ELASTIC ? adj : Math.min(adj, SUPPLY_BASE); // never past the base }

The honest limit is the second item on my original list: the supply policy and GBU settlement steer the value but cannot force a market to honour it. No software can. A shop in another currency's economy will pay whatever it likes for a coin. What the code can do is make the protocol react: when the market values ₷ below the peg, the chain stops minting and burns fees, which is the only lever it has. It is feedback, not enforcement, and contraction is one-sided and slow: it never confiscates a balance. The CLI says so too: rate prints a “protocol reference” and reminds you that what a market pays may differ.

Deflation: what it is, and how far it goes

Deflation was in the idea from the start. It was left out of the banner and out of the first version of this article, and it is in the code. Three things make the Speso deflationary:

Two limits, stated plainly. The supply barely shrinks on its own: the minimum fee is one speso, ₷ 0.001, and a score of 116 lowers the ceiling by about ₷ 2.9 million, which would take roughly 2.9 billion minimum-fee transactions to burn. In practice the ceiling works by stopping issuance, and the strong effect is on the price. And it is not perpetual: the score is built from growth rates around a baseline, so a world growing at the baseline rate sits at score 100 and the value is flat. Booms are also partly undone: when the ceiling recovers, mining resumes up to it, so coins burned below the ceiling are issued again. The older counter-cyclical rule, where a weak economy raised the ceiling, is still there behind -Dspeso.elastic=true.

Hardening

Making a Prototype Behave Like It Might Be Attacked

Retargeting every 20 blocks, ±1–2 bits
PBKDF2 (200k) + AES-256-GCM wallet
Append-only chain, re-validated on load
Incremental sync, 100 blocks per batch
Signed challenge-response per connection
Token bucket, ban scores, bounded pools
ECDSA P-256, SHA-256

The details are in the README, but three changes made while fixing the list are worth showing. Signatures are bound to the network. What gets signed is the network id plus the payload, so a transaction signed for a testnet is simply invalid anywhere else:

Transaction.javaJava
/** What actually gets signed: the payload bound to this network. */ String signingText() { return Params.NETWORK_ID + "|" + payload(); }

The network id is a hash of every consensus setting, including the network name and the publisher list, and it is part of the genesis block. Wallet files are created private from the start: the file is created with rw------- in the same call, instead of being written first and locked down afterwards, which leaves a window under a permissive umask. And the CLI will not start without an explicit --i-understand-unaudited flag, and on -Dspeso.network=mainnet it also refuses to start without a publisher list.

Audit

What 253 Passing Tests Do and Do Not Mean

The third item on my list was the code is unaudited and shouldn't hold anything of real value. I cannot fix that with code, and anyone who tells you their own code is audited because their own tests pass is fooling you or themselves. An audit is an independent party, with adversarial intent, reading the code. What I can do is make the code easier to audit, attack it myself, and refuse to let it be run casually.

253checks pass
4suites, each in its own JVM
1,183lines of tests
0independent audits

The suite is mostly attacks, not demonstrations: stolen signatures, a tampered amount, a signature from the wrong key, a signature not bound to this network, forged blocks with overspends and reused sequence numbers, miner-chosen indicators, wrong difficulty, a lighter fork trying to replace a heavier one, a corrupted and a tampered chain file, a wrong wallet password, oversized network lines, garbage payloads, and a peer who keeps sending garbage until it is banned. For the oracle there is a test for every row of the table above except the last, which no test can cover: it is the case where the trust itself is betrayed.

🛑
Do not put anything of value on this

The tests were written by the same hands as the code, so they share its blind spots. The publisher thread (--publish) has run live on one machine (one publisher, a miner and thin wallets in separate processes), but not across several machines with several real publishers. The automatic feed (--auto-feed) is tested against local fixtures only: the real World Bank and IMF endpoints were never contacted. Formats changed, so chains from the previous version will not load. Treat Speso as a reference implementation of an idea, run it on a test network, and if it ever matters, pay somebody independent to break it.

Run It

Build, Test, and Start a Three-Publisher Test Network

Plain Java 17+. If there is no javac, the build script falls back to the jdk.compiler module.

shellsh
./build.sh ./test.sh # 253 checks, four JVMs # every node needs the SAME publisher list (their wallet addresses); it is part of the network id export SPESO_PASSWORD='something long' PUB='-Dspeso.publishers=<addrP1>,<addrP2>,<addrP3>' java $PUB -cp out com.techducat.speso.Main --i-understand-unaudited --port 7000 --data nodeA --mine --rpc 7100 --publish indicators.txt java $PUB -cp out com.techducat.speso.Main --i-understand-unaudited --port 7001 --data nodeB --peer 127.0.0.1:7000 --mine --feed indicators.txt

A node prints its address when it starts; a publisher's address is what goes into -Dspeso.publishers. The console has send, sendgbu, quote, rate, publishers, chain, status and a few more. The feed file is five lines (gdp=3.4, inflation=2.6, and so on) and may add market=1.02 for the peg feedback. With --rpc 7100 a node serves a small JSON API, and the thin wallet Cli (no chain, no mining) signs locally against it: Cli --wallet alice.dat send <addr> 5. --auto-feed makes a node fetch the World Bank and IMF data itself and publish it.

Usability Test

A First Run, Screen by Screen

Tested with JDK 21 using build.sh on this code: all four test suites pass (253 checks). One testnet node (a single publisher with threshold 1, which also reports and mines) and thin wallets in separate processes, with -Dspeso.initbits=10 -Dspeso.blockms=1500 -Dspeso.retarget=10. Commands are shown as speso-wallet for java -cp out com.techducat.speso.Cli. Not exercised: several publishers on several machines, the live World Bank and IMF endpoints, and the Gradle build.

What worked

Found and fixed during the test

Still rough

All 28 screens
Discoverability: No arguments: every command on its own line, with what it does.
1. Discoverability. No arguments: every command on its own line, with what it does.
Create a wallet: Wallet creation warns plainly that there is no recovery.
2. Create a wallet. Wallet creation warns plainly that there is no recovery.
Get your address: Your address needs no password.
3. Get your address. Your address needs no password.
Check the node: status: network, height, rate, supply and oracle, in readable units.
4. Check the node. status: network, height, rate, supply and oracle, in readable units.
Read the rate: rate: score 111.6 means 1 Sm = 1.1160 GBU. New: burned since genesis is Sm 0, because the supply is far below its ceiling.
5. Read the rate. rate: score 111.6 means 1 Sm = 1.1160 GBU. New: burned since genesis is Sm 0, because the supply is far below its ceiling.
Oracle publishers: publishers: the registered publisher set and whether each is live.
6. Oracle publishers. publishers: the registered publisher set and whether each is live.
Check balance: A new wallet starts at zero.
7. Check balance. A new wallet starts at zero.
Price in GBU: quote: cost now and the worst case after 1, 3 and 6 blocks.
8. Price in GBU. quote: cost now and the worst case after 1, 3 and 6 blocks.
Send payment: Funding Alice from the node's wallet.
9. Send payment. Funding Alice from the node's wallet.
Track it: tx: confirmed, with height and confirmations.
10. Track it. tx: confirmed, with height and confirmations.
Balance updated: Alice now holds Sm 5.
11. Balance updated. Alice now holds Sm 5.
GBU payment: sendgbu with the 'auto' ceiling: a safe price limit chosen from the 3-block worst case.
12. GBU payment. sendgbu with the 'auto' ceiling: a safe price limit chosen from the 3-block worst case.
Receiver balance: Bob received 2 GBU, which at the block's rate is Sm 1.793.
13. Receiver balance. Bob received 2 GBU, which at the block's rate is Sm 1.793.
Pending state: With mining paused, a payment waits in the mempool.
14. Pending state. With mining paused, a payment waits in the mempool.
Pending state: tx reports 'pending'.
15. Pending state. tx reports 'pending'.
Pending state: status shows mempool 1 and mining no.
16. Pending state. status shows mempool 1 and mining no.
Confirmed: Mining resumed: the same payment confirms.
17. Confirmed. Mining resumed: the same payment confirms.
Peg feedback: A publisher adds market=0.90: the ceiling falls from about Sm 18.8M to Sm 15.1M. Supply is far below it, so nothing is burned yet.
18. Peg feedback. A publisher adds market=0.90: the ceiling falls from about Sm 18.8M to Sm 15.1M. Supply is far below it, so nothing is burned yet.
Node console: The node's own console: publishers, quote, chain, balance.
19. Node console. The node's own console: publishers, quote, chain, balance.
Error: overspend: Rejected with the specific cause and the amounts.
20. Error: overspend. Rejected with the specific cause and the amounts.
Error: bad address: Says what an address is and how to get yours.
21. Error: bad address. Says what an address is and how to get yours.
Error: fraction: Says what is wrong and the limit.
22. Error: fraction. Says what is wrong and the limit.
Error: missing args: Names the command and shows its usage.
23. Error: missing args. Names the command and shows its usage.
Error: no wallet: Clear.
24. Error: no wallet. Clear.
Error: wrong password: Clear.
25. Error: wrong password. Clear.
Error: node down: Says which URL it tried and how to point it elsewhere.
26. Error: node down. Says which URL it tried and how to point it elsewhere.
Launch guard: mainnet is refused without publishers, with a non-zero exit code.
27. Launch guard. mainnet is refused without publishers, with a non-zero exit code.
Launch guard: Without --i-understand-unaudited the node refuses to start (exit 1).
28. Launch guard. Without --i-understand-unaudited the node refuses to start (exit 1).
Conclusion

Twenty-Four Years, and a Documentary

The idea was there in 1995. In 2002 I had a list of bullet points, one named problem and the first code. In 2026 I have about 3,000 lines of plain Java that solve the problem, a price rule that cannot be set by any single party, a feedback loop that reacts to the market without pretending to command it, and a list of things it does not do, kept as honest as the list of things it does.

Somewhere along the line, distractions and detours came along and after several pitstops and after a grand total of 24 years from when I wrote the first code (the initial idea was in 1995 but I wrote no code), I finally conclude! Now it is more of a documentary than being a threat to the petro-dollar and me being identified as a threat to the United States and thereby drawing attention from various goon-squad types.

— The author

I have no illusions about what a prototype with no audit and no users can threaten. But the shape of the idea (a money that no one issues, priced by the state of the whole world instead of by a single country's promise) is the one I wrote down in April 2002, and I still think it is worth building properly. The code is open. Read it, run it, break it, and tell me where it fails.