← docs

Decision log

Newest first. Each entry: what was decided, by whom, why, and what it invalidates.


D-002 — Target chain changes from Litecoin to Pepecoin (2026-09-10)

Decided by: Martin.

"Cancel the light coin install. Let's work on Pepe coin but not inscriptions like Doge's. Inscriptions like using Omni. I think there are better features with Omni like the check sum validator and built-in token exchange"

Action taken: Litecoin Core install cancelled and fully removed (binaries, datadir, staged tarballs). Nothing was left on disk; 416 GiB free, same as before we started.

What this invalidates: - Phase 2 as originally written (recovering Martin's 2021-2023 Omni inscriptions from Litecoin) is suspended, not impossible. See "Recovery is not lost" below. - Phase 3's plan to validate our consensus engine against a live OmniLite reference node.

What survives unchanged: Phase 1 Omni research. The Omni protocol sources (OmniLite, Omni Core, the spec) remain the ground truth regardless of which chain we deploy on.


D-001 — Proceed on the 5400 RPM HDD; blockfilterindex disabled (2026-09-10)

Superseded in part by D-002. Machine survey remains valid.


Feasibility: can Omni run on Pepecoin?

Verdict: yes. Omni is a meta-layer, not a chain feature — it is a payload encoding plus off-chain consensus rules, interpreted by an indexer. It needs no forked node, only a node whose transaction policy will relay the payload-carrying outputs.

All of the following are verified in source code, from github.com/pepecoinppc/pepecoin at tag v1.1.0 (commit 4fb5a0cd, 2024-12-16), cloned to /home/martin/reference/pepecoin.

Requirement Pepecoin v1.1.0 Source Verdict
Class COP_RETURN data output MAX_OP_RETURN_RELAY = 83 bytes (80 usable payload) src/script/standard.h:30 ✅ identical to BTC/LTC
Data carrier relayed by default DEFAULT_ACCEPT_DATACARRIER = true src/script/standard.h:16
Data size tunable -datacarrier, -datacarriersize src/init.cpp:483-484,1094-1095
Class B — bare multisig relayed DEFAULT_PERMIT_BAREMULTISIG = true src/validation.h:143
Bare multisig standardness x-of-3 accepted as standard src/policy/policy.cpp:41-48 ✅ exactly what Class B needs (1-of-3)
Dust limit (soft) DEFAULT_DUST_LIMIT = RECOMMENDED_MIN_TX_FEE = 0.01 PEP src/policy/policy.h:70, :23 ✅ cheap
Dust limit (hard, standardness) DEFAULT_HARD_DUST_LIMIT = DUST/10 = 0.001 PEP src/policy/policy.h:81 ✅ tunable via -harddustlimit
Recommended min fee RECOMMENDED_MIN_TX_FEE = COIN/100 = 0.01 PEP src/policy/policy.h:23
Coin precision COIN = 100000000 (8 decimals) src/amount.h:18 ✅ same as BTC/LTC

Other chain parameters (verified in source code, src/chainparams.cpp):

Parameter Value Line
Block spacing 60 seconds (Litecoin is 150 s, Bitcoin 600 s) :89
Subsidy halving interval 100,000 blocks :78
AuxPoW (merged mining) from height 42,000 :136
AuxPoW chain ID 0x003f (63) :119
Mainnet P2P port 33874 :153
Testnet P2P port 44874 :282
Regtest P2P port 18444 :392

Consequence of 1-minute blocks: inscription chunks confirm ~2.5x faster than on Litecoin and ~10x faster than Bitcoin. Good for multi-chunk file inscription, which is inherently serial across many transactions.


Corrections to two assumptions in the pivot

Martin cited "the check sum validator and built-in token exchange" as reasons to prefer Omni. Both features are real, but one does not do what the name suggests.

1. Omni's "consensus hash" validates ledger state, not file content

omni_getcurrentconsensushash hashes the entire Omni ledger (balances, properties, offers) at a given block, so two independent implementations can prove they agree. It is an implementation-agreement check. It is not a per-inscription content checksum, and it will not tell you whether a recovered JPEG is intact.

A content hash is still the right idea — it was already in the Phase 4 v2 spec ("a SHA-256 hash of the content"). That is ours to design; Omni does not supply it. Label: assumption pending Phase 1 verification against consensushash.cpp.

2. The "built-in exchange" trades token ↔ base currency, probably not token ↔ token

Omni's DEx (types 20 offer / 22 accept) sells a token for the chain's native coin — so on Pepecoin, token ↔ PEP. That works and transfers cleanly.

Omni Core on Bitcoin additionally has MetaDEx (types 25-28) for token ↔ token trading. The transaction-type list Martin compiled from OmniLite does not include 25-28, which suggests OmniLite lacks MetaDEx. If token-to-token trading matters, that capability may have to be ported from Omni Core rather than OmniLite. Label: assumption — must be confirmed in Phase 1 against src/omnicore/omnicore.h.


The significant new risk: no reference implementation

On Litecoin, OmniLite exists. Our engine could be proven correct by replaying real mainnet history and comparing omni_getcurrentconsensushash against a reference node at every block.

On Pepecoin there is no Omni, so there is no oracle. We would be the first and only implementation, defining truth with no independent way to catch our own consensus bugs. Also: on day one the DEx has no counterparties, and we must define our own genesis parameters (activation height, Exodus-equivalent address, ecosystem IDs).

Mitigation — and it removes the need for any 217 GiB sync: OmniLite ships unit tests (src/omnicore/test/) and functional tests (test/functional/omni_*.py) that run on regtest. Those are executable, authoritative test vectors for the Omni rules, and they need no mainnet chain at all. Building our engine against that suite gives real correctness validation on this modest hardware. This was already the Phase 3 plan; it now carries more weight because it is our only oracle.

Recovery is not lost

Martin's 2021-2023 files are still in Litecoin blocks and are not going anywhere. Omni's encoding (Class B / Class C, the payload layouts) is chain-independent, so if the extractor is written chain-agnostically from the start — chain params, genesis height and Exodus address as configuration, not constants — recovering them later costs a Litecoin sync and a config file, not a rewrite.

Design requirement adopted: every component we build takes chain parameters as config. No hard-coded Pepecoin assumptions.


D-003 — App scope decisions (2026-09-10)

Decided by Martin:

Question Decision
First feature after the M0-M2 foundation M3 MetaDEx exchange
Protocol marker (Class C, replaces Omni's omni) pepo
Crowdsales, types 51/53 In scope

Marker note: pepo is consensus-critical. It can change freely before launch; after the first mainnet inscription it cannot change without splitting state.


⚠️ MetaDEx constraints that affect the order-book design

Verified in source code, omnicore/src/omnicore/tx.cpp:1643-1666 (logicMath_MetaDExTrade):

Rule Code Consequence
Both sides are property IDs (u32) payload layout, createpayload.cpp:486 The native coin is not a property. MetaDEx cannot quote PEP.
Non-fungible properties rejected isPropertyNonFungible(...)PKT_ERROR_TOKENS -27, :1653 NFTs cannot trade on MetaDEx at all
property != desired_property :1658PKT_ERROR_METADEX -29 no self-pairs
Both sides same ecosystem isTestEcosystemProperty(...), :1666 main and test books never mix
Type must be permitted for that property at that block IsTransactionTypeAllowed(...), :1643 activation rules are per-property

What this means in practice

A TOKEN/PEP market with real bids and asks is not possible on MetaDEx. Token ↔ native coin is DEx only (types 20/22), which is one-sided sell offers — an offers list, not a book.

This is not an oversight in Omni; it is how Omni is structured. Omni's answer was a base property: property 1 (OMNI) is a token, so every pair TOKEN/OMNI is a genuine two-sided MetaDEx book, while OMNI/BTC is handled by the one-sided DEx.

For us the same shape applies: we need a base property — call it property 1 — against which all MetaDEx pairs quote. Which raises the question Omni answered in 2013 with the Exodus crowdsale: how does property 1 come into existence and get distributed?

Options: 1. Crowdsale (type 51). Send PEP to the issuer during a window, receive base tokens. This is literally what Omni did, and crowdsales are now in scope (D-003). 2. Burn-to-mint. Send PEP to the Exodus-equivalent address, receive base tokens at a fixed ratio. Simple, permissionless, ongoing, no issuer trust. 3. Fixed issuance (type 50) to a treasury address, distributed manually. Simplest to build, most centralised, weakest story. 4. No base property. Accept that only DEx (one-sided, token↔PEP) exists. Much weaker than what Martin asked for.

Second open problem: NFTs cannot trade on MetaDEx (:1653). Selling an NFT therefore needs either the DEx path or a mechanism we design. Flagged for M4.


D-004 — Protocol shape (2026-09-10)

Decided by Martin:

Question Decision Consequence
Base property (property 1) origin Burn-to-mint Send PEP to the Exodus-equivalent address, receive base tokens at a fixed ratio, permanently open. No issuer trust, no deadline, no premine. Gives the base token a hard PEP cost floor.
NFT sales Extend DEx (20/22) to accept non-fungible ranges Deliberate, documented divergence from Omni Core, which rejects NFTs on MetaDEx (tx.cpp:1653) and has no NFT DEx path. Must be spec'd and tested as our own rule.
Activation height Set at launch, ~tip + 1 day Left unset in config until M7. Indexer backlog stays at zero through development.
Ecosystems Keep both (main + test) Mirrors Omni ID ranges; gives an on-mainnet sandbox that never mixes with the main book.

Consequences that now need specifying

  1. Burn-to-mint is a new transaction path with no Omni equivalent. Omni's Exodus output is a marker for Class B, not a value sink. We are giving it a second meaning: PEP sent to it mints base tokens. These two roles must not collide — a Class B marker output must not accidentally mint. Needs an explicit, tested rule (e.g. minting only on an output that is the sole payment to Exodus in a transaction carrying no Omni payload, or a dedicated mint transaction type).
  2. NFT-on-DEx is our own consensus rule. No reference implementation exists for it, so it carries the same "no oracle" risk as everything else, and needs its own test vectors.

D-005 — Name, economics, signing, scope (2026-09-10)

Decided by Martin:

Question Decision
Project name Ribbit
Class C marker rbit (0x72 0x62 0x69 0x74) — supersedes pepo from D-003
Base token 1 PEP = 1 RBIT, divisible (8 dp), uncapped, minted by burn-to-mint only. No premine.
Signing Node wallet RPC — app builds payload, pepecoind funds and signs via fundrawtransaction / signrawtransaction. Keys never enter our process.
Litecoin recovery (old Phase 2) Dropped.

Effect of dropping Litecoin recovery

This is a real simplification, not just a deletion:

Burn-to-mint, settled parameters


D-006 — Openness, access, feature scope (2026-09-10)

Question Decision
Spec and code Public from the start
Web UI binding 127.0.0.1 only — reach remotely via SSH port-forward
Base token ticker RBIT
Delegates (73/74) In scope
Freezing (71/72/185/186) In scope
Send-to-owners (3) In scope
Restricted send (2) Out of scope

Why public matters more than it looks: a second independent implementation is the only genuine oracle for consensus correctness, and that can only exist if the spec is public. This partially answers the "no reference implementation" risk from D-002 — not immediately, but it is the only path that ever resolves it.

Why localhost-only matters: the app can trigger signing through the node wallet (D-005), so exposing the UI exposes spending authority. Localhost is the correct default and costs nothing, since SSH port-forwarding covers remote use.


D-007 — No base token; burn-to-mint removed (2026-09-10)

Decided by Martin: no base token at all. Supersedes D-004 and D-005 on this point.

What prompted it

Martin asked: "If the Pepe is burned when the rbit is created, how would somebody convert their RBIT back to Pepe?"

They cannot. Ribbit is a sidecar meta-layer with no consensus power over PEP — only a private-key holder can move PEP. Burning means sending to an address with no key, so the protocol can never release it. This is structural. No design gives trustless redemption of the base coin; any redemption requires a custodian holding real PEP. (Omni itself was custodial here: its Exodus address held real BTC controlled by the foundation, not burned.)

Correction to an earlier claim of mine

D-005 recorded that burn-to-mint gives RBIT "a hard floor of 1 burned PEP." That was wrong, and backwards. Under uncapped 1:1 minting:

A quote asset is precisely what traders hold between trades, so this would have taxed every user of the order book. Dropping it is the right call.

What this removes

Removed Effect
RBIT the token, property 1 no base/quote asset ships with the protocol
Burn-to-mint the Exodus address reverts to being purely a Class B marker, as in Omni
Exodus dual-meaning rule the "marker vs value sink" collision problem (D-004) disappears entirely
One of two unreferenced divergences only NFT-on-DEx remains as novel consensus code

Net: meaningfully less consensus surface, and less of it unvalidated. "Ribbit" and the rbit marker are unaffected — they name the protocol, not a token.

What the exchange looks like now

Pair Mechanism Book shape
token ↔ token MetaDEx (25-28) genuine two-sided book, bids and asks
token ↔ PEP DEx (20/22), payment window one-sided sell offers
NFT ↔ PEP DEx, extended for ranges (D-004) one-sided listings

No canonical quote asset ships. If the market converges on one — a wrapped-PEP or a stablecoin issued by a participant — it becomes the de-facto quote asset without us blessing it. That is a defensible position: we provide the venue, not the money.

Implementation notes

  1. Property IDs 1 and 2 are special-cased in Omni as OMNI_PROPERTY_MSC / OMNI_PROPERTY_TMSC (verified in source, omnicore.h:123-124). With no base token these are free. Decision: leave 1 and 2 permanently reserved and unassigned, user properties start at 3. Costs nothing and keeps a slot if a base token is ever revisited.
  2. Ported Omni test vectors assume OMNI/TOMNI exist. Any vector that funds an account with property 1 or 2 needs adaptation. Expect friction in M2-M3 test porting; budget for it.

D-008 — Crowdsales dropped (2026-09-10)

Decided by Martin: remove types 51 (create property variable / crowdsale) and 53 (close crowdsale) from scope. Supersedes D-003.

Rationale: a crowdsale's payload carries propertyIdDesired (verified in source, createpayload.cpp:228-239) — it can only be funded with another property, never the native coin. With no base token (D-007), there is nothing to fund the first crowdsale with. Keeping them would have meant either shipping a feature that cannot be used, or adding a second novel consensus rule (native-coin crowdsale funding) immediately after deleting one.

Removed: bonus math, deadlines, early-close rules, issuer-percentage handling, and the crowdsale-participation code path in state processing. A meaningful slice of M6.

Consensus-hash note: GetConsensusHash() has a crowdsale section (verified in source, consensushash.cpp:232-234). We keep the section in our implementation but it will always be empty, so it contributes nothing. Documented so that re-adding crowdsales later does not change the hash of existing state.

Reversible? Yes, cleanly. Crowdsales touch property creation and balances but nothing else depends on them.


D-009 — Encrypted messaging: integration and carrier (2026-09-11)

Decided by Martin.

Question Decision
Where it lives Inside Ribbit — a Ribbit message type sharing the payload codec, Class B/C encoding, indexer, reorg handling and database
Primary carrier Class B (7,646 bytes/tx), Class C for messages fitting in 72 bytes
Messages > 7,646 bytes Chained Class B with a countdown index, using the Doginals UTXO-chaining mechanism

Correction: the 25-transaction limit is not a ceiling

I had cited Pepecoin's 25-ancestor mempool limit as an argument against OP_RETURN chunking. Martin pointed out this is wrong, and he is right: the limits are on in-mempool ancestors (init.cpp:437, :439), and removeForBlock (txmempool.h:554) drops mined transactions from the mempool, so a confirmation resets the count. Send 25, wait one block (~60 s), send 25 more. It is a throughput constraint, not a ceiling.

The real arguments for Class B are atomicity and cost:

A size limit that binds before the count does

Verified in source, validation.h:76,80: DEFAULT_ANCESTOR_SIZE_LIMIT = DEFAULT_DESCENDANT_SIZE_LIMIT = 101 kB.

An unconfirmed chain is capped at ~101 kB regardless of transaction count. Irrelevant on the OP_RETURN path (25 × ~250 B ≈ 6 kB, so the count binds first), but decisive for chained Class B: at ~14.7 kB per full transaction only ~6 can chain unconfirmed, not 25. Martin's batch-and-wait strategy applies exactly here.


What Doginals does, and what we take from it

Read from Martin's own github.com/martinseeger2002/doginals, doginals.js:434-560.

MAX_CHUNK_LEN   = 240    // bytes per data push
MAX_PAYLOAD_LEN = 1500   // bytes per transaction
  1. Data is split into 240-byte chunks. The inscription script is 'ord', chunk count, content type, then (index, chunk) pairs where the index counts down: parts.length - n - 1, so the final chunk carries index 0.
  2. Chunks are packed into ~1500-byte partials, one per transaction.
  3. Each transaction creates a P2SH output committing to its partial. The redeem script is only <pubkey> OP_CHECKSIGVERIFY <OP_DROP × n> OP_TRUE.
  4. The next transaction spends it, and the data rides in that transaction's scriptSig as plain pushes, which the OP_DROPs discard.

Why 1500 is not arbitrary

Verified in source, pepecoin/src/policy/policy.cpp:78-93:

scriptSig.size() > 1650   ->  "scriptsig-size", non-standard
!scriptSig.IsPushOnly()   ->  "scriptsig-not-pushonly"

plus MAX_SCRIPT_ELEMENT_SIZE = 520 (script.h:22). So ~1500 bytes of data, a ~72-byte signature and a ~40-byte redeem script land just under 1650. The constant is tuned precisely to that ceiling. The technique is mainnet-standard on Pepecoin.

The two properties worth stealing

Carrier comparison

Carrier Bytes/tx Atomic Ordering Mainnet-standard
Class C (OP_RETURN) 72 needs index
Class B (multisig) 7,646 ✅ ≤7,646 n/a, single tx
Doginals P2SH ~1,500 UTXO chain

Decision: borrow the mechanism, not the carrier. Chained Class B with a countdown index gives Doginals' structural ordering and self-describing completion while carrying 5× more data per transaction.

Resulting shape

Message size Carrier Chunking machinery
≤ 72 bytes Class C, one transaction none
≤ 7,646 bytes Class B, one transaction none
larger chained Class B, countdown index chunk header only on this path

D-010 — The Messenger is testnet-only, permanently (2026-09-11)

Decided by Martin: "The Messenger part of the application should always use testnet."

This is a permanent product rule, not a testing phase. Encrypted messaging never operates on mainnet.

What this changes

Area Effect
Network selection The messaging code path refuses to run against mainnet, enforced in code rather than by configuration. A mainnet params object passed to a messaging function is an error, not an option.
Mainnet standardness Still designed for. Testnet does not enforce standardness (fRequireStandard = false, chainparams.cpp:309), so a transaction that only works there would be silently non-standard. Keeping to mainnet rules means the format stays honest and portable.
Threat model Testnet chains can be reset or deep-reorged. Messages are not durable. This must be stated plainly to users — it is a property of the medium, not a bug.
Coins Testnet PEP has no value, so fee estimates are informational only.
Node Requires a second node alongside the mainnet Ribbit node.

Why designing to mainnet limits still matters

Testnet accepts non-standard transactions. Without the discipline of mainnet limits we would not discover that a format is unrelayable until someone tried it somewhere real. The regtest standardness check (-acceptnonstdtxn=0) in Step 4 is what actually proves it.


D-011 — Renamed to DogecoinArcade; multi-chain (2026-09-11)

Decided by Martin: "Let's change the name of the application to DogecoinArcade. I know it's for Pepe, but it should also work on Doge in the exact same way."

Item Old New
Project Ribbit DogecoinArcade
Class C marker rbit arcd (0x61 0x72 0x63 0x64)
Python package ribbit arcade
Distribution name ribbit dogecoin-arcade
Repo path ~/ribbit ~/dogecoin-arcade

Entries D-001 through D-010 above still say "Ribbit". They are left as written: a decision log that gets retroactively edited is not a record. Read "Ribbit" as "DogecoinArcade" in anything dated before 2026-09-11.

The marker change was free because nothing has launched. After the first mainnet transaction it would be impossible — a marker change splits state.

Relationship to the existing GitHub repo

github.com/martinseeger2002/DogecoinArcade already exists and already contains encrypted messaging (SendSms.py, DecryptSmsData.py, encrypt_data.py, callGetPubKey.py). Martin's decision: keep them entirely distinct. This is a new project that shares a name; nothing is merged and nothing is pushed.


Dogecoin support: verified, and nearly free

Verified against github.com/dogecoin/dogecoin at 1.14.99, cloned to /home/martin/reference/dogecoin.

Every constant this protocol depends on is identical between Dogecoin and Pepecoin — same values, and the same source line numbers, because Pepecoin is a Dogecoin fork:

Constant Value Source (identical in both)
MAX_OP_RETURN_RELAY 83 script/standard.h:30
DEFAULT_ACCEPT_DATACARRIER true script/standard.h:16
DEFAULT_PERMIT_BAREMULTISIG true validation.h:143
x-of-3 bare multisig standard yes policy/policy.cpp:41
COIN 100000000 amount.h:18
RECOMMENDED_MIN_TX_FEE COIN/100 policy/policy.h:23
DEFAULT_DUST_LIMIT = min fee policy/policy.h:70
DEFAULT_HARD_DUST_LIMIT DUST/10 policy/policy.h:81
scriptSig standardness limit 1650 policy/policy.cpp:86
DEFAULT_ANCESTOR_LIMIT / size 25 / 101 kB validation.h:74,76
Block spacing 60 s chainparams.cpp

Only chain identity differs:

Dogecoin Pepecoin
mainnet P2PKH version 30 (D…) 56 (P…)
mainnet P2SH version 22 22
mainnet P2P / RPC 22556 / 22555 33874 / 33873
testnet P2P / RPC 44556 / 44555 44874 / 44873

Because Params already holds exactly these fields, Dogecoin support cost three new Params instances and zero code changes.

⚠️ Testnet addresses are ambiguous between the two chains

Dogecoin testnet and Pepecoin testnet both use PUBKEY_ADDRESS = 113 and SCRIPT_ADDRESS = 196. A testnet address cannot tell you which chain it is for. Never infer the chain from an address; record it explicitly alongside any key announcement or message. Noted in arcade/config.py beside the params.

Messaging guard

require_messaging_network() enforces D-010 in code: messaging functions refuse any mainnet params object. MESSAGING_NETWORKS is {test, regtest, doge-test, doge-regtest}.


Martin's existing encryption, compared to the new brief

Read from github.com/martinseeger2002/DogecoinArcade, encrypt_data.py.

Existing scheme: secp256k1 ECDH → HKDF-SHA256 → AES-256-GCM key-wrap → AES-256-GCM data.

Existing Brief requires
Curve secp256k1, using wallet keys X25519, separate from wallet
Construction ECDH + HKDF + AES-GCM keywrap + AES-GCM libsodium sealed box
Envelope overhead 93 bytes (33 pubkey + 12 IV + 32 wrapped key + 16 tag) 48 bytes (32 ephemeral pubkey + 16 MAC)

The existing construction is cryptographically sound — ECDH+HKDF+AES-GCM is a legitimate KEM/DEM. Two reasons the new design still differs:

  1. 45 bytes of pure overhead. A sealed box derives the data key directly from the ECDH output instead of wrapping a separate AES key. On an 80-byte OP_RETURN that difference decides whether a short message fits at all.
  2. Wallet keys are the wrong keys. Encrypting to a spending key welds messaging identity to spending authority: the key is exposed by every signature, cannot be rotated without moving funds, and its compromise reads every past message. Separate X25519 identity keys fix all three.

D-012 — The application is permanently dual-chain (2026-09-11)

Decided by Martin: "the page should have both testnet and mainnet on it forever because we will be using testnet for messages and main for tokens and nfts and also pep transactions."

Chain Role Sections
Testnet Messenger Inbox, Compose, Keys
Mainnet Ledger Tokens, NFTs, Exchange, PEP transactions, Explorer

This is a permanent split, not a migration path. It supersedes the implicit assumption in the web UI that one network was selected at launch.

Why the split makes sense

Messages are not durable: testnet chains get reset and deep-reorged (D-010), and messages are conversational rather than assets. Tokens, NFTs and PEP balances are assets and belong on a chain nobody resets. Each side gets the chain whose properties match what it holds.

It also means a bug in the messaging code can never touch real value, which is a stronger guarantee than any amount of care in the messaging code itself.

The immediate constraint

The mainnet node runs with disablewallet=1 (docs/00-node-setup.md), chosen in Phase 0 because nothing then needed to sign. So mainnet is currently read-only: it can serve blocks, indexed token state and NFT state, but cannot send PEP or create a token.

Enabling a mainnet wallet is a deliberate step Martin has to take, because that wallet holds real money. Until then the UI must show mainnet sections as read-only rather than failing obscurely at signing time.

Guards that must hold

  1. require_messaging_network() continues to refuse mainnet for anything in arcade.messaging. The dual-chain UI makes it easier to point a messaging call at the wrong node, not harder, so this matters more now, not less.
  2. verify_connected_chain() runs on every connection, so a misconfigured datadir cannot silently cross the streams.
  3. Every section states which chain it is acting on, in the interface itself. A user who cannot tell whether they are about to spend testnet or real coins is one misclick from a bad day.

D-013 — The wallet is the identity; there is no passphrase (2026-09-11)

Decision. The messaging identity is derived from a wallet address. There is no passphrase, no key file, and nothing for a user to write down. wallet.dat is the only thing they have to keep.

What this replaces

D-009 gave the messaging key its own passphrase, encrypted at rest with Argon2id. That was cryptographically sound and a poor product. The failure mode was absolute: forget the passphrase and every message ever sent to that identity is unreadable, by anyone, forever. Two attempts were made to soften it — generating the passphrase rather than letting the user invent one, then saving it on the machine so it could be looked up later — and the second undoes most of the point of the first. Storing a secret next to the thing it protects is not protection; it is a passphrase-shaped ritual.

Martin's instruction was direct: "I don't want a pass phrase to be saved or shown. The only thing that the user should need is their wallet.dat files."

How it works

signature = wallet.signmessage(address, "DogecoinArcade messaging identity v1")
seed      = HKDF-SHA256(signature)
identity  = X25519 keypair from that seed

signmessage is deterministic — RFC 6979 — so the same address always produces the same signature and therefore the same identity. Verified on Pepecoin: three calls, byte-identical.

The address is filed in the wallet under the account arcade-identity. Accounts live inside wallet.dat, which is what makes a restore self-sufficient: the application finds the address again without needing its own database.

The subtle part, which was a bug first

getaccountaddress returns a fresh address as soon as the current one has been used. Calling it on each startup would therefore have changed the identity silently, orphaning every message already received — the exact failure the passphrase was removed to avoid. Caught in testing, before it shipped.

The rule is now: take the account's existing addresses, sort them, use the first. getaccountaddress is reached only when the account is genuinely empty. Three independent installations against one wallet derive the same identity, and the CLI and the web interface agree with each other.

What it costs, stated plainly

Whoever can use the wallet can re-derive the identity. The wallet's security becomes the messaging security; if the wallet is unencrypted, so is the identity in practice. Against a standalone key with its own passphrase this is a real reduction, and it is the trade being made deliberately: the passphrase's failure mode was certain and common, this one requires an attacker to already have the wallet — at which point they have the coins too.

A spending key is still never used for encryption. Deriving a separate X25519 key keeps encryption away from spending, so a compromise of one is not automatically a compromise of the other, and the messaging key can be rotated by moving to a different address without moving funds.

Consequences


D-014 — Public group posts may run on mainnet (2026-09-11)

Decision. A public, unencrypted group channel runs on both chains. D-010 is unchanged: nothing encrypted ever touches mainnet.

Why this is not a reversal of D-010

D-010 says the Messenger is testnet-only, permanently, and the reasons were specific: messaging code is young, a bug in it must never be able to touch real value, and an encrypted message carries key material that would tie a mainnet identity to a correspondence. None of that applies to a public post.

A group post is not sealed to anybody. It carries no key material. It reveals nothing that publishing it does not already reveal, because publishing it is the act. So the question "may this run on mainnet?" stops being a safety question and becomes a cost question, which is the user's to answer. Martin answered it.

How the boundary is kept

MessageSender still calls require_messaging_network() on construction. The only way past it is public_only=True, which is:

Carriage, and why a post is short

Class C: one OP_RETURN output. One output means no dust, so a post costs a fee and nothing else. That caps a post at about 60 characters.

That is a real limit and the right trade. Class B would carry 7.5 KB, but it pays roughly one unspendable output per 30 bytes — on testnet that is free coins, and on mainnet it is somebody's money burnt to say something in public. A 60-character public post costs about 0.0028 in fees; the same text carried Class B would cost that plus several coins of dust forever.

What the interface must say

The rest of the application trains the expectation that messages are private, so the public half has to contradict that expectation loudly rather than quietly:

  1. Every screen where a post can be written states that nothing there is encrypted, that it is permanent, and that it cannot be deleted.
  2. The two chains are chosen explicitly, labelled free coins, nothing at stake and real coins, permanent. There is no single control that silently spends money.
  3. A mainnet post shows the post's text back to the user inside the confirmation, with the fee, before anything is broadcast — the ground rule for mainnet transactions, applied here.

Channels have no membership

A channel is a name. Anyone using the same name is in the same room; there is no invitation, no member list, no way to remove anyone, and no moderation. Saying so plainly is the whole of the access-control design, and pretending otherwise would be worse than the limitation.