32 Commits

Author SHA1 Message Date
b0ccde749c chore: bump version to 0.1.4 2026-02-19 02:48:56 -05:00
4ba7a23ae3 feat: add comprehensive compilation checks to validate.sh
Adds interpreter, JS compilation, and C compilation checks for all
examples, showcase programs, standard examples, and projects (113 total
checks). Skip lists exclude programs requiring unsupported effects or
interactive I/O.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:43:46 -05:00
89741b4a32 fix: move top-level let initialization into main() in C backend
Top-level let bindings with function calls (e.g., `let result = factorial(10)`)
were emitted as static initializers, which is invalid C since function calls
aren't compile-time constants. Now globals are declared with zero-init and
initialized inside main() before any run expressions execute.

Also fixes validate.sh to use exit codes instead of grep for cargo check/build.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:31:49 -05:00
3a2376cd49 feat: port AST definitions to Lux (self-hosting)
Translate all 30+ type definitions from src/ast.rs (727 lines Rust)
into Lux ADTs in projects/lux-compiler/ast.lux.

Types ported: Span, Ident, Visibility, Version, VersionConstraint,
BehavioralProperty, WhereClause, ModulePath, ImportDecl, Program,
Declaration, FunctionDecl, Parameter, EffectDecl, EffectOp, TypeDecl,
TypeDef, RecordField, Variant, VariantFields, Migration, HandlerDecl,
HandlerImpl, LetDecl, TraitDecl, TraitMethod, TraitBound, ImplDecl,
TraitConstraint, ImplMethod, TypeExpr, Expr (19 variants), Literal,
LiteralKind, BinaryOp, UnaryOp, Statement, MatchArm, Pattern.

Passes `lux check` and `lux run`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:07:30 -05:00
4dfb04a1b6 chore: sync Cargo.lock with version 0.1.3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:05:51 -05:00
3cdde02eb2 feat: add Int.toFloat/Float.toInt JS backend support and fix Map C codegen
- JS backend: Add Int/Float module dispatch in both Call and EffectOp paths
  for toFloat, toInt, and toString operations
- C backend: Fix lux_strdup → lux_string_dup in Map module codegen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 02:05:40 -05:00
a5762d0397 feat: add built-in Map type with String keys
Add Map<String, V> as a first-class built-in type for key-value storage,
needed for self-hosting the compiler (parser/typechecker/interpreter all
rely heavily on hashmaps).

- types.rs: Type::Map(K,V) variant, all match arms (unify, apply, etc.)
- interpreter.rs: Value::Map, 12 BuiltinFn variants (new/set/get/contains/
  remove/keys/values/size/isEmpty/fromList/toList/merge), immutable semantics
- typechecker.rs: Map<K,V> resolution in resolve_type
- js_backend.rs: Map as JS Map with emit_map_operation()
- c_backend.rs: LuxMap struct (linear-scan), runtime fns, emit_map_operation()
- main.rs: 12 tests covering all Map operations
- validate.sh: now checks all projects/ directories too

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:45:13 -05:00
1132c621c6 fix: allow newlines before then in if/then/else expressions
The parser now skips newlines between the condition and `then` keyword,
enabling multiline if expressions like:
  if long_condition
    then expr1
    else expr2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:38:05 -05:00
a0fff1814e fix: JS backend scoping for let/match/if inside closures
Three related bugs fixed:
- BUG-009: let bindings inside lambdas hoisted to top-level
- BUG-011: match expressions inside lambdas hoisted to top-level
- BUG-012: variable name deduplication leaked across function scopes

Root cause: emit_expr() uses writeln() for statements, but lambdas
captured only the return value, not the emitted statements. Also,
var_substitutions from emit_function() leaked to subsequent code.

Fix: Lambda handler now captures all output emitted during body
evaluation and places it inside the function body. Both emit_function
and Lambda save/restore var_substitutions to prevent cross-scope leaks.
Lambda params are registered as identity substitutions to override any
outer bindings with the same name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:10:55 -05:00
4e9e823246 fix: record spread works with named type aliases
Resolve type aliases (e.g. Player -> { pos: Vec2, speed: Float })
before checking if spread expression is a record type. Previously
{ ...p, field: val } failed with "must be a record type, got Player"
when the variable had a named type annotation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 00:01:20 -05:00
6a2e4a7ac1 chore: bump version to 0.1.3 2026-02-18 23:06:10 -05:00
3d706cb32b feat: add record spread syntax { ...base, field: val }
Adds spread operator for records, allowing concise record updates:
  let p2 = { ...p, x: 5.0 }

Changes across the full pipeline:
- Lexer: new DotDotDot (...) token
- AST: optional spread field on Record variant
- Parser: detect ... at start of record expression
- Typechecker: merge spread record fields with explicit overrides
- Interpreter: evaluate spread, overlay explicit fields
- JS backend: emit native JS spread syntax
- C backend: copy spread into temp, assign overrides
- Formatter, linter, LSP, symbol table: propagate spread

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:05:27 -05:00
7c3bfa9301 feat: add Math.sin, Math.cos, Math.atan2 trig functions
Adds trigonometric functions to the Math module across interpreter,
type system, and C backend. JS backend already supported them.
Also adds #include <math.h> to C preamble and handles Math module
calls through both Call and EffectOp paths in C backend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:05:12 -05:00
b56c5461f1 fix: JS const _ duplication and hardcoded version string
- JS backend now emits wildcard let bindings as side-effect statements
  instead of const _ declarations, fixing SyntaxError on multiple let _ = ...
- Version string now uses env!("CARGO_PKG_VERSION") to auto-sync with Cargo.toml
- Add -lm linker flag for math library support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:05:03 -05:00
61e1469845 feat: add ++ concat operator and auto-invoke main
BUG-004: Add ++ operator for string and list concatenation across all
backends (interpreter, C, JS) with type checking and formatting support.

BUG-001: Auto-invoke top-level `let main = fn () => ...` when main is
a zero-parameter function, instead of just printing the function value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:01:41 -05:00
bb0a288210 chore: bump version to 0.1.2 2026-02-18 21:16:44 -05:00
5d7f4633e1 docs: add explicit commit instructions to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:09:27 -05:00
d05b13d840 fix: JS backend compiles print() to console.log()
Bare `print()` calls in Lux now emit `console.log()` in JS output
instead of undefined `print()`. Fixes BUG-006.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:09:07 -05:00
0ee3050704 chore: bump version to 0.1.1 2026-02-18 20:41:43 -05:00
80b1276f9f fix: release script auto-bumps patch by default
Release script now supports: patch (default), minor, major, or explicit
version. Auto-updates Cargo.toml and flake.nix before building.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:41:29 -05:00
bd843d2219 fix: record type aliases now work for unification and field access
Expand type aliases via unify_with_env() everywhere in the type checker,
not just in a few places. This fixes named record types like
`type Vec2 = { x: Float, y: Float }` — they now properly unify with
anonymous records and support field access (v.x, v.y).

Also adds scripts/validate.sh for automated full-suite regression
testing (Rust tests + all 5 package test suites + type checking).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:21:29 -05:00
d76aa17b38 feat: static binary builds and automated release script
Switch reqwest from native-tls (openssl) to rustls-tls for a pure-Rust
TLS stack, enabling fully static musl builds. Add `nix build .#static`
for portable Linux binaries and `scripts/release.sh` for automated
Gitea releases with changelog generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 19:09:32 -05:00
c23d9c7078 fix: test runner now supports module imports
The `lux test` command used Parser::parse_source() and
check_program() directly, which meant test files with `import`
statements would fail with type errors. Now uses ModuleLoader
and check_program_with_modules() to properly resolve imports,
and run_with_modules() for execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 17:11:16 -05:00
fffacd2467 feat: C backend module import support, Int/Float.toString, Test.assertEqualMsg
The C backend can now compile programs that import user-defined modules.
Module-qualified calls like `mymodule.func(args)` are resolved to prefixed
C functions (e.g., `mymodule_func_lux`), with full support for transitive
imports and effect-passing. Also adds Int.toString/Float.toString to type
system, interpreter, and C backend, and Test.assertEqualMsg for labeled
test assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:35:24 -05:00
2ae2c132e5 docs: add language philosophy document and compiler integration
Write comprehensive PHILOSOPHY.md covering Lux's six core principles
(explicit over implicit, composition over configuration, safety without
ceremony, practical over academic, one right way, tools are the language)
with detailed comparisons against JS/TS, Python, Rust, Go, Java/C#,
Haskell/Elm, and Gleam/Elixir. Includes tooling audit and improvement
suggestions.

Add `lux philosophy` command to the compiler, update help screen with
abbreviated philosophy, and link from README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:19:29 -05:00
4909ff9fff docs: add package ecosystem plan and error documentation workflow
Add PACKAGES.md analyzing the Lux package ecosystem gaps vs stdlib,
with prioritized implementation plans for markdown, xml, rss, frontmatter,
path, and sitemap packages. Add CLAUDE.md instructions for documenting
Lux language errors in ISSUES.md during every major task.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:01:56 -05:00
8e788c8a9f fix: embed C compiler path at build time for self-contained binary
build.rs captures the absolute path to cc/gcc/clang during compilation
and bakes it into the binary. On Nix systems this embeds the full
/nix/store path so `lux compile` works without cc on PATH.

Lookup order: $CC env var > embedded build-time path > PATH search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 08:12:18 -05:00
dbdd3cca57 chore: move blu-site to its own repo at ~/src/blu-site
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:57:55 -05:00
3ac022c04a chore: gitignore build output (_site/, docs/)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:48:51 -05:00
6bedd37ac7 fix: show help menu when running lux with no arguments
Previously `lux` with no args entered the REPL. Now it shows the help
menu. Use `lux repl` to start the REPL explicitly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:34:09 -05:00
2909bf14b6 fix: eliminate all non-json C backend errors (79→0)
Second round of C backend fixes, building on d8871ac which reduced
errors from 286 to 111. This eliminates all 79 non-json errors:

- Fix function references as values (wrap in LuxClosure*)
- Fix fold/map/filter with type-aware calling conventions
- Add String.indexOf/lastIndexOf emission and C runtime functions
- Add File.readDir with dirent.h implementation
- Fix string concat in closure bodies
- Exclude ADT constructors from closure free variable capture
- Fix match result type inference (prioritize pattern binding types)
- Fix Option inner type inference (usage-based for List.head)
- Fix void* to struct cast (dereference through pointer)
- Handle constructors in emit_expr_with_env

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:56:21 -05:00
d8871acf7e fix: improve C backend robustness, reduce compilation errors by 61%
- Fix closure captured variable types: look up actual types from var_types
  instead of hardcoding LuxInt for all captured variables
- Register function parameters in var_types so closures can find their types
- Replace is_string_expr() with infer_expr_type() for more accurate string
  detection in binary ops (concat, comparison)
- Add missing String operations to infer_expr_type (substring, indexOf, etc.)
- Add module method call type inference (String.*, List.*, Int.*, Float.*)
- Add built-in Result type (Ok/Err) to C prelude alongside Option
- Register Ok/Err/Some/None in variant_to_type and variant_field_types
- Fix variable scoping: use if-statement pattern instead of ternary when
  branches emit statements (prevents redefinition of h2/h3 etc.)
- Add RC scope management for if-else branches and match arms to prevent
  undeclared variable errors from cleanup code
- Add infer_pattern_binding_type for better match result type inference
- Add expr_emits_statements helper to detect statement-emitting expressions
- Add infer_option_inner_type for String.indexOf (returns Option<Int>)

Reduces blu-site compilation errors from 286 to 111 (remaining are mostly
unsupported json effect and function-as-value references).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:56:27 -05:00
120 changed files with 4447 additions and 5484 deletions

5
.gitignore vendored
View File

@@ -4,6 +4,11 @@
# Claude Code project instructions
CLAUDE.md
# Build output
_site/
docs/*.html
docs/*.css
# Test binaries
hello
test_rc

View File

@@ -42,17 +42,45 @@ When making changes:
7. **Fix language limitations**: If you encounter parser/type system limitations, fix them (without regressions on guarantees or speed)
8. **Git commits**: Always use `--no-gpg-sign` flag
### Post-work checklist (run after each major piece of work)
### Post-work checklist (run after each committable change)
**MANDATORY: Run the full validation script after every committable change:**
```bash
nix develop --command cargo check # No Rust errors
nix develop --command cargo test # All tests pass (currently 381)
./target/release/lux check # Type check + lint all .lux files
./target/release/lux fmt # Format all .lux files
./target/release/lux lint # Standalone lint pass
./scripts/validate.sh
```
This script runs ALL of the following checks and will fail if any regress:
1. `cargo check` — no Rust compilation errors
2. `cargo test` — all Rust tests pass (currently 387)
3. `cargo build --release` — release binary builds
4. `lux test` on every package (path, frontmatter, xml, rss, markdown) — all 286 package tests pass
5. `lux check` on every package — type checking + lint passes
If `validate.sh` is not available or you need to run manually:
```bash
nix develop --command cargo check # No Rust errors
nix develop --command cargo test # All Rust tests pass
nix develop --command cargo build --release # Build release binary
cd ../packages/path && ../../lang/target/release/lux test # Package tests
cd ../packages/frontmatter && ../../lang/target/release/lux test
cd ../packages/xml && ../../lang/target/release/lux test
cd ../packages/rss && ../../lang/target/release/lux test
cd ../packages/markdown && ../../lang/target/release/lux test
```
**Do NOT commit if any check fails.** Fix the issue first.
### Commit after every piece of work
**After completing each logical unit of work, commit immediately.** Do not let changes accumulate uncommitted across multiple features. Each commit should be a single logical change (one feature, one bugfix, etc.). Use `--no-gpg-sign` flag for all commits.
**After completing each logical unit of work, commit immediately.** This is NOT optional — every fix, feature, or change MUST be committed right away. Do not let changes accumulate uncommitted across multiple features. Each commit should be a single logical change (one feature, one bugfix, etc.). Use `--no-gpg-sign` flag for all commits.
**Commit workflow:**
1. Make the change
2. Run `./scripts/validate.sh` (all 13 checks must pass)
3. `git add` the relevant files
4. `git commit --no-gpg-sign -m "type: description"` (use conventional commits: fix/feat/chore/docs)
5. Move on to the next task
**Never skip committing.** If you fixed a bug, commit it. If you added a feature, commit it. If you updated docs, commit it. Do not batch unrelated changes into one commit.
**IMPORTANT: Always verify Lux code you write:**
- Run with interpreter: `./target/release/lux file.lux`
@@ -71,10 +99,45 @@ nix develop --command cargo test # All tests pass (currently 381)
| `lux serve` | `lux s` | Static file server |
| `lux compile` | `lux c` | Compile to binary |
## Documenting Lux Language Errors
When working on any major task that involves writing Lux code, **document every language error, limitation, or surprising behavior** you encounter. This log is optimized for LLM consumption so future sessions can avoid repeating mistakes.
**File:** Maintain an `ISSUES.md` in the relevant project directory (e.g., `~/src/blu-site/ISSUES.md`).
**Format for each entry:**
```markdown
## Issue N: <Short descriptive title>
**Category**: Parser limitation | Type checker gap | Missing feature | Runtime error | Documentation gap
**Severity**: High | Medium | Low
**Status**: Open | **Fixed** (commit hash or version)
<1-2 sentence description of the problem>
**Reproduction:**
```lux
// Minimal code that triggers the issue
```
**Error message:** `<exact error text>`
**Workaround:** <how to accomplish the goal despite the limitation>
**Fix:** <if fixed, what was changed and where>
```
**Rules:**
- Add new issues as you encounter them during any task
- When a previously documented issue gets fixed, update its status to **Fixed** and note the commit/version
- Remove entries that are no longer relevant (e.g., the feature was redesigned entirely)
- Keep the summary table at the bottom of ISSUES.md in sync with the entries
- Do NOT duplicate issues already documented -- check existing entries first
## Code Quality
- Fix all compiler warnings before committing
- Ensure all tests pass (currently 381 tests)
- Ensure all tests pass (currently 387 tests)
- Add new tests when adding features
- Keep examples and documentation in sync

216
Cargo.lock generated
View File

@@ -135,16 +135,6 @@ dependencies = [
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -235,7 +225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -297,21 +287,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -552,16 +527,17 @@ dependencies = [
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"bytes",
"futures-util",
"http",
"hyper",
"native-tls",
"rustls",
"tokio",
"tokio-native-tls",
"tokio-rustls",
]
[[package]]
@@ -794,7 +770,7 @@ dependencies = [
[[package]]
name = "lux"
version = "0.1.0"
version = "0.1.3"
dependencies = [
"lsp-server",
"lsp-types",
@@ -843,23 +819,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "native-tls"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nibble_vec"
version = "0.1.0"
@@ -905,50 +864,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -1203,15 +1118,15 @@ dependencies = [
"http",
"http-body",
"hyper",
"hyper-tls",
"hyper-rustls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
@@ -1219,15 +1134,30 @@ dependencies = [
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"winreg",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rusqlite"
version = "0.31.0"
@@ -1252,7 +1182,19 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring",
"rustls-webpki",
"sct",
]
[[package]]
@@ -1264,6 +1206,16 @@ dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -1298,15 +1250,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "schannel"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -1314,26 +1257,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.6.0"
name = "sct"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"bitflags 2.10.0",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a"
dependencies = [
"core-foundation-sys",
"libc",
"ring",
"untrusted",
]
[[package]]
@@ -1521,7 +1451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"core-foundation",
"system-configuration-sys",
]
@@ -1545,7 +1475,7 @@ dependencies = [
"getrandom 0.4.1",
"once_cell",
"rustix",
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -1619,16 +1549,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-postgres"
version = "0.7.16"
@@ -1655,6 +1575,16 @@ dependencies = [
"whoami",
]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -1750,6 +1680,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.8"
@@ -1941,6 +1877,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "whoami"
version = "2.1.1"

View File

@@ -1,6 +1,6 @@
[package]
name = "lux"
version = "0.1.0"
version = "0.1.4"
edition = "2021"
description = "A functional programming language with first-class effects, schema evolution, and behavioral types"
license = "MIT"
@@ -13,7 +13,7 @@ lsp-types = "0.94"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rand = "0.8"
reqwest = { version = "0.11", features = ["blocking", "json"] }
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls"] }
tiny_http = "0.12"
rusqlite = { version = "0.31", features = ["bundled"] }
postgres = "0.19"

367
PACKAGES.md Normal file
View File

@@ -0,0 +1,367 @@
# Lux Package Ecosystem Plan
## Current State
### Stdlib (built-in)
| Module | Coverage |
|--------|----------|
| String | Comprehensive (split, join, trim, indexOf, replace, etc.) |
| List | Good (map, filter, fold, head, tail, concat, range, find, any, all, take, drop) |
| Option | Basic (map, flatMap, getOrElse, isSome, isNone) |
| Result | Basic (map, flatMap, getOrElse, isOk, isErr) |
| Math | Basic (abs, min, max, sqrt, pow, floor, ceil, round) |
| Json | Comprehensive (parse, stringify, get, typed extractors, constructors) |
| File | Good (read, write, append, exists, delete, readDir, isDir, mkdir) |
| Console | Good (print, read, readLine, readInt) |
| Process | Good (exec, execStatus, env, args, exit, cwd) |
| Http | Basic (get, post, put, delete, setHeader) |
| HttpServer | Basic (listen, accept, respond) |
| Time | Minimal (now, sleep) |
| Random | Basic (int, float, bool) |
| Sql | Good (SQLite: open, query, execute, transactions) |
| Postgres | Good (connect, query, execute, transactions) |
| Schema | Niche (versioned data migration) |
| Test | Good (assert, assertEqual, assertTrue) |
| Concurrent | Experimental (spawn, await, yield, cancel) |
| Channel | Experimental (create, send, receive) |
### Registry (pkgs.lux) - 3 packages
| Package | Version | Notes |
|---------|---------|-------|
| json | 1.0.0 | Wraps stdlib Json with convenience functions (getPath, getString, etc.) |
| http-client | 0.1.0 | Wraps stdlib Http with JSON helpers, URL encoding |
| testing | 0.1.0 | Wraps stdlib Test with describe/it structure |
---
## Gap Analysis
### What's Missing vs Other Languages
Compared to ecosystems like Rust/cargo, Go, Python, Elm, Gleam:
| Category | Gap | Impact | Notes |
|----------|-----|--------|-------|
| **Collections** | No HashMap, Set, Queue, Stack | Critical | List-of-pairs with O(n) lookup is the only option |
| **Sorting** | No List.sort or List.sortBy | High | Must implement insertion sort manually |
| **Date/Time** | Only `Time.now()` (epoch ms), no parsing/formatting | High | blu-site does string-based date formatting manually |
| **Markdown** | No markdown parser | High | blu-site has 300+ lines of hand-rolled markdown |
| **XML/RSS** | No XML generation | High | Can't generate RSS feeds or sitemaps |
| **Regex** | No pattern matching on strings | High | Character-by-character scanning required |
| **Path** | No file path utilities | Medium | basename/dirname manually reimplemented |
| **YAML/TOML** | No config file parsing (beyond JSON) | Medium | Frontmatter parsing is manual |
| **Template** | No string templating | Medium | HTML built via raw string concatenation |
| **URL** | No URL parsing/encoding | Medium | http-client has basic urlEncode but no parser |
| **Crypto** | No hashing (SHA256, etc.) | Medium | Can't do checksums, content hashing |
| **Base64** | No encoding/decoding | Low | Needed for data URIs, some auth |
| **CSV** | No CSV parsing | Low | Common data format |
| **UUID** | No UUID generation | Low | Useful for IDs |
| **Logging** | No structured logging | Low | Just Console.print |
| **CLI** | No argument parsing library | Low | Manual arg handling |
### What Should Be Stdlib vs Package
**Should be stdlib additions** (too fundamental to be packages):
- HashMap / Map type (requires runtime support)
- List.sort / List.sortBy (fundamental operation)
- Better Time module (date parsing, formatting)
- Regex (needs runtime/C support for performance)
- Path module (cross-platform file path handling)
**Should be packages** (application-level, opinionated, composable):
- markdown
- xml
- rss/atom
- frontmatter
- template
- csv
- crypto
- ssg (static site generator framework)
---
## Priority Package Plans
Ordered by what unblocks blu-site fixes first, then general ecosystem value.
---
### Package 1: `markdown` (Priority: HIGHEST)
**Why:** The 300-line markdown parser in blu-site's main.lux is general-purpose code that belongs in a reusable package. It's also the most complex part of blu-site and has known bugs (e.g., `### ` inside list items renders literally).
**Scope:**
```
markdown/
lux.toml
lib.lux # Public API: parse, parseInline
src/
inline.lux # Inline parsing (bold, italic, links, images, code)
block.lux # Block parsing (headings, lists, code blocks, blockquotes, hr)
types.lux # AST types (optional - could emit HTML directly)
```
**Public API:**
```lux
// Convert markdown string to HTML string
pub fn toHtml(markdown: String): String
// Convert inline markdown only (no blocks)
pub fn inlineToHtml(text: String): String
// Escape HTML entities
pub fn escapeHtml(s: String): String
```
**Improvements over current blu-site code:**
- Fix heading-inside-list-item rendering (`- ### Title` should work)
- Support nested lists (currently flat only)
- Support reference-style links `[text][ref]`
- Handle edge cases (empty lines in code blocks, nested blockquotes)
- Proper HTML entity escaping in more contexts
**Depends on:** Nothing (pure string processing)
**Estimated size:** ~400-500 lines of Lux
---
### Package 2: `xml` (Priority: HIGH)
**Why:** Needed for RSS/Atom feed generation, sitemap.xml, and robots.txt generation. General-purpose XML builder that doesn't try to parse XML (which would need regex), just emits it.
**Scope:**
```
xml/
lux.toml
lib.lux # Public API: element, document, serialize
```
**Public API:**
```lux
type XmlNode =
| Element(String, List<XmlAttr>, List<XmlNode>)
| Text(String)
| CData(String)
| Comment(String)
| Declaration(String, String) // version, encoding
type XmlAttr =
| Attr(String, String)
// Build an XML element
pub fn element(tag: String, attrs: List<XmlAttr>, children: List<XmlNode>): XmlNode
// Build a text node (auto-escapes)
pub fn text(content: String): XmlNode
// Build a CDATA section
pub fn cdata(content: String): XmlNode
// Serialize XML tree to string
pub fn serialize(node: XmlNode): String
// Serialize with XML declaration header
pub fn document(version: String, encoding: String, root: XmlNode): String
// Convenience: self-closing element
pub fn selfClosing(tag: String, attrs: List<XmlAttr>): XmlNode
```
**Depends on:** Nothing
**Estimated size:** ~150-200 lines
---
### Package 3: `rss` (Priority: HIGH)
**Why:** Directly needed for blu-site's #6 priority fix (add RSS feed). Builds on `xml` package.
**Scope:**
```
rss/
lux.toml # depends on xml
lib.lux # Public API: feed, item, toXml, toAtom
```
**Public API:**
```lux
type FeedInfo =
| FeedInfo(String, String, String, String, String)
// title, link, description, language, lastBuildDate
type FeedItem =
| FeedItem(String, String, String, String, String, String)
// title, link, description, pubDate, guid, categories (comma-separated)
// Generate RSS 2.0 XML string
pub fn toRss(info: FeedInfo, items: List<FeedItem>): String
// Generate Atom 1.0 XML string
pub fn toAtom(info: FeedInfo, items: List<FeedItem>): String
```
**Depends on:** `xml`
**Estimated size:** ~100-150 lines
---
### Package 4: `frontmatter` (Priority: HIGH)
**Why:** blu-site has ~50 lines of fragile frontmatter parsing. This is a common need for any content-driven Lux project. The current parser uses `String.indexOf(line, ": ")` which breaks on values containing `: `.
**Scope:**
```
frontmatter/
lux.toml
lib.lux # Public API: parse
```
**Public API:**
```lux
type FrontmatterResult =
| FrontmatterResult(List<(String, String)>, String)
// key-value pairs, remaining body
// Parse frontmatter from a string (--- delimited YAML-like header)
pub fn parse(content: String): FrontmatterResult
// Get a value by key from parsed frontmatter
pub fn get(pairs: List<(String, String)>, key: String): Option<String>
// Get a value or default
pub fn getOrDefault(pairs: List<(String, String)>, key: String, default: String): String
// Parse a space-separated tag string into a list
pub fn parseTags(tagString: String): List<String>
```
**Improvements over current blu-site code:**
- Handle values with `: ` in them (only split on first `: `)
- Handle multi-line values (indented continuation)
- Handle quoted values with embedded newlines
- Strip quotes from values consistently
**Depends on:** Nothing
**Estimated size:** ~100-150 lines
---
### Package 5: `path` (Priority: MEDIUM)
**Why:** blu-site manually implements `basename` and `dirname`. Any file-processing Lux program needs these. Tiny but universally useful.
**Scope:**
```
path/
lux.toml
lib.lux
```
**Public API:**
```lux
// Get filename from path: "/foo/bar.txt" -> "bar.txt"
pub fn basename(p: String): String
// Get directory from path: "/foo/bar.txt" -> "/foo"
pub fn dirname(p: String): String
// Get file extension: "file.txt" -> "txt", "file" -> ""
pub fn extension(p: String): String
// Remove file extension: "file.txt" -> "file"
pub fn stem(p: String): String
// Join path segments: join("foo", "bar") -> "foo/bar"
pub fn join(a: String, b: String): String
// Normalize path: "foo//bar/../baz" -> "foo/baz"
pub fn normalize(p: String): String
// Check if path is absolute
pub fn isAbsolute(p: String): Bool
```
**Depends on:** Nothing
**Estimated size:** ~80-120 lines
---
### Package 6: `sitemap` (Priority: MEDIUM)
**Why:** Directly needed for blu-site's #9 priority fix. Simple package that generates sitemap.xml.
**Scope:**
```
sitemap/
lux.toml # depends on xml
lib.lux
```
**Public API:**
```lux
type SitemapEntry =
| SitemapEntry(String, String, String, String)
// url, lastmod (ISO date), changefreq, priority
// Generate sitemap.xml string
pub fn generate(entries: List<SitemapEntry>): String
// Generate a simple robots.txt pointing to the sitemap
pub fn robotsTxt(sitemapUrl: String): String
```
**Depends on:** `xml`
**Estimated size:** ~50-70 lines
---
### Package 7: `ssg` (Priority: LOW - future)
**Why:** Once markdown, frontmatter, rss, sitemap, and path packages exist, the remaining logic in blu-site's main.lux is generic SSG framework code: read content dirs, parse posts, sort by date, generate section indexes, generate tag pages, copy static assets. This could be extracted into a framework package that other Lux users could use to build their own static sites.
**This should wait** until the foundation packages above are stable and battle-tested through blu-site usage.
---
## Non-Package Stdlib Improvements Needed
These gaps are too fundamental to be packages and should be added to the Lux language itself:
### HashMap (Critical)
Every package above that needs key-value lookups (frontmatter, xml attributes, etc.) is working around the lack of HashMap with `List<(String, String)>`. This is O(n) per lookup and makes code verbose. A stdlib `Map` module would transform the ecosystem.
### List.sort / List.sortBy (High)
blu-site implements insertion sort manually. Every content-driven app needs sorting. This should be a stdlib function.
### Time.format / Time.parse (High)
blu-site manually parses "2025-01-15" by substring extraction and maps month numbers to names. A proper date/time library (even just ISO 8601 parsing and basic formatting) would help every package above.
---
## Implementation Order
```
Phase 1 (unblock blu-site fixes):
1. markdown - extract from blu-site, fix bugs, publish
2. frontmatter - extract from blu-site, improve robustness
3. path - tiny, universally useful
4. xml - needed by rss and sitemap
Phase 2 (complete blu-site features):
5. rss - depends on xml
6. sitemap - depends on xml
Phase 3 (ecosystem growth):
7. template - string templating (mustache-like)
8. csv - data processing
9. cli - argument parsing
10. ssg - framework extraction from blu-site
```
Each package should be developed in its own directory under `~/src/`, published to the git.qrty.ink registry, and tested by integrating it into blu-site.

View File

@@ -2,15 +2,22 @@
A functional programming language with first-class effects, schema evolution, and behavioral types.
## Vision
## Philosophy
Most programming languages treat three critical concerns as afterthoughts:
**Make the important things visible.**
1. **Effects** — What can this code do? (Hidden, untraceable, untestable)
2. **Data Evolution** — Types change, data persists. (Manual migrations, runtime failures)
3. **Behavioral Properties** — Is this idempotent? Does it terminate? (Comments and hope)
Most languages hide what matters most: what code can do (effects), how data changes over time (schema evolution), and what guarantees functions provide (behavioral properties). Lux makes all three first-class, compiler-checked language features.
Lux makes these first-class language features. The compiler knows what your code does, how your data evolves, and what properties your functions guarantee.
| Principle | What it means |
|-----------|--------------|
| **Explicit over implicit** | Effects in types — see what code does |
| **Composition over configuration** | No DI frameworks — effects compose naturally |
| **Safety without ceremony** | Type inference + explicit signatures where they matter |
| **Practical over academic** | Familiar syntax, ML semantics, no monads |
| **One right way** | Opinionated formatter, integrated tooling, built-in test framework |
| **Tools are the language** | `lux fmt/lint/check/test/compile` — one binary, not seven tools |
See [docs/PHILOSOPHY.md](./docs/PHILOSOPHY.md) for the full philosophy with language comparisons and design rationale.
## Core Principles

38
build.rs Normal file
View File

@@ -0,0 +1,38 @@
use std::path::PathBuf;
fn main() {
// Capture the absolute C compiler path at build time so the binary is self-contained.
// This is critical for Nix builds where cc/gcc live in /nix/store paths.
let cc_path = std::env::var("CC").ok()
.filter(|s| !s.is_empty())
.and_then(|s| resolve_absolute(&s))
.or_else(|| find_in_path("cc"))
.or_else(|| find_in_path("gcc"))
.or_else(|| find_in_path("clang"))
.unwrap_or_default();
println!("cargo:rustc-env=LUX_CC_PATH={}", cc_path);
println!("cargo:rerun-if-env-changed=CC");
println!("cargo:rerun-if-env-changed=PATH");
}
/// Resolve a command name to its absolute path by searching PATH.
fn find_in_path(cmd: &str) -> Option<String> {
let path_var = std::env::var("PATH").ok()?;
for dir in path_var.split(':') {
let candidate = PathBuf::from(dir).join(cmd);
if candidate.is_file() {
return Some(candidate.to_string_lossy().into_owned());
}
}
None
}
/// If the path is already absolute and exists, return it. Otherwise search PATH.
fn resolve_absolute(cmd: &str) -> Option<String> {
let p = PathBuf::from(cmd);
if p.is_absolute() && p.is_file() {
return Some(cmd.to_string());
}
find_in_path(cmd)
}

449
docs/PHILOSOPHY.md Normal file
View File

@@ -0,0 +1,449 @@
# The Lux Philosophy
## In One Sentence
**Make the important things visible.**
## The Three Pillars
Most programming languages hide the things that matter most in production:
1. **What can this code do?** — Side effects are invisible in function signatures
2. **How does data change over time?** — Schema evolution is a deployment problem, not a language one
3. **What guarantees does this code provide?** — Properties like idempotency live in comments and hope
Lux makes all three first-class, compiler-checked language features.
---
## Core Principles
### 1. Explicit Over Implicit
Every function signature tells you what it does:
```lux
fn processOrder(order: Order): Receipt with {Database, Email, Logger}
```
You don't need to read the body, trace call chains, or check documentation. The signature *is* the documentation. Code review becomes: "should this function really send emails?"
**What this means in practice:**
- Effects are declared in types, not hidden behind interfaces
- No dependency injection frameworks — just swap handlers
- No mocking libraries — test with different effect implementations
- No "spooky action at a distance" — if a function can fail, its type says so
**How this compares:**
| Language | Side effects | Lux equivalent |
|----------|-------------|----------------|
| JavaScript | Anything, anywhere, silently | `with {Console, Http, File}` |
| Python | Implicit, discovered by reading code | Effect declarations in signature |
| Java | Checked exceptions (partial), DI frameworks | Effects + handlers |
| Go | Return error values (partial) | `with {Fail}` or `Result` |
| Rust | `unsafe` blocks, `Result`/`Option` | Effects for I/O, Result for values |
| Haskell | Monad transformers (explicit but heavy) | Effects (explicit and lightweight) |
| Koka | Algebraic effects (similar) | Same family, more familiar syntax |
### 2. Composition Over Configuration
Things combine naturally without glue code:
```lux
// Multiple effects compose by listing them
fn sync(id: UserId): User with {Database, Http, Logger} = ...
// Handlers compose by providing them
run sync(id) with {
Database = postgres(conn),
Http = realHttp,
Logger = consoleLogger
}
```
No monad transformers. No middleware stacks. No factory factories. Effects are sets; they union naturally.
**What this means in practice:**
- Functions compose with `|>` (pipes)
- Effects compose by set union
- Types compose via generics and ADTs
- Tests compose by handler substitution
### 3. Safety Without Ceremony
The type system catches errors at compile time, but doesn't make you fight it:
```lux
// Type inference keeps code clean
let x = 42 // Int, inferred
let names = ["Alice", "Bob"] // List<String>, inferred
// But function signatures are always explicit
fn greet(name: String): String = "Hello, {name}"
```
**The balance:**
- Function signatures: always annotated (documentation + API contract)
- Local bindings: inferred (reduces noise in implementation)
- Effects: declared or inferred (explicit at boundaries, lightweight inside)
- Behavioral properties: opt-in (`is pure`, `is total` — add when valuable)
### 4. Practical Over Academic
Lux borrows from the best of programming language research, but wraps it in familiar syntax:
```lux
// This is algebraic effects. But it reads like normal code.
fn main(): Unit with {Console} = {
Console.print("What's your name?")
let name = Console.readLine()
Console.print("Hello, {name}!")
}
```
Compare with Haskell's equivalent:
```haskell
main :: IO ()
main = do
putStrLn "What's your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
```
Both are explicit about effects. Lux chooses syntax that reads like imperative code while maintaining the same guarantees.
**What this means in practice:**
- ML-family semantics, C-family appearance
- No monads to learn (effects replace them)
- No category theory prerequisites
- The learning curve is: functions → types → effects (days, not months)
### 5. One Right Way
Like Go and Python, Lux favors having one obvious way to do things:
- **One formatter** (`lux fmt`) — opinionated, not configurable, ends all style debates
- **One test framework** (built-in `Test` effect) — no framework shopping
- **One way to handle effects** — declare, handle, compose
- **One package manager** (`lux pkg`) — integrated, not bolted on
This is a deliberate rejection of the JavaScript/Ruby approach where every project assembles its own stack from dozens of competing libraries.
### 6. Tools Are Part of the Language
The compiler, linter, formatter, LSP, package manager, and test runner are one thing, not seven:
```bash
lux fmt # Format
lux lint # Lint (with --explain for education)
lux check # Type check + lint
lux test # Run tests
lux compile # Build a binary
lux serve # Serve files
lux --lsp # Editor integration
```
This follows Go's philosophy: a language is its toolchain. The formatter knows the AST. The linter knows the type system. The LSP knows the effects. They're not afterthoughts.
---
## Design Decisions and Their Reasons
### Why algebraic effects instead of monads?
Monads are powerful but have poor ergonomics for composition. Combining `IO`, `State`, and `Error` in Haskell requires monad transformers — a notoriously difficult concept. Effects compose naturally:
```lux
// Just list the effects you need. No transformers.
fn app(): Unit with {Console, File, Http, Time} = ...
```
### Why not just `async/await`?
`async/await` solves one effect (concurrency). Effects solve all of them: I/O, state, randomness, failure, concurrency, logging, databases. One mechanism, universally applicable.
### Why require function type annotations?
Three reasons:
1. **Documentation**: Every function signature is self-documenting
2. **Error messages**: Inference failures produce confusing errors; annotations localize them
3. **API stability**: Changing a function body shouldn't silently change its type
### Why an opinionated formatter?
Style debates waste engineering time. `gofmt` proved that an opinionated, non-configurable formatter eliminates an entire category of bikeshedding. `lux fmt` does the same.
### Why immutable by default?
Mutable state is the root of most concurrency bugs and many logic bugs. Immutability makes code easier to reason about. When you need state, the `State` effect makes it explicit and trackable.
### Why behavioral types?
Properties like "this function is idempotent" or "this function always terminates" are critical for correctness but typically live in comments. Making them part of the type system means:
- The compiler can verify them (or generate property tests)
- Callers can require them (`where F is idempotent`)
- They serve as machine-readable documentation
---
## Comparison with Popular Languages
### JavaScript / TypeScript (SO #1 / #6 by usage)
| Aspect | JavaScript/TypeScript | Lux |
|--------|----------------------|-----|
| **Type system** | Optional/gradual (TS) | Required, Hindley-Milner |
| **Side effects** | Anywhere, implicit | Declared in types |
| **Testing** | Mock libraries (Jest, etc.) | Swap effect handlers |
| **Formatting** | Prettier (configurable) | `lux fmt` (opinionated) |
| **Package management** | npm (massive ecosystem) | `lux pkg` (small ecosystem) |
| **Paradigm** | Multi-paradigm | Functional-first |
| **Null safety** | Optional chaining (partial) | `Option<T>`, no null |
| **Error handling** | try/catch (unchecked) | `Result<T, E>` + `Fail` effect |
| **Shared** | Familiar syntax, first-class functions, closures, string interpolation |
**What Lux learns from JS/TS:** Familiar syntax matters. String interpolation, arrow functions, and readable code lower the barrier to entry.
**What Lux rejects:** Implicit `any`, unchecked exceptions, the "pick your own adventure" toolchain.
### Python (SO #4 by usage, #1 most desired)
| Aspect | Python | Lux |
|--------|--------|-----|
| **Type system** | Optional (type hints) | Required, static |
| **Side effects** | Implicit | Explicit |
| **Performance** | Slow (interpreted) | Faster (compiled to C) |
| **Syntax** | Whitespace-significant | Braces/keywords |
| **Immutability** | Mutable by default | Immutable by default |
| **Tooling** | Fragmented (black, ruff, mypy, pytest...) | Unified (`lux` binary) |
| **Shared** | Clean syntax philosophy, "one way to do it", readability focus |
**What Lux learns from Python:** Readability counts. The Zen of Python's emphasis on one obvious way to do things resonates with Lux's design.
**What Lux rejects:** Dynamic typing, mutable-by-default, fragmented tooling.
### Rust (SO #1 most admired)
| Aspect | Rust | Lux |
|--------|------|-----|
| **Memory** | Ownership/borrowing (manual) | Reference counting (automatic) |
| **Type system** | Traits, generics, lifetimes | ADTs, effects, generics |
| **Side effects** | Implicit (except `unsafe`) | Explicit (effect system) |
| **Error handling** | `Result<T, E>` + `?` | `Result<T, E>` + `Fail` effect |
| **Performance** | Zero-cost, systems-level | Good, not systems-level |
| **Learning curve** | Steep (ownership) | Moderate (effects) |
| **Pattern matching** | Excellent, exhaustive | Excellent, exhaustive |
| **Shared** | ADTs, pattern matching, `Option`/`Result`, no null, immutable by default, strong type system |
**What Lux learns from Rust:** ADTs with exhaustive matching, `Option`/`Result` instead of null/exceptions, excellent error messages, integrated tooling (cargo model).
**What Lux rejects:** Ownership complexity (Lux uses GC/RC instead), lifetimes, `unsafe`.
### Go (SO #13 by usage, #11 most admired)
| Aspect | Go | Lux |
|--------|-----|-----|
| **Type system** | Structural, simple | HM inference, ADTs |
| **Side effects** | Implicit | Explicit |
| **Error handling** | Multiple returns (`val, err`) | `Result<T, E>` + effects |
| **Formatting** | `gofmt` (opinionated) | `lux fmt` (opinionated) |
| **Tooling** | All-in-one (`go` binary) | All-in-one (`lux` binary) |
| **Concurrency** | Goroutines + channels | `Concurrent` + `Channel` effects |
| **Generics** | Added late, limited | First-class from day one |
| **Shared** | Opinionated formatter, unified tooling, practical philosophy |
**What Lux learns from Go:** Unified toolchain, opinionated formatting, simplicity as a feature, fast compilation.
**What Lux rejects:** Verbose error handling (`if err != nil`), no ADTs, no generics (historically), nil.
### Java / C# (SO #7 / #8 by usage)
| Aspect | Java/C# | Lux |
|--------|---------|-----|
| **Paradigm** | OOP-first | FP-first |
| **Effects** | DI frameworks (Spring, etc.) | Language-level effects |
| **Testing** | Mock frameworks (Mockito, etc.) | Handler swapping |
| **Null safety** | Nullable (Java), nullable ref types (C#) | `Option<T>` |
| **Boilerplate** | High (getters, setters, factories) | Low (records, inference) |
| **Shared** | Static typing, generics, pattern matching (recent), established ecosystems |
**What Lux learns from Java/C#:** Enterprise needs (database effects, HTTP, serialization) matter. Testability is a first-class concern.
**What Lux rejects:** OOP ceremony, DI frameworks, null, boilerplate.
### Haskell / OCaml / Elm (FP family)
| Aspect | Haskell | Elm | Lux |
|--------|---------|-----|-----|
| **Effects** | Monads + transformers | Cmd/Sub (Elm Architecture) | Algebraic effects |
| **Learning curve** | Steep | Moderate | Moderate |
| **Error messages** | Improving | Excellent | Good (aspiring to Elm-quality) |
| **Practical focus** | Academic-leaning | Web-focused | General-purpose |
| **Syntax** | Unique | Unique | Familiar (C-family feel) |
| **Shared** | Immutability, ADTs, pattern matching, type inference, no null |
**What Lux learns from Haskell:** Effects must be explicit. Types must be powerful. Purity matters.
**What Lux learns from Elm:** Error messages should teach. Tooling should be integrated. Simplicity beats power.
**What Lux rejects (from Haskell):** Monad transformers, academic syntax, steep learning curve.
### Gleam / Elixir (SO #2 / #3 most admired, 2025)
| Aspect | Gleam | Elixir | Lux |
|--------|-------|--------|-----|
| **Type system** | Static, HM | Dynamic | Static, HM |
| **Effects** | No special tracking | Implicit | First-class |
| **Concurrency** | BEAM (built-in) | BEAM (built-in) | Effect-based |
| **Error handling** | `Result` | Pattern matching on tuples | `Result` + `Fail` effect |
| **Shared** | Friendly errors, pipe operator, functional style, immutability |
**What Lux learns from Gleam:** Friendly developer experience, clear error messages, and pragmatic FP resonate with developers.
---
## Tooling Philosophy Audit
### Does the linter follow the philosophy?
**Yes, strongly.** The linter embodies "make the important things visible":
- `could-be-pure`: Nudges users toward declaring purity — making guarantees visible
- `could-be-total`: Same for termination
- `unnecessary-effect-decl`: Keeps effect signatures honest — don't claim effects you don't use
- `unused-variable/import/function`: Keeps code focused — everything visible should be meaningful
- `single-arm-match` / `manual-map-option`: Teaches idiomatic patterns
The category system (correctness > suspicious > idiom > style > pedantic) reflects the philosophy of being practical, not academic: real bugs are errors, style preferences are opt-in.
### Does the formatter follow the philosophy?
**Yes, with one gap.** The formatter is opinionated and non-configurable, matching the "one right way" principle. It enforces consistent style across all Lux code.
**Gap:** `max_width` and `trailing_commas` are declared in `FormatConfig` but never used. This is harmless but inconsistent — either remove the unused config or implement line wrapping.
### Does the type checker follow the philosophy?
**Yes.** The type checker embodies every core principle:
- Effects are tracked and verified in function types
- Behavioral properties are checked where possible
- Error messages include context and suggestions
- Type inference reduces ceremony while maintaining safety
---
## What Could Be Improved
### High-value additions (improve experience significantly, low verbosity cost)
1. **Pipe-friendly standard library**
- Currently: `List.map(myList, fn(x: Int): Int => x * 2)`
- Better: Allow `myList |> List.map(fn(x: Int): Int => x * 2)`
- Many languages (Elixir, F#, Gleam) make the pipe operator the primary composition tool. If the first argument of stdlib functions is always the data, pipes become natural. This is a **library convention**, not a language change.
- **LLM impact:** Pipe chains are easier for LLMs to generate and read — linear data flow with no nesting.
- **Human impact:** Reduces cognitive load. Reading left-to-right matches how humans think about data transformation.
2. **Exhaustive `match` warnings for non-enum types**
- The linter warns about `wildcard-on-small-enum`, but could also warn when a match on `Option` or `Result` uses a wildcard instead of handling both cases explicitly.
- **Both audiences:** Prevents subtle bugs where new variants are silently caught by `_`.
3. **Error message improvements toward Elm quality**
- Current errors show the right information but could be more conversational and suggest fixes more consistently.
- Example improvement: When a function is called with wrong argument count, show the expected signature and highlight which argument is wrong.
- **LLM impact:** Structured error messages with clear "expected X, got Y" patterns are easier for LLMs to parse and fix.
- **Human impact:** Friendly errors reduce frustration, especially for beginners.
4. **`let ... else` for fallible destructuring**
- Rust's `let ... else` pattern handles the "unwrap or bail" case elegantly:
```lux
let Some(value) = maybeValue else return defaultValue
```
- Currently requires a full `match` expression for this common pattern.
- **Both audiences:** Reduces boilerplate for the most common Option/Result handling pattern.
5. **Trait/typeclass system for overloading**
- Currently `toString`, `==`, and similar operations are built-in. A trait system would let users define their own:
```lux
trait Show<T> { fn show(value: T): String }
impl Show<User> { fn show(u: User): String = "User({u.name})" }
```
- **Note:** This exists partially. Expanding it would enable more generic programming without losing explicitness.
- **LLM impact:** Traits provide clear, greppable contracts. LLMs can generate trait impls from examples.
### Medium-value additions (good improvements, some verbosity cost)
6. **Named arguments or builder pattern for records**
- When functions take many parameters, the linter already warns at 5+. Named arguments or record-punning would help:
```lux
fn createUser({ name, email, age }: UserConfig): User = ...
createUser({ name: "Alice", email: "alice@ex.com", age: 30 })
```
- **Trade-off:** Adds syntax, but the linter already pushes users toward records for many params.
7. **Async/concurrent effect sugar**
- The `Concurrent` effect exists but could benefit from syntactic sugar:
```lux
let (a, b) = concurrent {
fetch("/api/users"),
fetch("/api/posts")
}
```
- **Trade-off:** Adds syntax, but concurrent code is important enough to warrant it.
8. **Module-level documentation with `///` doc comments**
- The `missing-doc-comment` lint exists, but the doc generation system could be enhanced with richer doc comments that include examples, parameter descriptions, and effect documentation.
- **LLM impact:** Structured documentation is the single highest-value feature for LLM code understanding.
### Lower-value or risky additions (consider carefully)
9. **Type inference for function return types**
- Would reduce ceremony: `fn double(x: Int) = x * 2` instead of `fn double(x: Int): Int = x * 2`
- **Risk:** Violates the "function signatures are documentation" principle. A body change could silently change the API. Current approach is the right trade-off.
10. **Operator overloading**
- Tempting for numeric types, but quickly leads to the C++ problem where `+` could mean anything.
- **Risk:** Violates "make the important things visible" — you can't tell what `a + b` does.
- **Better:** Keep operators for built-in numeric types. Use named functions for everything else.
11. **Macros**
- Powerful but drastically complicate tooling, error messages, and readability.
- **Risk:** Rust's macro system is powerful but produces some of the worst error messages in the language.
- **Better:** Solve specific problems with language features (effects, generics) rather than a general metaprogramming escape hatch.
---
## The LLM Perspective
Lux has several properties that make it unusually well-suited for LLM-assisted programming:
1. **Effect signatures are machine-readable contracts.** An LLM reading `fn f(): T with {Database, Logger}` knows exactly what capabilities to provide when generating handler code.
2. **Behavioral properties are verifiable assertions.** `is pure`, `is idempotent` give LLMs clear constraints to check their own output against.
3. **The opinionated formatter eliminates style ambiguity.** LLMs don't need to guess indentation, brace style, or naming conventions — `lux fmt` handles it.
4. **Exhaustive pattern matching forces completeness.** LLMs that generate `match` expressions are reminded by the compiler when they miss cases.
5. **Small, consistent standard library.** `List.map`, `String.split`, `Option.map` — uniform `Module.function` convention is easy to learn from few examples.
6. **Effect-based testing needs no framework knowledge.** An LLM doesn't need to know Jest, pytest, or JUnit — just swap handlers.
**What would help LLMs more:**
- Structured error output (JSON mode) for programmatic error fixing
- Example-rich documentation that LLMs can learn patterns from
- A canonical set of "Lux patterns" (like Go's proverbs) that encode best practices in memorable form
---
## Summary
Lux's philosophy can be compressed to five words: **Make the important things visible.**
This manifests as:
- **Effects in types** — see what code does
- **Properties in types** — see what code guarantees
- **Versions in types** — see how data evolves
- **One tool for everything** — see how to build
- **One format for all** — see consistent style
The language is in the sweet spot between Haskell's rigor and Python's practicality, with Go's tooling philosophy and Elm's developer experience aspirations. It doesn't try to be everything — it tries to make the things that matter most in real software visible, composable, and verifiable.

View File

@@ -14,6 +14,7 @@
pkgs = import nixpkgs { inherit system overlays; };
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rust-analyzer" ];
targets = [ "x86_64-unknown-linux-musl" ];
};
in
{
@@ -22,8 +23,8 @@
rustToolchain
cargo-watch
cargo-edit
pkg-config
openssl
# Static builds
pkgsStatic.stdenv.cc
# Benchmark tools
hyperfine
poop
@@ -43,7 +44,7 @@
printf "\n"
printf " \033[1;35m \033[0m\n"
printf " \033[1;35m \033[0m\n"
printf " \033[1;35m \033[0m v0.1.0\n"
printf " \033[1;35m \033[0m v0.1.4\n"
printf "\n"
printf " Functional language with first-class effects\n"
printf "\n"
@@ -61,18 +62,47 @@
packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "lux";
version = "0.1.0";
version = "0.1.4";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.openssl ];
doCheck = false;
};
# Benchmark scripts
packages.static = let
muslPkgs = import nixpkgs {
inherit system;
crossSystem = {
config = "x86_64-unknown-linux-musl";
isStatic = true;
};
};
in muslPkgs.rustPlatform.buildRustPackage {
pname = "lux";
version = "0.1.4";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
CARGO_BUILD_TARGET = "x86_64-unknown-linux-musl";
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
doCheck = false;
postInstall = ''
$STRIP $out/bin/lux 2>/dev/null || true
'';
};
apps = {
# Release automation
release = {
type = "app";
program = toString (pkgs.writeShellScript "lux-release" ''
exec ${self}/scripts/release.sh "$@"
'');
};
# Benchmark scripts
# Run hyperfine benchmark comparison
bench = {
type = "app";

View File

@@ -1,284 +0,0 @@
# Lux Language Issues — blu-site SSG Project
Issues discovered while building a static site generator in Lux. Each entry includes a reproduction case and the workaround used.
---
## Issue 1: `Html.render()` / `Html.document()` not available in interpreter
**Category**: Missing feature
**Severity**: High
The Html module only works in the JS backend, not the interpreter. All HTML must be generated via string concatenation.
**Workaround**: Build all HTML as concatenated strings.
---
## Issue 2: String interpolation `{}` has no escape mechanism
**Category**: Missing feature → **Fixed** (added `\{` and `\}` escape sequences)
**Severity**: High
Any `{` inside a string literal triggers interpolation mode, and there was no way to include literal braces. This breaks when generating JavaScript like `function() { ... }` or JSON like `{}`.
**Reproduction**:
```lux
let s = "function() { return 1; }" // Parse error: treats { } as interpolation
```
**Fix**: Added `\{` and `\}` as escape sequences in the lexer (src/lexer.rs). Also fixed the formatter (src/formatter.rs) to re-escape `{` and `}` in string literals so that `lux fmt` doesn't break escaped braces. Now:
```lux
let s = "function() \{ return 1; \}" // Works, produces literal braces
```
---
## Issue 3: Module-qualified constructors not supported in pattern matching
**Category**: Parser limitation
**Severity**: High
Cannot use `module.Constructor` in match patterns. The parser expects `=>` but finds `.`.
**Reproduction**:
```lux
import frontmatter
let result = frontmatter.parse(content);
match result {
frontmatter.ParseResult(front, body) => ... // ERROR: Expected =>, found .
}
```
**Workaround**: Consolidated everything into a single file. Alternatively, use `import foo.*` (wildcard imports), though this can cause name conflicts.
---
## Issue 4: Tuple field access (`.0`, `.1`) not supported
**Category**: Missing feature
**Severity**: High
The parser's field access only works on Records (with named fields), not tuples. The `.` operator expects an identifier.
**Reproduction**:
```lux
let pair = ("hello", 42);
let x = pair.0; // ERROR: Expected identifier
```
**Workaround**: Use ADTs (algebraic data types) with accessor functions:
```lux
type Pair = | Pair(String, Int)
fn first(p: Pair): String = match p { Pair(a, _) => a }
```
---
## Issue 5: Multi-line function arguments cause parse errors
**Category**: Parser limitation
**Severity**: High
Function call arguments cannot span multiple lines. The parser sees `\n` where it expects `,` or `)`.
**Reproduction**:
```lux
let result = someFunction(
arg1,
arg2
); // ERROR: Expected ,, found \n
```
**Workaround**: Keep all function arguments on a single line, or use `let` bindings for intermediate values:
```lux
let a = arg1;
let b = arg2;
let result = someFunction(a, b);
```
---
## Issue 6: Multi-line lambdas in function call arguments fail
**Category**: Parser limitation
**Severity**: High
Lambda bodies that span multiple lines inside function call arguments cause parse errors.
**Reproduction**:
```lux
List.map(items, fn(x: Int): Int =>
x + 1 // ERROR: Expected ,, found \n
);
```
**Workaround**: Either keep lambdas on one line or extract to named functions:
```lux
fn addOne(x: Int): Int = x + 1
List.map(items, addOne)
```
---
## Issue 7: Effectful callbacks in `List.map`/`forEach`/`fold`
**Category**: Type checker limitation
**Severity**: Medium
The type checker requires pure callbacks for higher-order List functions, but effectful callbacks are often needed (e.g., reading files in a map operation).
**Reproduction**:
```lux
fn readFile(path: String): String with {File} = File.read(path)
let contents = List.map(paths, fn(p: String): String => readFile(p));
// ERROR: Effect mismatch: expected {File}, got {}
```
**Workaround**: Use manual recursion instead of List.map/forEach:
```lux
fn mapRead(paths: List<String>): List<String> with {File} =
match List.head(paths) {
None => [],
Some(p) => {
let content = readFile(p);
match List.tail(paths) {
Some(rest) => List.concat([content], mapRead(rest)),
None => [content]
}
}
}
```
---
## Issue 8: `String.indexOf` and `String.lastIndexOf` missing from type checker
**Category**: Type checker gap → **Fixed**
**Severity**: Medium
These functions exist in the interpreter but were not registered in the type checker, causing "Module 'String' has no member" errors.
**Fix**: Added type registrations in src/types.rs:
- `String.indexOf(String, String) -> Option<Int>`
- `String.lastIndexOf(String, String) -> Option<Int>`
---
## Issue 9: No `List.sort` / `List.sortBy`
**Category**: Missing feature
**Severity**: Medium
No built-in sorting for lists. Must implement manually.
**Workaround**: Insertion sort via `List.fold`:
```lux
fn sortByDateDesc(items: List<Page>): List<Page> =
List.fold(items, [], sortInsert)
fn insertByDate(sorted: List<Page>, item: Page): List<Page> = {
match List.head(sorted) {
None => [item],
Some(first) =>
if pgDate(item) >= pgDate(first) then
List.concat([item], sorted)
else
match List.tail(sorted) {
Some(rest) => List.concat([first], insertByDate(rest, item)),
None => [first, item]
}
}
}
```
---
## Issue 10: No HashMap/Map type
**Category**: Missing feature
**Severity**: Medium
No associative data structure available. Tag grouping required manual list scanning.
**Workaround**: Use `List<(key, value)>` with `List.filter`/`List.find` for lookups. O(n) per lookup.
---
## Issue 11: No regex support
**Category**: Missing feature
**Severity**: Medium
Inline markdown parsing (bold, italic, links, code) required manual character-by-character scanning with index tracking.
**Workaround**: Recursive `processInlineFrom(text, i, len, acc)` function scanning character by character.
---
## Issue 12: No multiline string literals
**Category**: Missing feature
**Severity**: Medium
HTML templates require very long single-line strings with many `+` concatenations and `\"` escapes.
**Workaround**: Concatenate multiple single-line strings with `+`.
---
## Issue 13: `Json.parse` returns `Result<Json, String>`
**Category**: Documentation gap
**Severity**: Low
Not immediately obvious that `Json.parse` wraps the result in `Ok`/`Err`. Error message "Json.get expects Json, got Constructor" is confusing.
**Workaround**: Pattern match on `Ok(j)` / `Err(_)`:
```lux
let json = match Json.parse(raw) { Ok(j) => j, Err(_) => ... };
```
---
## Issue 14: No `File.copy`
**Category**: Missing feature
**Severity**: Low
Must shell out to copy files/directories.
**Workaround**: `Process.exec("cp -r static/* _site/")`.
---
## Issue 15: No file globbing
**Category**: Missing feature
**Severity**: Low
Must manually scan directories and filter by extension.
**Workaround**: `File.readDir(dir)` + `List.filter(entries, fn(e) => String.endsWith(e, ".md"))`.
---
## Summary
| # | Issue | Severity | Status |
|---|-------|----------|--------|
| 1 | Html module interpreter-only | High | Open |
| 2 | No `\{` `\}` string escapes | High | **Fixed** |
| 3 | Module-qualified pattern matching | High | Open |
| 4 | Tuple field access `.0` `.1` | High | Open |
| 5 | Multi-line function arguments | High | Open |
| 6 | Multi-line lambdas in calls | High | Open |
| 7 | Effectful callbacks in List HOFs | Medium | Open |
| 8 | String.indexOf/lastIndexOf types | Medium | **Fixed** |
| 9 | No List.sort | Medium | Open |
| 10 | No HashMap/Map | Medium | Open |
| 11 | No regex | Medium | Open |
| 12 | No multiline strings | Medium | Open |
| 13 | Json.parse return type docs | Low | Open |
| 14 | No File.copy | Low | Open |
| 15 | No file globbing | Low | Open |

View File

@@ -1,9 +0,0 @@
{
"siteTitle": "Brandon Lucas",
"siteUrl": "https://blu.cx",
"author": "Brandon Lucas",
"description": "Personal website of Brandon Lucas. Bitcoin Lightning developer at Voltage, privacy advocate at Payjoin. Writing about software, history, and philosophy.",
"contentDir": "content",
"outputDir": "_site",
"staticDir": "static"
}

View File

@@ -1,60 +0,0 @@
// Site configuration loaded from config.json
pub type SiteConfig =
| SiteConfig(String, String, String, String, String, String, String)
// title url author desc contentDir outputDir staticDir
pub fn loadConfig(path: String): SiteConfig with {File} = {
let raw = File.read(path);
let json = match Json.parse(raw) { Ok(j) => j, Err(_) => match Json.parse("\{\}") { Ok(j2) => j2 } };
let title = match Json.get(json, "siteTitle") {
Some(v) => match Json.asString(v) { Some(s) => s, None => "Site" },
None => "Site"
};
let url = match Json.get(json, "siteUrl") {
Some(v) => match Json.asString(v) { Some(s) => s, None => "" },
None => ""
};
let author = match Json.get(json, "author") {
Some(v) => match Json.asString(v) { Some(s) => s, None => "" },
None => ""
};
let desc = match Json.get(json, "description") {
Some(v) => match Json.asString(v) { Some(s) => s, None => "" },
None => ""
};
let contentDir = match Json.get(json, "contentDir") {
Some(v) => match Json.asString(v) { Some(s) => s, None => "content" },
None => "content"
};
let outputDir = match Json.get(json, "outputDir") {
Some(v) => match Json.asString(v) { Some(s) => s, None => "_site" },
None => "_site"
};
let staticDir = match Json.get(json, "staticDir") {
Some(v) => match Json.asString(v) { Some(s) => s, None => "static" },
None => "static"
};
SiteConfig(title, url, author, desc, contentDir, outputDir, staticDir)
}
pub fn getTitle(c: SiteConfig): String =
match c { SiteConfig(t, _, _, _, _, _, _) => t }
pub fn getUrl(c: SiteConfig): String =
match c { SiteConfig(_, u, _, _, _, _, _) => u }
pub fn getAuthor(c: SiteConfig): String =
match c { SiteConfig(_, _, a, _, _, _, _) => a }
pub fn getDescription(c: SiteConfig): String =
match c { SiteConfig(_, _, _, d, _, _, _) => d }
pub fn getContentDir(c: SiteConfig): String =
match c { SiteConfig(_, _, _, _, cd, _, _) => cd }
pub fn getOutputDir(c: SiteConfig): String =
match c { SiteConfig(_, _, _, _, _, od, _) => od }
pub fn getStaticDir(c: SiteConfig): String =
match c { SiteConfig(_, _, _, _, _, _, sd) => sd }

View File

@@ -1,261 +0,0 @@
---
title: Micropayments and the Lightning Network
description: Content monetization on the internet is broken. Credit card fees prohibit the ability to make small payments for individual pieces of content. Creators have to choose between invading user privacy by selling their data, annoying them with a barrage of ads, or putting up paywalls and forcing them to sign up for subscriptions to continue. The Lightning Network offers a new, alternative monetization strategy in the form of micropayments.
date: 2023-03-10
tags: bitcoin
---
_Content monetization on the internet is broken. Credit card fees prohibit the ability to make small payments for individual pieces of content. Creators have to choose between invading user privacy by selling their data, annoying them with a barrage of ads, or putting up paywalls and forcing them to sign up for subscriptions to continue. The Lightning Network offers a new, alternative monetization strategy in the form of micropayments._
Check out the [Voltage](https://voltage.cloud/) presentation on this topic [here](https://youtu.be/6Vq6foKst54).
_If you're interested in learning more about the history of micropayments, check out [Amber Case's article](https://caseorganic.medium.com/who-killed-the-micropayment-a-history-ec9e6eb39d05) and the [Open Index Protocol's episode](https://www.youtube.com/watch?v=9_6cLtpf3AE)_
## Before Bitcoin
Before the World Wide Web, the internet, or even the personal computer revolution, the concept of making tiny payments in exchange for digital content had already been conceptualized. The term “micropayment” can be traced back to Ted Nelson as early as 1960, when he began brainstorming his ideas for what a globally accessible, computer-based repository of human knowledge could look like, and coined many other terms and ideas that are used on the web today, like “hypertext” and “hypermedia”. While often failing to implement his own vision for the web (and being vocally regretful of how the web took shape), others took many of his ideas and built them into what we know it as today.
In 1992, Tim Berners-Lee, creator of HTTP and HTML, released his [second version of HTTP](https://www.w3.org/Protocols/HTTP/HTRESP.html) and the first reference to the status codes now in common use. Among them was a code that Berners-Lee and others thought would one day be used to pay for digital content: <code>402 payment required</code>. Sadly, this status code is officially [“reserved for future use”](https://www.rfc-editor.org/rfc/rfc7231#section-6.5.2), as the various attempts to make micropayments on the web, from its inception, failed to come to fruition. Over 30 years after the invention of the internet, were still waiting for one of its major original visions to be fulfilled.
Berners-Lee founded the World Wide Web Consortium (W3C) in 1994 to guide the development of the web, and micropayments were a major consideration from the start. In 1995, Phillip Hallam-Baker, who wrote a number of RFCs on internet security, drafted the [Micro Payment Transfer Protocol (MPTP)](https://www.w3.org/TR/WD-mptp), which never seems to have been implemented. It offers a number of insights on the nature of micropayments that are as relevant today as they were at the founding of the internet:
> There is a large interest in payment systems which support charging relatively small amounts for a unit of information. Here the speed and cost of processing payments are critical factors in assessing a schemes usability. Fast user response is essential if the user is to be encouraged to make a large number of purchases.
A critical limitation of MPTP, however, was that a third party, referred to as a _broker_, was explicitly required by the protocol.
At the time, there was no way to make a digital payment without a trusted mediator, so any attempt at a protocol for micropayments had to be developed with some sort of custodianship of the money in mind.
The W3C continued to push for micropayments for a while, in 1998 publishing an [overview](https://www.w3.org/ECommerce/Micropayments/) of micropayments and suggesting MPTP as a practical approach and noting:
> Micropayments have to be suitable for the sale of non-tangible goods over the Internet […] With the rising importance of intangible (e.g. information) goods in global economies and their instantaneous delivery at negligible cost, "conventional" payment methods tend to be more expensive than the actual product.
This echoes Hallam-Bakers second major concern of transaction costs imposed by technological or overhead costs of available payment mechanisms. His first concern, the need for a “fast user response”, is often overlooked in discussions about the viability of micropayments. But one astute observer, who would later go on to have a strong influence on the development of Bitcoin, was already thinking in depth about this problem. Nick Szabo, creator of the concept of smart contracts and Bitgold, wrote [“Micropayments and Mental Transaction Costs”](https://nakamotoinstitute.org/static/docs/micropayments-and-mental-transaction-costs.pdf) in 1999. Szabo states that the _primary_ reason for the failure of micropayments to take hold was not due to technological or overhead costs. It was due to the _mental transaction cost_ experienced by the user of having to decide whether a purchase was worth the price for every interaction on the internet.
> […] customer mental transaction costs are significant and ubiquitous, so much so that in real world circumstance cognitive costs usually well outweigh technological costs, and indeed technological resources are best applied towards the objective of reducing cognitive costs. […] Micropayment technology efforts which stress technological savings over cognitive savings will become irrelevant.
A web based on micropayments implies frequent purchases, which implies decision fatigue. It is likely the case that for most micropayments, the mental transaction cost incurred by having to make constant choices about purchases likely outweighs the value of what theyre paying for.
Large companies like Compaq and IBM, and startups like Pay2See, Millicent, iPin, and others, tried their hand at reducing these technological and mental transaction costs for micropayments in the early days, and it was still an assumption that the concept would be a lasting feature from the start.
Perhaps the most notable of these companies, which would go on to have a lasting impact on the Bitcoin community, was DigiCash, spearheaded by David Chaum. Chaum had already formalized many ideas for a [blockchain-like data structure](https://nakamotoinstitute.org/literature/computer-systems-by-mutually-suspicious-groups/) and [secure digital cash](https://nakamotoinstitute.org/literature/blind-signatures/) in 1982 before going on to found DigiCash in 1989. DigiCash implemented Chaums proposals, allowing users to withdraw money (called “eCash”) from a bank and make untraceable digital micropayments with it. Unfortunately, only one bank ever implemented eCash, and in 1998 the company went bankrupt.
Other micropayment efforts were also dissolving around the same time, and the W3C itself [closed their contributions to micropayment activity](https://www.w3.org/ECommerce/Micropayments/) in 1998.
The dot-com crash was in full swing, and micropayments were one of the ideas that crashed the hardest. It was a good time to be a critic. In 2000, writer Clay Shirky wrote [“The Case Against Micropayments”](http://mx.thirdvisit.co.uk/2002/10/04/theacaseaagainstamicropayments/) in which he boldly declared:
> Micropayment systems have not failed because of poor implementation; they have failed because they are a bad idea. Furthermore, since their weakness is systemic, they will continue to fail in the future.
His primary argument for their fundamental flaw was not technological or infrastructural — but instead echoed Nick Szabo one year prior: decision fatigue. He goes on:
> In particular, users want predictable and simple pricing. Micropayments, meanwhile, waste the users mental effort in order to conserve cheap resources, by creating many tiny, unpredictable transactions. Micropayments thus create in the mind of the user both anxiety and confusion, characteristics that users have not heretofore been known to actively seek out.
Shirky went on to predict three methods of payment that would become dominant on the web which did not suffer from the decision fatigue problem: aggregation (bundling low-value things into a single higher-value transaction), subscription, and subsidy (getting someone other than the user to pay for the content — today this has manifested itself as the ad model).
By the end of the dot-com crash, Shirkys predictions were looking more salient. Credit cards, with their infrastructure costs prohibiting payments of less than $1, had become the de-facto method of payment, and passion for the micropayments project was losing steam. What was a self-evident and exciting prospect for the webs future faded against the backdrop of its increasingly centralized, surveilled, and ad-driven successor — Web 2.0.
## Bitcoin and the Centralized Web
> We have to trust them with our privacy, trust them not to let identity thieves drain our accounts. Their massive overhead costs make micropayments impossible.
— Satoshi Nakamoto
> The idea driving 402 was that its obvious that support for payments should be a first-class concept on the web and its obvious that there should be a lot of direct commerce taking place on the web […] In fact, what emerged is a single dominant business model which is advertising. That leads to a lot of centralisation, because you get the highest cost per clicks and with the largest platforms.
— John Collison, President of Stripe
Satoshi Nakamoto released the Bitcoin whitepaper near the end of 2008, aptly timed in the midst of the U.S. Housing Crisis. He released the original code for it shortly after. Bitcoin was a massive breakthrough, both in the history of computer science and the history of money, and spurred a new wave of interest in the possibilities of the internet. For the first time, there was a permissionless way to transfer value with an _internet native_ currency, without all the inelegant, bloated infrastructure needed to use credit cards.
For awhile, the price of bitcoin was so small that some people did advocate its use for micropayment systems, despite Satoshi admitting that it wasnt (yet) a great solution to that problem:
> Bitcoin isn't currently practical for very small micropayments. Not for things like pay per search or per page view without an aggregating mechanism, not things needing to pay less than 0.01.
But the limits imposed by fees didnt stop people from dreaming about new possibilities enabled by it. Marc Andreessen, creator of the first popular web browser, [gave the examples](https://archive.nytimes.com/dealbook.nytimes.com/2014/01/21/why-bitcoin-matters/) of content monetization and fighting spam:
> One reason media businesses such as newspapers struggle to charge for content is because they need to charge either all (pay the entire subscription fee for all the content) or nothing (which then results in all those terrible banner ads everywhere on the web). All of a sudden, with Bitcoin, there is an economically viable way to charge arbitrarily small amounts of money per article, or per section, or per hour, or per video play, or per archive access, or per news alert.
This statement is not true today of course (at least, as far as Layer 1 is concerned), but the fees in 2014 were low enough that it was actually possible to build around the concept of micropayments. One interesting project built around this time was [Bitmonet](https://www.pcworld.com/article/447463/news-junkies-opensource-project-links-bitcoin-with-publishers.html), which allowed users to choose their subscription level by paying 10 cents for just one article, 15 cents for an hour of unlimited access to the website, or 20 cents for a day pass. Unfortunately transaction fees are [no longer low enough](https://bitinfocharts.com/comparison/bitcoin-transactionfees.html#alltime) to be able to allow for arbitrarily small micropayments, and although the issue was clearly on Satoshis mind from the earliest days of Bitcoin, it wasnt designed to specifically address the micropayment problem.
### Subscriptions and Ads
Shirkys predictions for content monetization were pretty accurate, particularly when it came to subscription and ad models.
In the ad model, content was subsidized by an advertiser, typically through a third-party. From 2014 to 2022, Google and Facebook essentially [held a duopoly](https://www.ft.com/content/4ff64604-a421-422c-9239-0ca8e5133042) over the online ad market as the third-party mediator between advertisers and content creators. The two companies (and in reality, most of big tech) [collected enormous amounts of personal information](https://www.security.org/resources/data-tech-companies-have/) and simply asked their users to entrust them with the safety of their data, despite numerous breaches. This [information is used](https://www.forbes.com/sites/forbestechcouncil/2022/02/16/what-does-big-tech-actually-do-with-your-data/?sh=6a79a424515f) to show targeted ads for products people are more likely to buy. Companies often refer to this model as “free — with ads”. But in reality, users do pay a price. The ad model forces users to trade two things in exchange for the content:
1. Their data, forcing them to give it to third parties, which, as Nick Szabo famously stated, are [security holes](https://nakamotoinstitute.org/literature/trusted-third-parties/).
2. Their attention. There is a reason for the phrase “pay attention”, and its well illustrated here. More time users spend on a site with ads, the more money advertisers, ad platforms, and the content creator makes. Thus a creator is economically incentivized to show as many ads as they possibly can without annoying the user so much that they leave the platform. The currency of the “free with ads” web is your attention. You are the product.
Whats clear in the ad model is that the _consumer_ becomes a second-class citizen. Since a layer of abstraction exists between a creators revenue and the end user, making a great user experience is not the highest priority. And as more and more [consumers use ad-blockers](https://backlinko.com/ad-blockers-users), content creators are forced to get more aggressive with ads, making using the web a more degraded experience for everyone.
Subscriptions also rose in popularity. Customers showed they were much more willing to pay for bulk access to licensed content like movies and music on a regular basis instead of paying to own individual songs. Although a more honest business model, they can also be very problematic when they are the sole option for payment. And in recent years, as these services become increasingly competitive, more and more people find themselves suffering from subscription fatigue. The inability to access just one (or a few) specific news articles, movies, or songs at any given time forces a suboptimal choice of trying to pay in bulk and optimize the most content for a given subscription.
Take streaming services, for example. Today there are so many streaming services, all battling for content licensing, that users end up paying multiple subscriptions to try to capture a greater subset of the movies and TV shows they want. But all they _really_ want is to watch an incredibly small subset of whats offered by any given service. When they pick a service for a movie or show they want, it often doesnt stay long, and will be bounced around unpredictably from company to company as licensing expires and gets updated.
News articles are another example. Companies like The New York Times or The Economist entice readers by allowing them to read just a few seconds of an article before blocking the content with a subscription paywall. Even more so with newspapers than with movies, customers are far more likely to want to pay a small amount for a single article of their choice than for a package deal to articles they dont want.
While subscriptions offer a more straightforward approach than ads, using them in practice often makes for what is an increasingly costly and stressful management game.
When Clay Shirky was writing about the issues of mental transaction costs, he was writing before the mental costs of subscriptions and ads began to weigh down on people as they do today. Bitcoin gave a solution to the problem of an internet-native currency, but slow processing and high fees quickly became a prohibitive problem for a system supporting micropayments. One more major innovation was needed before true implementations of micropayments technology could take off.
## The Lightning Network
> A decentralized system is proposed whereby transactions are sent over a network of micropayment channels (a.k.a. payment channels or transaction channels) whose transfer of value occurs off-blockchain. — Joseph Poon and Thaddeus Dryja, The Bitcoin Lightning Network
In February 2015, [“The Bitcoin Lightning Network”](https://lightning.network/docs/) was published, representing perhaps the biggest advancement in Bitcoin since its inception. Through the clever use of Bitcoin properties, a series of payment “channels” could be constructed such that transactions did not have to be announced to the blockchain but could still retain the lack of trust in a third party, using the Bitcoin blockchain itself for dispute resolution. Payments can either be made directly between two channel partners, or they can be routed across a series of partners, so long as there exists a path of connected partners between sender and recipient. Because of this, it was no longer necessary to pay fees to miners for every transaction, and payments could be settled near-instantly. Since the primary reason micropayments werent viable on Bitcoin in the first place was due to high fees and slow transaction processing, the Lightning Network (abbreviated LN or called “lightning” for short) opened up a whole new world of possibilities that are just beginning to be explored.
### Methods
The smallest unit of bitcoin is a satoshi (often abbreviated to sat). One bitcoin is equal to 100,000,000 sat. At [current conversion rates](https://www.coinbase.com/converter/sats/usd), 1 satoshi is equal to $0.00023, meaning that the smallest payments that can be made (assuming no routing fees) are currently at a granularity of about _two ten-thousandths_ of a penny, a finer granularity than was ever seriously considered by the webs early founders. Technically, even finer granularity payments of a thousandth of a satoshi (called millisatoshi, abbreviated msat) can be made within the confines of the Lightning Network, but any sub-satoshi amounts will be rounded down if the money is ever moved back to the blockchain. Therefore, all of the methods of payment discussed below can be used to make micropayments, but different mechanisms allow for a variety of [novel payment schemes](https://bitcoinmagazine.com/business/can-bitcoin-fix-micropayments) to be set up. I wont go into the technical details of how the Lightning Network works here. My intention is to provide a high level overview of some ways micropayments can be made with it, and hopefully to spark peoples imaginations to the possibilities.
### Invoice
The most basic way to make a payment on the Lightning Network is through the [Bolt-11](https://www.bolt11.org/) invoice. This is an encoded string of information containing everything necessary for the payment to be made. The invoice is generated by the recipient and then sent to the payee. Typically, from a users perspective, the payment string is encoded as a QR code which the payee can scan.
_Benefits:_
- Least abstractions from a technological level
- Available by default in LN implementations
- Payments occur directly node to node
_Drawbacks:_
- A recipient having to first send an invoice can make for awkward UX
- Both nodes must be online for a payment to be made
- Invoices are non-reusable and must be freshly generated every time
Most of the ways to pay on LN today either simply use Bolt-11 invoices or some abstraction of them under the hood.
### Keysend
An alternative to invoices are [keysend](https://voltage.cloud/blog/bitcoin-lightning-network/keysend-payments-explained-voltage-technical-series/) payments, which allow for payments directly between two nodes without an invoice. Senders only need a nodes public key. One crucial downside to keysend is the lack of a proof of payment, making it unsuitable for merchants or other systems that rely on proof of purchase. For content monetization, however, keysend pairs nicely with the concept of [value4value](https://value4value.info/): a model which gives content away freely and asks the user to give back the value they felt they received in turn (and requires no proof of payment).
_Benefits:_
- No invoices required, payment can be sent instantly
- Available by default in LND, Eclair, and Core-Lightning (but must be manually turned on)
- Direct node to node payments
_Drawbacks:_
- No proof of payment limits scope of use
_Possible micropayment use cases:_
- Streaming sats (Boosting) while listening/watching/reading content
- Micro tipping
- [Boostagrams](https://podnews.net/article/how-to-earn-bitcoin-from-your-podcast)
_Examples:_
- [Podcast Index](https://podcastindex.org/)/Podcasting 2.0
### LNURL
[LNURL](https://github.com/lnurl/luds) is a set of standards designed to expand the capabilities of LN by providing a specification for “out-of-band” or third party communication between nodes. LNURL standardizes a whole set of LN-based interactions for developers. For example, the [LNURL-Pay](https://github.com/lnurl/luds/blob/luds/06.md) spec defines how services can create static QR codes that can be paid multiple times. Under the hood, the service is generating a new invoice every time a payment attempt is made.
_Benefits:_
- Static pay/withdrawal links
- LN based authentication schemes
- [Many more](https://github.com/lnurl/luds)
_Drawbacks:_
- LNURL operates “out-of-band” meaning that there is a service between LN nodes creating invoices on a nodes behalf. This can mean that the service must be trusted to stay online in addition to the nodes involved in the payment.
_Possible micropayment use cases:_
- Private, printable, reusable QR code for donations on content
- Reusability allows for payments to be made even when embedded in images and videos
- Withdrawals for content creators whove accumulated sats on a given platform
- Donations to protestors: Anyone can hold up a sign a protest movement captured on video, and sympathizers can easily donate. To quote Marc Andreessen from [“Why Bitcoin Matters”](https://a16z.com/2014/01/21/why-bitcoin-matters-nyt/) in 2014:
> Think about the implications for protest movements. Today protesters want to get on TV so people learn about their cause. Tomorrow theyll want to get on TV because thats how theyll raise money, by literally holding up signs that let people anywhere in the world who sympathize with them send them money on the spot. Bitcoin is a financial technology dream come true for even the most hardened anticapitalist political organizer.
_Examples:_
- [Cointrain](https://satoshkey.com/site/cointrain)
- [Lightning addresses](https://lightningaddress.com/)
- [List of Awesome LNURL things](https://github.com/lnurl/awesome-lnurl)
### LSAT
> Many speculate that [the 402 status code] was intended to be
> used by some sort of digital cash or micropayment scheme, which didn't yet
> exist at the time of the initial HTTP specification drafting.
>
> However, several decades later, we _do_ have a widely used digital cash system:
> Bitcoin! On top of that, a new network oriented around micropayments has also
> arisen: the Lightning Network.
>
> — Olaoluwa Osuntokun, [LSAT: Authentication and Payments for the Lightning-Native Web](https://lightning.engineering/posts/2020-03-30-lsat/)
[Lightning Service Authentication Tokens (LSATs)](https://lsat.tech/) are a protocol created for authentication and paid APIs that leverage the forgotten HTTP error code 402 discussed earlier in conjunction with the lightning network. LSATs can be thought of like reusable tickets that need to be paid for to gain access to a particular resource. But the powerful thing here is that _rules_ can be applied to modify access to resources on a given site, and the tickets can be reused on subsequent visits to the site.
_Benefits:_
- Extremely flexible
- Reusable
- Allows for highly granular and fine-grained access to digital resources
- Finally makes use of 402 error code!
_Drawbacks:_
- Difficult version control
- No current methods for efficient management (Could be solved by browser extension such as [Alby](https://github.com/getAlby/lightning-browser-extension/issues/9))
- Underutilized/underdeveloped despite being around for awhile
_Possible micropayment use cases:_
- Pay for single video/podcast/song/article and remember it on subsequent visits
- Metered subscriptions of your choice
- Custom expiration dates on content
_Examples:_
- [Aperture](https://docs.lightning.engineering/the-lightning-network/lsat/aperture)
- [Boltwall](https://medium.com/tierion/boltwall-middleware-for-lightning-payments-authorization-e3a1dbb54a4c)
- [lsat-js](https://github.com/Tierion/lsat-js)
- [Alby Mixtape](https://mixtape.albylabs.com/)
- [Nakaphoto](https://nakaphoto.vercel.app/)
### WebLN
[WebLN](https://www.webln.guide/introduction/readme) is a standard for abstracting away LN interactions by relying on a client (such as a browser extension) to communicate with a WebLN enabled site. The client must have the ability to interact with the users lightning node. For example, a site might use WebLN to request a payment from the user when a button is clicked. When clicked, the users client extension will popup, providing whatever payment details the WebLN enabled site sent to the client in a simple to use interface. WebLN greatly simplifies the payment flow for browser based interactions, where users no longer have to pull out their phones to make QR code based payments.
_Benefits:_
- Better UX
- Simple implementation
- No third party required
- Compatible with LNURL
_Drawbacks:_
- Browser client required
- Few clients in use
- If user doesnt have a client installed, fallback options (such as LNURL or invoices) must be provided
_Possible micropayment use cases:_
- Anything an invoice, LNURL, or Keysend can do!
### Projects Using Micropayments
Finally, here is a short list of projects using micropayments that I found interesting:
- [micropay.ai](https://micropay.ai/): Micropayments to generate images using DALLE-2
- [WebLN Experiments](https://webln.twentyuno.net/): A collection of various WebLN demos
- [LN VPN](https://lnvpn.net/): Pay as you go VPN service, pay as little as $0.1 for 1 hour
- [LNCal](https://lncal.com/): Allow people to pay you in bitcoin to schedule meetings
- [BTCMap](https://btcmap.org/): Map of locations accepting bitcoin. Uses Boosting for favored payments
- [Lightning Video](https://lightning.video/?type=top&nsfw=false&all=true): Like Youtube, but uses bitcoin micropayments instead of ads
- [Sats4Likes](https://www.sats4likes.com/): Get micropaid for micro tasks
- [PeerTube Lightning Plugin](https://p2ptube.us/): Pay PeerTube creators with LN
- [Stacker News](https://stacker.news/): Reddit-style discussions with micropayment tipping
- [Bookmark.org](https://bookmark.org/): Pay to archive links
- [THNDR Games](https://www.thndr.games/): Earn bitcoin while playing games
See [bolt.fun](http://bolt.fun), the [Lightning App Directory](https://dev.lightning.community/lapps/index.html), or [Lightning Landscape](https://www.lightning-landscape.net/projects) for a larger list of projects being built on Lightning.
## Conclusion
Micropayments have arrived on the web, and this article just scratches the surface of whats possible. While there are significant technological and mental barriers remaining, I hope the above examples show how far weve already come -- and how much further we can go -- for a cleaner, freer, and more expressive internet.

View File

@@ -1,382 +0,0 @@
---
title: Payjoin for a Better Bitcoin Future
description: Payjoin is a protocol that uses a simple, clever trick in the way Bitcoin creates transactions to solve many of its problems at once.
date: 2023-10-31
tags: payjoin bitcoin
---
Payjoin is a protocol that uses a simple, clever trick in the way Bitcoin creates transactions to solve many of its problems at once. It was created to solve Bitcoin's biggest [privacy problem](https://decrypt.co/107376/bitcoin-privacy-problem-what-cypherpunks-are-doing), but can also assist with its [scaling problem](https://en.wikipedia.org/wiki/Bitcoin_scalability_problem), and help people [save on fees](https://github.com/orgs/payjoin/discussions/91#discussion-5439172). It's particularly compatible with Lightning Network Nodes, as it currently has a liveness requirement for the receiver, meaning they must be online at the moment they are being sent money (just like a Lightning Node). In the future, even this requirement will be eliminated and it can be used offline. It can be [easily integrated into wallets](https://geyser.fund/project/payjoin#:~:text=Payjoin%20is%20easy%20to%20integrate%2C%20but%20can%20only%20take%20off%20when%20software%20supports%20sending%20and%20receiving%20via%20the%20BIP%2078%20spec), it can be used to [open many lightning channels at once](https://github.com/payjoin/nolooking), and it's usage can be made passive, so that you can receive the benefits of Payjoin without even knowing you're using it. The privacy benefits of payjoin cascade, so that everyone would see privacy gains even if only a [minority of people used it](https://reyify.com/blog/payjoin#conclusion). And, perhaps best of all, payjoin doesn't require a hard or soft fork. It can be and is used on Bitcoin today, and it could be used on Bitcoin from the very first version of the software.
Payjoin is a derivative of [Coinjoin](https://en.bitcoin.it/wiki/CoinJoin), which is an older, more interactive protocol, meaning the user must be heavily engaged with it to use it, which necessarily drives down usability and inhibits adoption. Somehow, despite this, Coinjoin has seen much higher adoption than payjoin so far, despite the more obvious benefits and ease of use with payjoin. A complex, unclear path forward for developers inhibits adoption by wallet software.
Payjoin has been around for a few years, and given:
1. The _many_ benefits listed above
2. The ability for a passive, nonintrusive user interface
3. The simplicity for wallet providers to integrate it
Why is [adoption so low](https://en.bitcoin.it/wiki/PayJoin_adoption)?
Especially compared to the far more interactive, difficult to use, and expensive Coinjoin protocol?
In this article, we'll look at current attacks on Bitcoin privacy, the history of payjoin from the perspective of privacy, how it works and how it can provide so many benefits with no changes to Bitcoin, and the current state of adoption. If payjoin can radically improve privacy, scaling, and fee issues with Bitcoin, the low effort required for wallets to integrate it would be well worth it.
## Why Privacy Matters for Bitcoin
Before discussing the importance of payjoin, we must understand the importance of privacy. If you don't need convincing on this front, feel free to skip down to the history and explanation of payjoin.
In Western democracies, the prime importance of privacy is difficult to communicate, as its benefits still seem invisible to people. It is harder to convincingly explain to someone why privacy matters (especially in the face of higher cost or greater inconvenience) if they've never felt the often terrible consequences of bad actors having too much information about them, perhaps because it requires people to think long-term about the consequences of these invasions.
Still, privacy seems to be something more and more people want in theory, but they typically do little to achieve it unless the barriers are extremely low and convenient. Therefore, technologies that wish to protect people's privacy should be designed to do so with minimal requirements for interaction and inconvenience for the user.
### Fungibility
Privacy is not the only problem payjoin can help solve, but it is the problem it was created to solve. The lack of privacy by default on Bitcoin has long been lamented, and one taken very seriously in the Bitcoin community. Bitcoin was designed to be directly peer-to-peer and censorship resistant. But the ability to track owners of future payments once coins have been linked to an identity allows for discrimination. This destroys fungibility -- or the degree to which one of a currency's individual units are indistinguishable from another of the same unit -- which is one of the primary properties of a good money.
If buyers can be tracked, not only can coins that are currently owned by illicit actors be rejected, but coins that _have ever_ been used for illicit purposes can be marked as such and be rejected by merchants, nevermind whether the current owners obtained them through perfectly legitimate means. Imagine not being able to buy milk at the grocery store because the dollars you have were once-upon-a-time used to buy drugs, would it be fair to you to be discriminated against by having them say "your money is no good here"? Should you be punished for someone else's crime? And what would that do to how you treat those dollars? It would make them worthless as your spending power is diminished by owning them, and make some dollars (the "non-tainted" dollars) more valuable than other dollars, which of course shouldn't make sense. One dollar should always equal one dollar, no matter what individual one dollar bill is used to represent it, or the ability of the dollar to do its job of transferring value is impeded.
### The Criminal Myth
It is often said by Bitcoin and privacy detractors that privacy is for criminals. This is the familiar "if you aren't doing anything wrong, you have nothing to hide" logic, which is easily refuted:
- Few people would be okay with an internet live stream of them taking a shower or using the bathroom being broadcast. Is that because it's wrong? This is to point out that _everyone_ has something to hide, and hiding things isn't inherently wrong.
- More broadly, the government is charged with providing the legal basis for what's wrong, but that definition is always subject to change. If people aren't free to have privacy, then they really aren't free at all, as their actions will be severely restricted by perceived social, if not governmental, restrictions. People are judged and attacked for doing perfectly legal things by others all the time. [Privacy is the power to choose who you reveal yourself to](https://nakamotoinstitute.org/cypherpunk-manifesto/).
Aside from this naive and self-evidently illogical argument, criminals, unlike most law-abiding citizens, are willing to accept high costs to obtain privacy, so measures to hurt privacy-by-default hurt regular people far more than they hurt criminals. This would be the case even if governments weren't doing a poor job of actually using their new privacy-restricting measures to actually catch criminals en masse, but instead to "pick and choose" and selectively spy on the citizenry at their will. If a citizen says something those in power don't like, and what those in power don't like is always in flux, it can selectively decide to target and hurt them.
Finally, the desire for privacy isn't merely the fear of government overreach. It is a practical safety or reputational concern. If someone can find out how much money you have and where you live, how hard is it to steal from you? Now consider the number of places you entered your address, payment details, pictures, on the internet. Do you trust every one of those sites to keep your information secure? You shouldn't, because even the best fail, and criminals are paying large sums of money to hackers who exploit weaknesses for this valuable information.
### Privacy and Democracy
A prerequisite to control in any totalitarian state is _knowledge_ of the speech, access to information, and financial activity of its citizens. Without it, it can't know what to attack, shut down, or manipulate to advance its narrative or further its control. If it doesn't reliably have access to this information, it becomes difficult to target citizens on a whim. In totalitarian societies of the past, such as the Soviet Union and Nazi Germany, privacy was eroded by ensuring the indoctrination of family members who would report on unfavorable opinions in private conversations, destroying webs of trust in families. When this same erosion of privacy occurs with money, its effects can be even more powerful than with speech. Cutting off the source of funding can be a very effective means of disbanding political dissent.
### Bitcoin Privacy is Under Attack
> Never waste an opportunity afforded by a good crisis
-- Attributed to Nicollo Machiavelli
It is in the name of combating criminals, in this case Hamas terrorists, that new regulation is opportunistically being proposed to make privacy-preserving methods in Bitcoin illegal.
On October 10, 2023, an article was published by the [Wall Street Journal](https://www.wsj.com/world/middle-east/militants-behind-israel-attack-raised-millions-in-crypto-b9134b7a) reporting that Hamas acquired $130 million dollars of funding by means of cryptocurrency. One week later, senator Elizabeth Warren [wrote a letter](https://www.warren.senate.gov/imo/media/doc/2023.10.17%20Letter%20to%20Treasury%20and%20White%20House%20re%20Hamas%20crypto%20security.pdf) to President Biden urging him to address questions regarding his administration's response to the "use of cryptocurrency by terrorist organizations" by October 31, citing the WSJ article as evidence of the urgent need for such regulation. The letter obtained the signatures of 29 of the 100 senators, and 76 members of the House. Curiously, on October 19th, just two days after the letter was sent, the Financial Crimes Enforcement Network [published a proposal](https://www.fincen.gov/sites/default/files/federal_register_notices/2023-10-19/FinCEN_311MixingNPRM_FINAL.pdf) for regulating cryptocurrency mixing due to the risk of money laundering. It lists, among the methods used to obscure transactions:
> _Using programmatic or algorithmic code to coordinate, manage, or manipulate the structure of a transaction:_ This method involves the use of software that coordinates two or more persons transactions together in order to obfuscate the individual unique
> transactions by providing multiple potential outputs from a coordinated input, decreasing the probability of determining both intended persons for each unique transaction.
This definition includes Coinjoin and Payjoin, although _using algorithmic code_ is broad enough to include any transaction, and therefore justify at-will censorship.
But the WSJ article, which provided the sentiment for the letter and attempted to justify the regulation, horribly misinterpreted the data -- the actual dollar amount that was connected with Hamas was [$450,000](https://www.chainalysis.com/blog/cryptocurrency-terrorism-financing-accuracy-check/). Cryptocurrency is nowhere near a major source of Hamas' funding. Hamas itself explicitly stated that they did not want funding via Bitcoin due to its traceability.
The irony of the proposed regulation, then, is that its effects will be minimally impactful to the terrorist organization that is the justification for its existence, but maximally impactful to everyone else who wants to use Bitcoin and other cryptocurrencies.
There can be little doubt that the battle for the right to privacy in Bitcoin has arrived in the U.S., and it has predictably masked itself in the guise of national security against foreign adversaries. It's more important now than ever to understand the need for and use of privacy-preserving techniques on Bitcoin to combat the ongoing attempts to chip away at it.
> [We must defend our own privacy if we expect to have any.](https://nakamotoinstitute.org/cypherpunk-manifesto/)
## 1. How Transactions Work
To understand the need for payjoin and how it works, it's crucial to understand how transactions work. Each transaction in Bitcoin is a combination of _inputs_ and _outputs_. Outputs define which public key or "address" the bitcoin is being sent to. Inputs define which "sources" or _previous_ outputs are used in creating the new outputs for a transaction. It is helpful to think of this as an analogy to how we use different denominations of cash to pay for things. Imagine you need to pay $25 to a restaurant for dinner, plus a $5 tip for the waiter, for a total of $30 (these are your outputs, two different "chunks" of money going to two different people -- the restaurant and the waiter).
How would you pay it? Let's say you have the following bills available (these are your inputs):
- One $20
- Two $10's
- Six $5's
To construct this transaction, you could use one $20 bill and two $5 bills, one being for the waiter:
![Restaurant payment](/images/articles/payjoin-better-future/25-5-tx.webp)
Note one important way in which our cash analogy breaks down: the $20 and the $5 _merge_ into one "bill". This would be more analagous to melting individual pieces of gold into one bigger piece in order to be able to pay the exact amount required, instead of handing over multiple pieces. Bitcoin allows us to split and merge inputs however we'd like to create the appropriate output.
You might also use two $10 bills and two $5 bills like so:
![Restaurant payment](/images/articles/payjoin-better-future/10-5-tx.webp)
You could even use your six $5 bills:
![Restaurant payment](/images/articles/payjoin-better-future/5-5-tx.webp)
Before they're spent on something, the individual bitcoin "bills" you have are called _unspent transaction outputs_, or UTXOs. This name is confusing at first, but makes perfect sense if you think about it. They are the "results" (_outputs_) of transactions that haven't yet been used up by _other_ transactions. A transaction output that is _unspent_ is a transaction output that you _can spend_. Therefore, in effect, UTXOs become like the bills of cash in your wallet. After they're spent, they become inputs to another transaction's outputs (the cash in someone else's wallet), and are no longer spendable by you, but the _record_ of the fact that you spent that bill to them remains on the blockchain.
Unlike cash, for a Bitcoin transaction to be valid, we need the approval of the sender. This is done with the sender's _digital signature_, which serves as proof of their intent to spend the coin. A valid signature (that is, a signature corresponding to the address of the UTXO) needs to be present on each UTXO that the sender is trying to spend as an input to a new transaction. The presence of a signature "unlocks" the UTXO and signals that it was its owner's intent to spend it in the proposed transaction.
Here is an example of a real transaction that had 1 confirmation while writing this:
![Example of a real transaction with 1 input and three outputs, one being a fee](/images/articles/payjoin-better-future/real-transaction-1.webp)
[(Link)](https://mempool.space/tx/6a1f3e9b12a3e4f947b471a290c8c90681e1fe6e9869245dbc253b4015dc3bf6)
As we can see, the above transaction consumes one input and creates two outputs, one being the actual payment, and the other almost certainly being change sent back to the spender. A fee equal to the sum of the inputs minus outputs is also paid to the miner.
This "UTXO model" is very powerful. Since every transaction has inputs and outputs, and since the outputs of one transaction become the inputs of another transaction later, we end up with a long chain of transactions, and are able to track bitcoins' transfer of ownership from transaction to transaction all the way back to a miner. Since Bitcoin has a limited supply, and derives its crucially non-inflationary properties from this fact, it's important to be able to audit how much is in circulation or "unspent" at any given time, which the UTXO model allows for.
This is the crux of the privacy problem with Bitcoin. _Every transaction has a history_. All the bitcoin that came to you and anywhere you send it to can be _easily_ tracked. The system was explicitly designed to support this, albeit not with the intention of tracking individuals. In this system, your only true bet is to never link your identity with your public key, which, in our age of mass surveillance, can be very hard.
## The Historical Origins of Payjoin
### Satoshi's Modest Mistake
When Satoshi published the [whitepaper](https://bitcoin.org/bitcoin.pdf) in 2008, he recognized the privacy problems that came from the requirement of announcing every transaction to the public, and keeping it private.
He had two recommendations to avoid linking an identity with transactions:
1. Keep public keys anonymous
2. Don't reuse public keys
This is good advice, but for 1) it is very difficult to keep our identities completely isolated from our payments without extreme caution when making payments online. And for 2) even without public key reuse, if transactions spending from multiple keys are spent together in subsequent payments, it is not too difficult for a tracker to put together which keys belong to which person. These suggestions, even when combined, make for a very difficult to enact and imperfect privacy solution.
After these suggestions, Satoshi then makes a modest mistake, exaggerating the weakness of his own system:
> As an additional firewall, a new key pair should be used for each transaction to keep them from being linked to a common owner. Some linking is still unavoidable with multi-input transactions, which necessarily reveal that their inputs were owned by the same owner. The risk is that if the owner of a key is revealed, linking could reveal other transactions that belonged to the same owner.
Satoshi's assumption, and indeed, all the examples shown so far, presume that all the inputs in a transaction belong to the same owner. In other words, it assumes all the "bills" come from your wallet, which is a fair assumption, but one that isn't _necessarily_ true. This assumption is called the [common input ownership heuristic](https://en.bitcoin.it/wiki/Common-input-ownership_heuristic). It is almost always true for any given transaction, and it is the basis of chain surveillance.
### Coinjoin
In early 2013, Gregory Maxwell played a fun game in the [bitcointalk.org forum](https://bitcointalk.org/index.php?topic=139581.0). He provided one of his UTXOs (worth 1 BTC) and an address of his, and asked people to create new transactions using that UTXO as an input. If they proposed a transaction where they sent less than 1 BTC back to him, that meant they were trying to take from him, if more, they were giving to him. If they used the transaction to send exactly 1 BTC back to him, however, they were simply using his address in the transaction to increase privacy by making it _look_ as though it was one of the spender's UTXOs, even though it wasn't. When one of Maxwell's inputs was consumed and sent back to one of his addresses, he posted another one so someone could continue the game. From the perspective of a chain analysis company, this would have the effect of making Maxwell seem rich! Since his address was public, and many UTXOs were being used to construct transactions involving him, anyone analyzing the transactions and making the assumption that all inputs in a transaction were owned by the same person would think Maxwell was transacting with far more bitcoin than he actually was, hence the post's title: "I taint rich!".
Of course, this game wasn't really private, since Maxwell posted his address in a public forum, but it proved a very important and consequential concept. As Maxwell states:
> A lot of people mistakenly assume that when a transaction spends from multiple addresses all those addresses are owned by the same party. This is commonly the case, but it doesn't have to be so: people can cooperate to author a transaction in a secure and trustless manner.
In a [later post](https://bitcointalk.org/?topic=279249) that same year, Maxwell formalizes this idea into a concept he called Coinjoin:
> When considering the history of Bitcoin ownership one could look at transactions which spend from multiple distinct scriptpubkeys as co-joining their ownership and make an assumption: How else could the transaction spend from multiple addresses unless a common party controlled those addresses?
>
> [...]
>
> This assumption is incorrect. Usage in a single transaction does not prove common control (though it's currently pretty suggestive), and this is what makes CoinJoin possible:
>
> The signatures, one per input, inside a transaction are completely independent of each other. This means that it's possible for Bitcoin users to agree on a set of inputs to spend, and a set of outputs to pay to, and then to individually and separately sign a transaction and later merge their signatures. The transaction is not valid and won't be accepted by the network until all signatures are provided, and no one will sign a transaction which is not to their liking.
This means that, in effect, an arbitrary number of people can coordinate to create transactions each contributing and signing their own inputs, without risk of theft by others _at any point_.
He then highlights another benefit of Coinjoin transactions, which is that transactions can be _batched_ to save on fees by finding someone else who wants also wants to make a payment at the same time as you:
> The idea can also be used more casually. When you want to make a payment, find someone else who also wants to make a payment and make a joint payment together. Doing so doesn't increase privacy much, but it actually makes your transaction smaller and thus easier on the network (and lower in fees); the extra privacy is a perk.
And finally, Coinjoin is a protocol such that, if enough people use it, everyone wins, resulting in privacy gains for everyone:
> Such a transaction is externally indistinguishable from a transaction created through conventional use. Because of this, if these transactions become widespread they improve the privacy even of people who do not use them, because no longer will input co-joining be strong evidence of common control.
To provide a concrete example, say we find three people who want to do a coinjoin. Beforehand, they agree to mix 0.1 bitcoin, gaining privacy by having three equal-sized outputs to make it unclear which address owns which coins. The change addresses will be clear to an analyst, but the 0.1 values will not.
![Coinjoin Example](/images/articles/payjoin-better-future/coinjoin-tx.webp)
The privacy gains aren't necessarily very strong with only three participants, especially if the other participants de-anonymize in later transactions by tying them to their identity, but this can be mitigated with multiple rounds of coinjoin on those coins or by using larger anonymity sets.
To summarize, **_Coinjoin is a transaction created with inputs and outputs from multiple parties, such that it is difficult to tell who owns which coins after the transaction._**
For a deeper dive into how to actually create a Coinjoin and what tools are available, see [this guide](https://bitcoinmagazine.com/technical/a-comprehensive-bitcoin-coinjoin-guide).
Coinjoin is one of the most effective and widely adopted privacy solutions for Bitcoin today, but it has some major drawbacks:
1. <b>Interactivity</b>: Coinjoin requires a high degree of interaction from the participants; they need to agree to a uniform output size and must all supply their signatures within a reasonable time. High interactivity requirements create friction for users, and therefore hinder adoption by wider audiences.
2. <b>Centralized coordinators</b>: Wasabi and Whirlpool are currently the most popular methods of Coinjoining. They take fees for conducting the coordination, in addition to the miner fees paid just to participate in the transaction (since Coinjoin transactions have a lot of signature data, they are more expensive). JoinMarket is an example of a non-coordinated service, but this has the tradeoff of increased interactivity.
3. <b>Multiple rounds required for enhanced privacy</b>: To get better privacy, it is often recommended to Coinjoin multiple rounds due to the limitations of small anonymity set sizes, which costs time, increases interactivity, and costs more in fees.
4. <b>Coinjoins look different from normal transactions</b>: Coinjoin transactions have a distinctive, recognizable pattern: multiple inputs from multiple parties resulting in multiple _uniformely sized_ outputs. This means that if your coins have been identified prior to you doing the Coinjoin, the snooper will know you've Coinjoined. They may not know where the money went or what you do with it after you Coinjoin, but they know how much you had and that you did a Coinjoin in the first place.
Clearly, this is not an end-all be-all solution for Bitcoin privacy due to these limitations, especially for more passive Bitcoiners who want a privacy-by-default approach.
After a few years, a better solution was on the horizon, one that could be done without _any_ extra steps having to be taken by the transactees, was directly peer-to-peer with no centralized coordinators or marketplaces (therefore saving time and money), and looked no different from normal transactions: Payjoin.
Payjoin has been developed from a series of earlier innovations, let's take a look at those.
## BIP-21
An important user experience (UX) improvement in early Bitcoin was BIP-21. [BIP](https://github.com/bitcoin/bips/tree/master) stands for Bitcoin Improvement Proposal, and defines a set of standards that either require consensus changes to the Bitcoin protocol (e.g. hard forks or soft forks) or provide information and methods otherwise useful to interacting with Bitcoin.
BIP-21 is a standard that defines a scheme using URIs to simplify user interaction with Bitcoin, allowing them to pay by clicking links or scanning QR codes. A few query parameters, such as `amount`, `label`, and `message` are also defined so that client software can easily access and parse them for better UX. [Here is an example](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) BIP-21 URI with those parameters:
![BIP-21 Simple](/images/articles/payjoin-better-future/bip21-message.webp)
Importantly, the standard is extensible, as custom query parameters can be created and new standards can be built on top of it. For example, in the Lightning Network, the custom parameter `lightning` can be added [in addition](https://bitcoinqr.dev/) to the Bitcoin address so that the user can pay with either method:
![BIP-21 with Lightning](/images/articles/payjoin-better-future/bip21-lightning.webp)
This powerful and flexible BIP would prove useful in combination with concepts from Coinjoin.
### Pay-to-Endpoint (P2EP)
The [earliest mention](https://blog.blockstream.com/en-improving-privacy-using-pay-to-endpoint/) of the Payjoin concept I could find was by Blockstream, published in August 2018, where the article [references a workshop](https://nopara73.medium.com/pay-to-endpoint-56eb05d3cac6) from which the concept emerged. It referred to the resulting idea as Pay-to-Endpoint (P2EP), because it combined the concepts of Coinjoin and BIP-21 to allow a sender and a receiver to collaboratively contribute inputs to a transaction via a BIP-21 compliant endpoint provided by the receiver. Here is an example given in the article of what an endpoint provided by the receiver may look like:
![P2EP Example](/images/articles/payjoin-better-future/p2ep.webp)
Of note in particular is the `p2ep` parameter, whose value is an endpoint (in this case a `.onion` address, but it could just as easily be an `http://` address or any other compatible endpoint), which signals to receiving wallets that the sender would like to try a P2EP payment. If that fails, wallets should fall back to the sender paying the address normally, just using the sender's inputs.
Because the contribution of inputs are collaborative in P2EP, and don't result in the "coin-tainting" uniform outputs that Coinjoin creates, Payjoin transactions are much more difficult to identify.
The idea was a major step in the right direction, but the idea was still nascent and unformalized, and had some additional complexity to be removed.
#### Aside - Satoshi's Pay-to-IP (P2IP)
A variation of this idea, called [Pay-to-IP](https://www.reddit.com/r/Bitcoin/comments/4isxjr/comment/d30y3k4/), was actually implemented by Satoshi in the _very first_ version of Bitcoin. There were, however, major privacy concerns with this approach, and so it was abandoned in subsequent versions of Bitcoin.
### Bustapay
Later that same month, Ryan Havar emailed a [revised version](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-August/016340.html) of the idea to the Bitcoin developer mailing list, and formalized it into a BIP which he called [Bustapay](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-August/016340.html). This version simplified the initial P2EP protocol and removed some of the complexity in favor of simplicity, the idea being that simplicity would prove essential for adoption.
The Bustapay proposal still had a few major issues that needed refinement, and the protocol was not as refined as it could have been. But it was a further step in the right direction, and its focus on simplicity for the purposes of wallet integration was a key one, especially for the slow-moving and cautious Bitcoin developer ecosystem. Although Bustapay never took off, it was a final precursor to the Payjoin proposal we have today, which is ripe for wallet integration, and positive transformation of on-chain transactions.
### The Payjoin Proposal
Finally, by mid-2019, the concepts for Bustapay and P2EP were further refined and added to by Nicolas Dorier, founder of BTCPayServer, and Kukks, with the publication of [BIP-78](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki), titled "A Simple Payjoin Proposal".
With our background of the protocols leading up to payjoin, the meaning and purpose of the opening abstract of the proposal should now be clear:
> This document proposes a protocol for two parties to negotiate a coinjoin transaction during a payment between them.
This proposal laid out, in much more rigorous detail than prior methods, how to conduct a coinjoin transaction between a sender and a receiver in a way that breaks the common-input ownership heuristic that is simple, flexible, and cheap.
## How Payjoin Works
Let's say Alice wants to pay Bob 1.1 BTC, and a chain surveillance company sees a transaction like this:
![Example assuming common-input ownership heuristic](/images/articles/payjoin-better-future/regular-tx.webp)
They might assume that Alice paid Bob the 0.5 BTC output and gave the rest to herself as change, like this:
![Example that assumes the common-input ownership heuristic was used](/images/articles/payjoin-better-future/regular-tx-named.webp)
And the vast majority of the time, they'd be right! After all, normally the change is the larger amount, and 0.5 is more "round" and thus likely to be used for payment, than 1.1.
They might also wonder why she used an unnecessary input (the 0.8 and 0.3 BTC inputs would've sufficed), but they can never know for sure that this wasn't a normal transaction, and they can't know for sure why an extra UTXO was used. She could have just been consolidating a UTXO for easier management later. It _could_ be a payjoin, but even if you assumed it was, which UTXO's are Alice's and which are Bob's? It's impossible to know. Since most transactions _aren't_ payjoins, they'll probably make the faulty assumption that it wasn't.
Alice, however, is smart and wants to preserve her privacy, and she knows about payjoin, so she asks Bob to contribute one of his own inputs to the transaction. Bob agrees, so he creates a transaction that spends one (or more) of his UTXOs as an input, and proposes it to Alice. If the transaction looks good to Alice, she broadcasts the payjoin transaction to the network. In this case, the transaction actually looks like this:
![Example payjoin transaction](/images/articles/payjoin-better-future/payjoin-tx-named.webp)
If chain surveillance made the assumption that both inputs were owned by Alice (as they did in the first example, and currently do in practice), their assumptions about what both Alice and Bob own would be wrong!
It's also interesting to note that Alice and Bob conducting this transaction helps everybody gain privacy. Because, unlike coinjoin, it looks like a normal transaction, if enough people use payjoin chain surveillance will be unable to trust whether any transaction is normal. By foiling chain surveillance's attempts at spying on their transaction, Alice and Bob make _every_ transaction a little bit suspect. If there are enough people doing it, it makes them all suspect. Online privacy is often a numbers game, and the more people participate, the better the privacy.
In this example, Alice and Bob collaborated to create a transaction that used both of their inputs to preserve privacy. Now, of course, this whole process can be automated (and _is_ in practice).
In BIP-78, the process is more formally defined as follows:
1. A receiver shows the sender a BIP-21 URI with a query parameter `pj=` that refers to an endpoint/server people can send [partially signed bitcoin transactions](https://bitcoinops.org/en/topics/psbt/) (PSBTs) to. The endpoint can be HTTPs, .onion, or any other authenticated encryption protocol. For example:
![Example PJ](/images/articles/payjoin-better-future/pj.webp)
2. The sender creates a finalized, broadcastable PSBT using only their own inputs that is fully capable of making the required payment, to the receiver's endpoint. This is called the "Original PSBT".
3. The receiver modifies the PSBT to include his own inputs, signs his inputs, and gives this back to the sender. He does not modify any of the inputs or outputs of the sender. This is called the "Payjoin Proposal".
4. The sender verifies the proposal, re-signs her inputs to finalize the transaction, and broadcasts it to the network.
If something goes wrong at any point in the process, such as, for example, the receiver doesn't have any UTXOs with which to create the Payjoin Proposal, then the receiver simply broadcasts the Original PSBT and it goes though as a normal transaction. Although this uses all inputs from the same owner, again, if enough people are using payjoin, it becomes impossible to make the assumption that the two parties _didn't_ payjoin, and surveillance will simply have to assume they did and try to find other methods of tracking payments.
## Payjoin's Many Benefits
### More Broken Surveillance Heuristics
The common-input ownership heuristic is not the only privacy-destroying assumption that payjoin breaks. BIP-78 identifies two other heuristics that can be used to identify owners:
- Change identification from the `scriptPubkey`:
In Bitcoin, `scriptPubKey` is the "locking script" that specifies the conditions under which Bitcoin can be spent. It's called `scriptPubKey` because the locking condition requires a valid signature matching the public key (the address) to unlock it. In other words, only the person who controls the private key to a particular UTXO's associated public key can unlock it.
There are multiple types of `scriptPubKey`'s in use, (P2PKH, P2WPKH, P2SH, P2TR). Typically, wallets use the same `scriptPubkey` for all transactions, and therefore, the _change_ amount (the amount the sender sends back to themselves after consuming UTXOs as inputs) will likely have the same `scriptPubkey`, while outputs sent to the receiver are much more likely to have a different `scriptPubkey`. This means that UTXOs with the same `scriptPubkey` in a transaction [can be identified](https://en.bitcoin.it/wiki/Privacy) as likely belonging to the spender, assuming the outputs sent to the receiver are different.
BIP-78 specifies a method to allow the receiver to only use `scriptPubkey`'s matching the spender's, breaking the above heuristic's ability to tell which outputs are the change outputs.
- Change identification and payment from round payment amounts:
Normally, payments made to peers have round amounts since it's far more natural to transact with round numbers. If Bob is charging Alice (and they aren't basing the bitcoin price on a direct conversion to a "rounder" fiat price), he's more likely to charge her an amount like 0.00010000 as opposed to a non-rounded amount like 0.00010231, for simplicity. For transactions in which only one output is round, it's very likely the case (for now) that the non-round output is the change output.
Payjoin also allows for this metric to be broken by describing a way for the receiver to add extra round outputs when constructing the proposal.
### Asymmetric Gains by Uniting with a Bigger Crowd
As mentioned earlier, one of the main drawbacks with coinjoin from a privacy perspective is that: 1) coinjoins are easily distinguishable from other transactions and 2) few people do them relative to non-coinjoin transactions. This creates problems for the fungibility of Bitcoin, because it's possible for people to mark coins as tainted, since some may have the ludicrous perception that the desire for privacy implies malicious intent. Of course, as _most_ transactions, or even a threshold of transactions, preserve privacy, then they cease to stand out.
Payjoin appears like any other transaction, and therefore doesn't draw attention. An external observer has no reason to even look at a payjoin more closely, because the payjoin shows no effort to obfuscate the payment and change amounts.
By appearing like any other transaction, even marginal gains in payjoin adoption mean that everyone's privacy is harder to invade, because surveillance heuristics quickly become unreliable. Adam Gibson, a foundational contributor to JoinMarket and an expert in Bitcoin privacy, [sums it up well](https://reyify.com/blog/payjoin):
> If you're even reasonably careful, these PayJoin transactions will be basically indistinguishable from ordinary payments.
> [...]
> Now, here's the cool thing: suppose a small-ish uptake of this was publically observed. Let's say 5% of payments used this method. The point is that nobody will know which 5% of payments are PayJoin. That is a great achievement [...], because it means that all payments, including ones that don't use PayJoin, gain a privacy advantage!
## UTXO Consolidation
Clearly, payjoin and it's predecessors were aimed at solving privacy problems. But there is a nice ancillary benefit to using payjoin, specified right in BIP-78 itself: UTXO consolidation.
Satoshi's suggestion to use a new address for every received payment results in user's wallets having many UTXOs to manage. When these UTXOs are all used together as inputs to new transactions (and assuming it's not a coinjoin or payjoin), these transactions cost more in fees. Fees are calculated based on the size of the transaction in bytes, since block space is the scarce resource, so more inputs equals more fees for a single large transaction.
It should be noted that using payjoin for UTXO consolidation does not necessarily save on fees, because the expense of each UTXO for chain space still needs to be paid at some point. Rather, it spreads the fees out over time and gives an opportunity to batch the UTXO with the payment. Batching makes UTXO consolidation cheaper than doing it as its own transaction. It also makes for easier management of UTXOs, and less data stored on disk. In addition, it is possible for wallets to implement a way for a receiver to specify that they would like to consolidate their UTXOs when mining fees are low in advance, making UTXO consolidation an automatic and smoother process.
## Lightning and Payjoin: A Perfect Match
### Opening Lightning Channels with Payjoin
The [Lightning Network (LN)](https://lightning.network/lightning-network-paper.pdf) is a second-layer solution built on Bitcoin that takes transactions off-chain to allow for near-instant, final settlements with far lower fees, tremendously increasing transaction throughput, improving privacy, and allowing for new use cases for Bitcoin such as [micropayments](https://brandonlucas.net/articles/bitcoin/micropayments). It uses a network of payment channels between nodes to route payments from source to destination. These channels require node operators to lock up "liquidity" (bitcoin that can flow between one node and its channel partner) between their channel partners. How much bitcoin you can spend in a channel is limited by how much liquidity exists on your side of that channel.
Most of the complexity in managing a lightning node comes from opening these channels and managing the liquidity in them. Onboarding is one of the biggest pain points, since there are several steps involved. Let's say Alice wants to open up a channel with Bob and she has setup a brand-new, unfunded lightning node. Alice has to do the following:
1. Send an onchain transaction funding her newly created lightning wallet with sufficient funds to at least open the channel and wait (at least 10 minutes) for it to be confirmed.
2. Use her lightning wallet software to negotiate a channel opening transaction with Bob and wait for it to be confirmed.
At a minimum, Alice has to pay two fees and wait ~10 minutes per transaction, which can be tedious.
![Lightning Channel Open Process](/images/articles/payjoin-better-future/lightning-open-channel.webp)
Payjoin can simplify this process and help Alice save money by doing both the funding and the opening transaction at once.
In this scenario, Alice preconfigures her payjoin receiving endpoint with the details of how she'll open channels: including the amount of bitcoin and which channel partners she'll open to. Then, using a wallet with payjoin support, someone (presumably Alice) will send a payjoin proposal, negotiate the payjoin transaction with the receiving endpoint, and upon receiving the funds, the endpoint will make the necessary API calls to open a channel with Bob's node.
In other words, the sender (probably Alice in this case) will send a payjoin proposal to Alice's receiving payjoin endpoint to create a transaction that pays directly to the 2-of-2 multisignature output between Bob's node and Alice's node to construct a lightning channel between them. Which turns the process into a one-step transaction:
![Lightning Channel with Payjoin](/images/articles/payjoin-better-future/lightning-open-payjoin.webp)
One interesting thing to note is that both LN and payjoin currently have a _liveness_ requirement (though, in the case of payjoin at least, perhaps [not for long](https://gist.github.com/DanGould/243e418752fff760c9f6b23bba8a32f9)), meaning that participants must be online when transactions take place. This is fairly limiting in comparison to on-chain Bitcoin, which only requires the sender to be online _at the time of payment_. However, this also allows for the two protocols to pair together quite nicely.
For example, Lightning is great for increasing privacy by taking payments off-chain, and for massively increasing Bitcoin's ability to be used as a medium-of-exchange (i.e. something you would actually use to buy everyday things) in addition to a store of value. But the requirement to open channels on-chain means that the size of your channels and the people you open channels to leaves an onchain footprint. For reasons already discussed, payjoin can obfuscate and destroy many assumptions made by snoops.
It also makes things _simpler_ because users now only have to make one payment instead of two to open their first channel, _faster_ because they only have to wait for one transaction to be confirmed, and _cheaper_ because they only have to pay one fee. In fact, more than one channel can be opened by these means. What if you could make a list of all the nodes you wanted to open channels to, make a single transaction to a BIP-21 payjoin receiving endpoint, and have them all open at once, automatically, waiting for one transaction to confirm and paying one fee. _For all your channels_. Well, you can!
There is already a project which implemented this idea, called [Nolooking](https://github.com/payjoin/nolooking), which allows you to queue up a list of public keys and _batch_ open multiple lightning channels at once! This way, Alice can not only open a channel up to Bob, but she can open up channels to Bob, Carol, and Dina, all the while making _just one on-chain payment_! Needless to say, the potential for this to simplify Lightning UX is huge. It's exciting to imagine a future in which Lightning wallets just have payjoin on by default, and the _de facto_ user experience is to just pick your channel partners, make a single Bitcoin transaction, and _voila_! You now have a lightning node with as many channels as you can afford, and paid _one_ transaction for it. How amazing would that be?
It's easy to imagine how this could simplify self-custodial lightning adoption. It would be interesting if lightning wallet software could just have a "quick setup" option, where all the user has to do is input how much Bitcoin they can lock up (i.e. how much liquidity they want), set to a default of opening a few, reasonably sized channels with decent tradeoffs for routing and fees. For advanced users, there could be an "I know what I'm doing" option.
## Weaknesses
As with any protocol, there are tradeoffs to payjoin.
One of the main issues is the liveness (or online-ness) requirement. The receiver's payjoin web server must be online at the time the transaction is sent in the current implementation, due to the need for both sender and receiver to negotiate (programmatically, of course) the resulting transaction. This might limit adoption to merchant servers or lightning nodes, as they are the only people with an incentive to always stay online. It would be much more convenient from a user's perspective if a transaction could be sent at any time irrespective of whether the receiving server is online.
Another less likely but more dangerous weakness is that if a payjoin server (i.e. the receiver's server) is on an unsecured server, the receiver's outputs could be [modified in flight](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#unsecured-payjoin-server)(before they reach the sender) and stolen.
However, as we'll discuss later, a solution to the above two problems has already been proposed.
Finally, one weakness of the protocol is simply the fact that it faces adoption barriers as wallets must do the work to integrate it. A particular challenge is that the ideal user interface would be one in which payjoin is just enabled by default. Sending wallets and receiving wallets just try to payjoin without a user necessarily needing to be burdened with setting up that privacy feature. The best privacy is default privacy, since requiring users to take action to defend it means they probably won't. Therefore, for payjoin adoption to really take off for the average user, it needs to be a seamless experience that they don't have to think about. Wallets should enable it by default. Remember, it's built right into the protocol to fallback on regular transactions without requiring any additional action from the user in case the payjoin fails.
### Serverless Payjoin
Dan Gould has submitted a [draft BIP](https://gist.github.com/DanGould/243e418752fff760c9f6b23bba8a32f9) for a version 2 of payjoin which allows it to be done _asynchronously_ and without using a server. This _serverless payjoin_ would solve both the requirement for a receiver to be online at the time of payment, and the security issues related to this being done on unsecured servers, mentioned earlier. As the always-online nature of payjoin receivers is perhaps the biggest user experience barrier to adoption, the implementation of this BIP could be a huge benefit for payjoin adoption and passive Bitcoin privacy.
## The Current State of Payjoin Adoption
As of late 2023, payjoin adoption is still relatively low, but has been steadily increasing since its inception in 2018.<sup>[1](https://payjoin.substack.com/p/tracking-growth-in-payjoin-adoption)</sup> Since payjoin is ready to use today and doesn't require any Bitcoin consensus changes, the only real impediment is for wallets to write the software to adopt it, and the tools to help them do so are getting better every day. [Payjoin Dev Kit (PDK)](https://payjoindevkit.org/introduction/) is a new payjoin implementation with modules that wallet software can use to integrate today. It even comes with a `payjoin-cli` tool that allows you to create payjoins from the command line. The library is written in Rust, but bindings allowing other languages to use it are currently being built.
### Wallet Support
BTCPayServer and JoinMarket already support both sending and receiving, although not by default. BlueWallet, Sparrow, Wasabi, and BitMask support sending. A few other wallets support it via an extension, [including Bitcoin Core](https://github.com/payjoin/rust-payjoin). There are active PRs to integrate payjoin into Mutiny Wallet today. See [here](https://en.bitcoin.it/wiki/PayJoin_adoption) for the current state of payjoin adoption.
## Payjoin and Bitcoin's Future
As mentioned earlier, Adam Gibson has proposed that even just 5% of on-chain transactions being conducted with payjoin could have a huge impact on Bitcoin privacy. We just need a threshold number large enough that analysis companies can never safely assume they're interpreting the transactions correctly. Once their methods of spying on us are broken, then ill-informed, arbitrary, and bad-faith restrictions on Bitcoin privacy imposed by those who neither understand its benefits nor have any interest in preserving our right to privacy will become irrelevant.
And as we've seen, due to the many possibilities opened up by payjoin's flexibility, it is not merely a privacy solution, but an extensible cooperative transaction protocol that allows for solutions such as fee reduction and single transaction multi-channel opening for lightning nodes, among others. The benefits it would provide to Bitcoin could be enormous and achieved today without changing anything about Bitcoin itself.
So what are we waiting for?
## Support
If you'd like to support or contribute to payjoin's development, start by joining the [discord](https://discord.gg/6rJD9R684h), [donating](https://geyser.fund/project/payjoin/), or checking out [payjoin.org](https://payjoin.org/).
## Acknowledgements
I want to give a huge thank you to Dan Gould for reviewing and suggesting very helpful improvements to this article.

View File

@@ -1,102 +0,0 @@
---
title: Fear to Attempt
description: Our doubts are traitors and make us lose the good we oft might win by fearing to attempt
date: 2025-06-20
tags: philosophy
---
> Our doubts are traitors and make us lose the good we oft might win by fearing to attempt
>
> - Shakespeare, _Measure for Measure_
We _need_ you to dream; then pursue those dreams with vigor. You are a human being. You are a machine with hopes and dreams; a calculator who looks at cold, mute, void, dead nature, and somehow sees God and meaning despite yourself. You are a [teetering bulb of dread and dream](http://famouspoetsandpoems.com/poets/russell_edson/poems/15012). You cannot help but see the world in infinite terms. Despite your best efforts, you believe in right and wrong, black and white, good and evil. Nihilism is your greatest lie to yourself, and you feel the nausea it induces when it [crawls into your heart and dies](https://www.youtube.com/watch?v=T5xuzSjl8eU), because it is _unnatural_ and anti-life. Are you truly so bored with the ease and comfort that the vast array of the past has struggled, and suffered, and died to provide you with, that you have to cut yourself to remember you can feel?
> Shall I be carried to the skies,
>
> On flowery beds of ease,
>
> While others fought to win the prize,
>
> And sailed through bloody seas?
>
> Laura Ingalls Wilder, _Little House in the Big Woods_, p. 96
---
_Use this life_. Determine what is good, or at least, what is not evil, then _act_ as a humble servant of the former, and in direct repudiation of the latter. Your heart will thank you for it, even if the world never does. It is a cooling salve for your depression and melancholy. Though at the cost of greater anxiety. But anxiety is excitement also, and adventurousness, and _worth the struggle_.
> Far better it is to dare mighty things, to win glorious triumphs, even though checkered by failure, than to take rank with those poor spirits who neither enjoy much nor suffer much, because they live in the gray twilight that knows not victory nor defeat.
>
> Theodore Roosevelt, [_Strenuous Life_](https://voicesofdemocracy.umd.edu/roosevelt-strenuous-life-1899-speech-text)
You are hopelessly finite; but your actions can and do graze infinity. You can make a [dent in the Universe](<https://en.wikipedia.org/wiki/Steve_Jobs_(book)>). Some of us little people have. And they who have are frightfully similar to you.
---
How can any honest person look around at their world and not see that it is a [museum of passion projects](https://x.com/collision/status/1529452415346302976)? A handful of souls through the brief span of history shake off their multitude of excuses, doubts, fears, false friends, oppressive family - shed the dying skin - to chase these impossible dreams, and oftener than we should expect, achieve them. Even in the infested rivers of Calcutta, what is all that trash polluting the river? It is the unmaintained dreams of the past piling on and on into a neglectful present. Plastics and juice boxes and old toys are miracles if you stop them from coalescing into growing piles of grime.
Work passionately to some purpose. Most of those who regret working hard worked hard as slaves to some lower passion like money or cheap status. Those who conduct their work in service of Eternity have a cleaner conscience. Did we hear honest words of regret from Socrates? From Cicero or Cato? From Adams, Hamilton, Jefferson, or Washington? Only in their moments of weakness and fear when their tasks ahead bore relentlessly down on them. But, determining to answer the Call of Duty, they steeled themselves and pressed forward, leaving behind a trail of proud memories which sustained genuine pride in the shade of life.
---
> My depression is the most faithful mistress I have known — no wonder, then, that I return the love.
>
> Soren Kierkegaard, _Either/Or: A Fragment of Life_
Work toward depression and nothingness, and you will achieve it.
Weekends we spend alone, absorbing Netflix, Youtube, ads. Burning away the candle on video games, where we pretend we are heroes, when _real_ knighthood beckons to the brave. Doomscrolling our ten-second videos of AI-generated sequences of ever-more unintelligible noises and images, then we complain that we can't afford a house. Watch porn, then complain there are no good women. Work harder and smarter on justifying our inaction, and find enemies to our supposed purposes everywhere. We will never find an enemy sufficient to outsource our self-loathing. In this way we will never be cured of it. We are not taking our problems seriously by failing to recognize ourselves as their root cause. And who can we expect to take us seriously if not ourselves?
You are not jealous of the past. You have more than them if you cared to look. You are jealous of your own better nature which you glimpse buried within yourself manifesting forthrightly in others. Their lived virtue is recognizable to you as your own dormant morality.
You are not jealous of the past, except their Hope for the future. Reclaim the Hope, and you'll quickly see the thousand conveniences and asymmetric advantages afforded to you not given them.
That hope will be reclaimed with the youthful vigor of active optimism; never by a passive pessimism.
---
> "Friends," said he, "the taxes are indeed very heavy, and if those laid on by the government were the only ones we had to pay, we might more easily discharge them; but we have many others, and much more grievous to some of us. We are taxed twice as much by our idleness, three times as much by our pride, and four times as much by our folly; and from these taxes the commissioners cannot ease or deliver us by allowing an abatement. However, let us hearken to good advice, and something may be done for us; _God helps them that help themselves_.
>
> Benjamin Franklin, _The Way to Wealth_
You have the _internet_ friend! And you're using it to watch cat videos and porn. What's wrong with you? How many philosophers through the millenia have _dreamed_ of this moment, where anyone and everyone had access to the voluminous fount of human knowledge! So many of them thought this day would [unleash the golden age](https://www.theintrinsicperspective.com/p/why-we-stopped-making-einsteins) we've finally been waiting for. You can, for next to nothing, read the minds of the greatest thinkers, inventors, and scientists who've ever lived. But instead, we discovered that we are easily hackable, we will still use the evil software of companies we know are ill-designing against us, or at best do not care. We sell our souls in as near literal a sense as anyone could have possibly imagined, for a little convenience, a little fun, a little dopamine, and say that it is impractical to live without them, and give up. If we use this great tool for knowledge at all we're most likely to use it for a petty win in a debate not worth having. For the most part, we've shortened our attention spans and become addicted further still to our baser instincts. This is not Utopia. This is a Brave New World, and we, for the most part, feel oppressed by a future we wrongly think we have little power to avoid.
---
> The best way to predict the future is to invent it.
>
> [Alan Kay](https://www.youtube.com/watch?v=dTPI6wh-Lr0)
Knowledge and wisdom are now cheap, and as such, unvalued. Even our educated don't know much today, because knowledge and understanding are no longer the point. The knowledge of our college graduates is the kind of knowledge any true thinker hates. Commoditized and packaged so they can "get a job" and "have a career", and rarely develop the mark of a truth-seeker:
> So many people today - and even professional scientists - seem to me like somebody who has seen thousands of trees but has never seen a forest. A knowledge of the historic and philosophical background gives that kind of independence from prejudices of his generation from which most scientists are suffering. This independence created by philosophical insight is - in my opinion - the mark of distinction between a mere artisan or specialist and a real seeker after truth.
>
> Albert Einstein, [Correspondance to Robert Thorton in 1944](https://www3.nd.edu/~dhoward1/AEquotes.pdf)
---
You can adopt the whole human heritage. Do not be bound by superficial alliances. Be bound by vision and destiny. The human race is your anchor. Any giant's shoulders are shoulders to stand on if they are good and true. I admire Socrates and Pericles, though I am not Greek. Cicero and the Antonines, though I am not Roman. Confucius, Nietzsche, Kant, Petrarch, Da Vinci, what do I care for their allegiances? They painted their minds to us, and now they are mine.
Even within these flawed humans, or any flawed human, we can take the good and discard the bad. No human is really one human. We are instead a hash of blurry archetypes, oscillatingly present or absent from person to person, and within the same person at different times, places, and circumstances.
> Every man I meet is in some way my superior, and in that, I can learn of him.
>
> Emerson, _Think_, Vol. 4-5 (1938), p. 32
---
Is the state of society not to your liking? _Good_. _Fix it!_. What else do you have to do? Complain? Settle? You are not allowed to be angry and _do nothing_. Society will sink under the festering weight of this accumulated hypocrisy. I do not intend to imply this is easy; swimming against the tide never is. Far from it. Only that it is worth it. It is _necessary_.
Do you see problems? Well I see an army of problem-solvers; an array of latent potential waiting to unleash its energy, and hold back the tide of societal decay. Your mission in life is waiting for you. What excites you? If you have nothing that excites you, then what bothers you? What do you wish was, that isn't now? There is your destiny. Only a liar can't think of a worthy pursuit that will provide him a lifetime of work. Worthy pursuits and things worth doing are ever-present. You are far more limited than they are scarce. If you cultivate reasons enough for Being, imbibe enough meaning and purpose in your daily diet, a panoply of destinies scream and beg for you to take them on.
I've heard the cry from other people my age that "everything's been done", and all this noise about artificial intelligence isn't helping young people dream about brighter futures. It seems to be feeding their already weak grasp on the meaning of life. They constantly ask themselves what they will have to do, not realizing the opportunity lying latent in every problem. Every where I look I see little issues, and have reached the point that I lament the fact that I don't have the time or energy to work on all of them myself, as they would distract too much from an even greater or more exciting mission.
Do you feel that the problems are too big? Start small. Rome wasn't built in a day. Do you feel unmatched by the impenetrable weight of what you're fighting? Fight it anyway, and become a salient force to be reckoned with along the way, growing strong from each victory, and stronger still from each defeat. A scattered array of untrained militia defeated the might of the British Empire at its height, with muskets they used to hunt birds and deer. Can you honestly decry that your petty worries are anywhere near _that_ insurmountable? You have been living through the _Pax Americana_, and you and I spent the whole of it complaining, and if we're not extremely careful we will be reminded what real pain and suffering is.
---
> I think that there is only one way to science - or to philosophy, for that matter: to meet a problem, to see its beauty and fall in love with it; to get married to it and to live with it happily, till death do ye part - unless you should meet another and even more fascinating problem or unless, indeed, you should obtain a solution. But even if you do obtain a solution, you may then discover, to your delight, the existence of a whole family of enchanting, though perhaps difficult, problem children, for whose welfare you may work, with a purpose, to the end of your days.
>
> Karl Popper, _Realism and the Aim of Science_
So long as there are problems, this world needs your hidden and unique offerings. So long as there are problems, you can be the solution. Do not deprive us of your secret skills. Meet your destiny head-on. Cast your gaze upward toward the Lights of the Firmament, and dream.

View File

@@ -1,228 +0,0 @@
# Introducing Lyceum: The Richest Interface for Reading Ancient Greek Texts
<https://lyceum.quest>
I built a tool with the goal of making Ancient Greek authors more accessible in their own words. I started trying to learn Ancient Greek a few months ago with the goal of reading the Greek pantheon that shaped history like works by Homer, Plato, Aristotle, the New Testament, Marcus Aurelius' Meditations, etc. without the meaning being filtered through a translation. I started by using the available online resources and books such as _Athenaze_ and _Reading Greek_ (and still am!), but became bogged down by trying to memorize endless nuanced rules of grammar and decipher stories made up by the authors, which made the learning experience boring and tedious, and full glossaries often weren't given (so many of the morphological forms required inference). I don't care about some random made up Athenian named Δικαιοπολις and the silly hijinks of his small family farm, I want to read the _Iliad!_. And more importantly, despite after knowing more about Greek grammar than English after a few chapters, I still couldn't read a word of it! I became skeptical of this method, and began looking for ways to just learn it by _reading what I wanted to read_. There are a couple readers online which have the Ancient Greek texts in the original language, the best of which seems to be [Scaife Reader](https://scaife.perseus.org), but even that is buggy, difficult to use, and was missing features I wanted. So my goal was to combine the freely available datasets for lemmas, morphologies, and original texts and (some) of their translations into one unified reader that had all of the features I wanted for my own personal study. This way, I can hopefully learn Greek closer to the way the man who discovered Troy, [Heinrich Schliemann](https://en.wikipedia.org/wiki/Heinrich_Schliemann), learned it:
> "In order to acquire quickly the Greek vocabulary," Schliemann writes, "I procured a modern Greek translation of _Paul at Virginie_, and read it through, comparing every word with its equivalent in the French original. When I had finished this task I knew at least one half the Greek words the book contained; and after repeating the operation I knew them all, or nearly so, without having lost a single minute by being obliged to use a dictionary....Of the Greek grammar I learned only the declensions and the verbs, and never lost my precious time in studying its rules; for as I saw that boys, after being troubled and tormented for eight years and more in school with the tedious rules of grammar, can nevertheless none of them write a letter in ancient Greek without making hundreds of atrocious blunders, I thought the method pursued by the schoolmasters must be altogether wrong....I learned ancient Greek as I would have learned a living language."
## Current Features
So far, the reader includes:
- A browseable catalog of 373 ancient authors and 1,837 works in Ancient Greek
![Browseable catalog of ancient Greek texts](/images/articles/introducing-lyceum/browse.png)
- Multi-language translations when available from the data
![Multi-language translations side by side](/images/articles/introducing-lyceum/translations.png)
- Word-level definitions and morphology lookups from Perseus and LSJ. Click on any word to see its definition and other info!
![Word popup with definition and morphology](/images/articles/introducing-lyceum/popup.png)
- Anki-style vocab card import from texts. Learn the words of the texts you are trying to read as you read them with spaced repetition!
![Spaced repetition flashcard](/images/articles/introducing-lyceum/srs-card-1.png)
- Dashboard to track progress
![Progress dashboard](/images/articles/introducing-lyceum/dashboard.png)
- (For select texts): Word-level interlinear contextual AI definitions (show what each word means in its context)
- (For select texts): Word-level AI generated Latin transliteration (for pronunciation clarity)
(side-note on the use of AI: I know that it is discouraged in the rules on this subreddit, but please read my case for it in the [full article]()).
And there are many features and cleanup items on my TODO list, like:
- Add missing translations for popular texts (e.g. Meditations)
- Add more transliterations and contextual glosses (e.g. interlinear)
- Make texts downloadable in nice readable formats. A fun thing might even be to allow PDF generation in whatever display format options you've selected for offline reading.
- Fix any missing or incorrect dictionary definitions, with a user-driven feedback mechanism.
- Audio for pronunciation
- A mobile app
I would love any feedback on this. Try it and let me know what you think!
With side-by-side English (and other language) translations when available in the data. There is a sidebar which allows for viewing selected grammatical information for each word as you read, which includes color-coding for parts of speech (e.g. noun/article/verb etc.), color coding of inflected form endings, and grammar badges above each word. In the Greek originals, you can click on any word to see a popup of its definition from the [Perseus Digital Library]() or the [Liddel, Scott Jones Ancient Greek Lexicon (LSJ)](https://en.wikipedia.org/wiki/A_Greek%E2%80%93English_Lexicon). Since many Greek words have many potential definitions in different contexts, the Pro version provides Contextual AI generated meanings for select texts (with many more in the works) so that you can see the definition of a Greek word _in the context it was written in_, instead of having to guess which of the definitions is being used in that particular instance.
There is a reader view to show just the text (or translations if you like beside it) for distraction-free reading.
![Reader view for distraction-free reading](/images/articles/introducing-lyceum/reader-view.png)
Finally, one of my personal favorite features is the Anki-like spaced-repetition cards, where you can add cards as you read them (or by section, or in bulk) to your deck for review.
## Why Build This?
### Love of History
Living in Boston, my fascination with history has grown immensely over the past few years. The natural starting point was American Revolutionary history, and the epic adventures of founders Jefferson (Jon Meacham), Washington (Ron Chernow), Franklin (Walter Isaacson), Hamilton (Chernow), and especially John Adams (David McCullough, the _John Adams_ TV series) ignited a passion that resulted in _their_ historical heroes becoming mine also. Cicero and Rome became my fascinations as they were Adams'. And recently, Greece became my passion as it was Cicero's. And this was cemented with Will Durant's excellent books _Caesar and Christ_ and _The Life of Greece_.
### For Most of History, Greek was An Essential Aspect of Education
One interesting thing to note was
### How Best to Learn?
I attempted several methods, and in each I felt something was lacking. At first, I researched on Grok and Reddit and discovered books that came highly recommended and bought them: _Athenaze_ and _Reading Greek_. After being "troubled and tormented with the tedious rules of grammar" as Schliemann wrote, I had an understanding of the foundational grammar and some of the most common words. But with full-time work commitments, family responsibilities, and other hobbies, this was unsustainable and a painful way to learn, and after all this tedium I discovered that I knew a lot about made-up Athenian farmer Δικαιοπολις and couldn't read _any_ _Iliad_. I became demotivated and burnt-out.
I did some searching, and found the book _Complete Ancient Greek_ by Gavin Betts and Alan Henry, which agreed with my sentiment that Greek should be learned with the material the student _wants to read_ top of mind. Even so, the vocabularies given were often incomplete (a surprisingly common problem in these Greek textbooks!) which made deciphering even the basic exercises unnecessarily painful, and the choice phrases from works like _Odyssey_ were modified and simplified, which still gave me a sense of inauthenticity (although I felt much closer), and an unnecessary roundabout-ness to the learning process. So the search to learn by getting closer to the source continued.
I figured I'd start with the beginning of Western literature, the _Iliad_, and I searched for an interlinear/Hamiltonian translation (where each word is translated according to its contextual definition). I thought I'd be able to do this for any of the major texts I wanted to read, but these sorts of translations are surprisingly sparse! I found one on [archive.org](https://archive.org/details/iliadhomerwitha00clargoog/page/n12/mode/2up), and began making an [Anki]() deck for each word and memorizing it. There were still a couple problems. Although this was probably good enough to learn from, it was still difficult for me to completely understand what was happening, and I wanted to pull up a couple other translations side-by-side for comparison. This way I could simultaneously compare multiple editions of natural English to Hamilton's interlinear version to get a comprehensive sense of the meaning where it wasn't clear from the interlinear. Also, adding Anki cards manually for each word became a very time-consuming process.
Here the idea for the website began to take shape. What if I could combine all the features I was looking for (interlinear texts, side-by-side translations, and the ability to import words into an Anki-like system for spaced-repetition word learning from whatever text you were reading) into one web interface?
### Finding Texts in the Original Greek, in the Format You Want, is Very Difficult
If you go on eBay or Amazon and search for the Odyssey in Ancient Greek, you'll probably be presented with the Loeb Library edition. As far as I can tell these are the best in-class
One of the key features I needed for this was an interlinear translation. Going on eBay and finding your book of choice in _only Greek_ is very difficult. Finding it in
### Alternative Solutions Are Insufficient
The best viewer I've found online for reading Ancient Greek texts is the [Scaife](https://scaife.perseus.org) viewer from Perseus, and it has many problems. It is old and outdated, and the web deserves something better. You can start by taking a look at its [Google Lighthouse](https://developer.chrome.com/docs/lighthouse) score:
![Scaife Lighthouse score](/images/articles/introducing-lyceum/scaife-lighthouse-score.png)
### There is No Substitute for the Original
When I started looking into ancient Rome and Greece, my desire to forego translations kept rising. More and more, I began to realize that translations are often not what they say they are. They are often more like movies based on books -- which is to say that they deviate frequently and often significantly from the source material, and even in translations which attempt to be faithful the author's voice inevitably intrudes.
I actually learned this most plainly while building this website. While building the contextual glosses for Aesop's Fables, I noticed a word I recognized in Book 1, _ἀλώπεκος_, which means "fox", didn't appear at all in the famous [Townsend](https://en.wikipedia.org/wiki/George_Fyler_Townsend) translation, which is considered a standard translation. Here is the original Greek:
> Λέαινα ὀνειδιζομένη ὑπὸ ἀλώπεκος ἐπὶ τῷ διὰ παντὸς ἕνα τίκτειν·" Ἕνα, ἔφη, ἀλλὰ λέοντα." Ὅτι τὸ καλὸν οὐκ ἐν πλήθει δεῖ μετρεῖν, ἀλλὰ πρὸς ἀρετὴν ἀφορᾶν.
Here is the [Townsend translation](https://demo.lyceum.quest/read/tlg0096.tlg002.perry-grc1?ref=257&trans=tlg0096.tlg002.perry-eng1):
> The Lioness
>
> A CONTROVERSY prevailed among the beasts of the field as to which of the animals deserved the most credit for producing the greatest number of whelps at a birth. They rushed clamorously into the presence of the Lioness and demanded of her the settlement of the dispute. 'And you,' they said, 'how many sons have you at a birth?' The Lioness laughed at them, and said: 'Why! I have only one; but that one is altogether a thoroughbred Lion.' The value is in the worth, not in the number.
You can see at a glance that there are a lot more words in the Townsend translation than the original Greek, which arose my suspicion, which was further aroused by looking at the contextual meanings I had generated for it:
![Contextual meaning comparison](/images/articles/introducing-lyceum/read.png)
The AI-generated contextual meaning claims that this is the genitive form of "fox", i.e. "of fox". Is it wrong? Well, the nice thing about our tool is that we can immediately cross-reference with the LSJ dictionary, or, better yet for our purposes, Perseus for the various morphological forms of a word. We can click the word to open up the popup, as below, to compare:
![Popup showing morphological validation](/images/articles/introducing-lyceum/popup.png)
And we see that this does indeed mean "fox". So there is an entire character in the original Greek fable that is missing in a widely-respected translation! Cross referencing multiple AI responses and combining that with a by-hand interlinear-ization using Perseus data, we can see that the original fable translates to something more like:
> The lioness was being reproached by the fox because she always gave birth to only one.
> “One,” she replied, “but a lion.”
> For the noble is not to be measured by quantity, but one should have regard for virtue.
This problem pervades all translations. This sentiment is better expressed in the introduction to the book _Complete Ancient Greek_ by Gavin Betts and Alan Henry than I could give myself:
> A modern translation of an ancient classic such as Homers Iliad often puzzles readers with
> the difference between the works overall conception and the flatness of the English. The
> works true merit may flicker dimly through the translations mundane prose or clumsy verse
> but any subtlety is missing. Instead of a literary masterpiece we are often left with a
> hotchpotch of banal words and awkward expressions. Take this version of the first lines of the
> Iliad: _The Wrath of Achilles is my theme, that fatal wrath which, in fulfilment of the will of
> Zeus, brought the Achaeans so much suffering and sent the gallant souls of many
> noblemen to Hades, leaving their bodies as carrion for the dogs and passing birds. Let us
> begin, goddess of song, with the angry parting that took place between Agamemnon King of
> Men and the great Achilles son of Peleus. Which of the gods was it that made them quarrel?_
> (translated E.V. Rieu, Penguin Books 1950) Can this really represent the work of a poet who
> has been universally admired for millennia? Or is it a TV announcer introducing a guest
> singer, whom he flatters with the trite phrase goddess of song?
> Compare the eighteenth-century translation of Alexander Pope:
> _Achilles wrath, to Greece
> the direful spring
> Of woes unnumberd, heavenly goddess, sing!
> That wrath which hurld to Plutos gloomy reign
> The souls of mighty chiefs untimely slain;
> Whose limbs unburied on the naked shore,
> Devouring dogs and hungry vultures tore:
> Since great Achilles and Atrides strove,
> Such was the sovereign doom, and such the will of Jove!
> Declare, O Muse! in what ill-fated hour
> Sprung the fierce strife, from what offended power?_
> Here we have genuine poetry. Only when the translator himself is a real poet can the result
> give some idea of the original but even then its true spirit is lost and, as here, the translators
> own style and personality inevitably intrudes. There is no substitute for getting back to the
> authors actual words. To understand and appreciate the masterpieces of ancient Greek
> literature we must go back to the original Greek.
There is no substitute for the original. The only solution is to learn the languages.
### Building with Claude: AI is Surprisingly Good at This
With all the noise being made about how great AI is lately, I wondered if I could use it to create something that satisfies my requirements. Could I use it to speed up the process of collecting and organizing various data sources for word definitions, morphologies (the various forms of each word _lemma_ and how that changes its meaning), source and translation texts? And since interlinear versions of most texts do not exist (and even less in a machine-readable format), could I use an LLM to create a pipeline to generate them? Could I get it anywhere near the quality of Hamilton's system?
The questions, in essence, became:
1. Could I create a system that achieves parity with the best available tools for making original Greek sources and their translations accessible?
2. Could I improve upon them with new features and ways to engage with the texts?
3. Could AI reliably create interlinear versions of the texts with enough accuracy to be more helpful than harmful?
I believe the free features already surpass the top competing sites (Perseus, Scaife, etc.) and combine their best features into a much more accessible view. The Pro features add even more helpful features of Anki-like spaced repetition, Hamiltonian-style interlinears with contextual meanings, and progress charts. This is the
The r/AncientGreek subreddit rules state the following:
"Machine translators and AI are not reliable.
ChatGPT, Google Translate, and the like will confidently give you wrong answers about translations and Latin grammar. And if you only have a beginner's proficiency in Ancient Greek, there will be enough correct information to trick you. Generally, posts about machine translators and AI will be removed."
I sympathize with this sentiment, especially about AI's tendency to hallucinate. But I think it is a) a partially outdated opinion (AI tools have gotten better and better under the right supervisor), and b) it underestimates the value AI can bring from doing "grunt-work" which can later be validated.
Interestingly, there is a relatively recent [discussion]() on the same subreddit where several users talk about how AI has come in handy for their personal study of Ancient Greek. I wonder if these rules had been set in the earlier days of AI, when the false positive rate was high enough to render it more harmful than helpful. But I think two points need to be made here about AI:
1. Humans can (and do) make mistakes or simply make things up to suit their retelling (as in Townsend's translations above -- or really any of the mainstream Aesop translations that I've found).
2. The possibility of errors do not inherently mean something is more of a hindrance to learning than helpful. Rather, the question is is the error rate high enough that it is doing more harm than good.
3. Since learning Ancient Greek is a far more niche activity than it used to be, quality resources for learning it are becoming rarer and rarer based on the earlier discussion of the Townsend translations, we can see it's often the case that the current state
4. AI can do the initial grunt work no or few humans are willing to do, and thus make the problem one of _validation_ (which can be done either by careful reviews of experts)
To be clear, AI _does_ hallucinate, and it does so frequently, but many of the current models are quite good at doing proper interpretations,
### Data Sources
#### Texts
- [Perseus Digital Library](https://github.com/PerseusDL/canonical-greekLit) Greek texts and English translations from canonical-greekLit repository. 50+ authors, 631 works, from Homer through the Church Fathers.
- [Diorisis Ancient Greek Corpus](https://figshare.com/articles/dataset/The_Diorisis_Ancient_Greek_Corpus/6187256): 820 pre-lemmatized XML texts with word-level morphology, POS tagging, and sentence boundaries. Used to aid in generating contextual meanings.
- [First1K Greek Project](https://github.com/OpenGreekAndLatin/First1KGreek): Additional Greek texts from the Open Greek and Latin project at the University of Leipzig.
- [Chambry Aesopica](http://www.mythfolklore.net/aesopica/): Chambry's critical edition of Aesop's Fables with Perry numbering, multiple recensions, and scholarly apparatus.
- [Townsend Aesop Translation](https://www.gutenberg.org/ebooks/21): George Fyler Townsend's 1867 English translation of Aesop's Fables, used for parallel reading.
#### Morphology & Glosses
- [Perseus Ancient Greek Dependency Treebank](https://github.com/PerseusDL/treebank_data) Syntactic annotations (dependency trees) and glosses for select texts including Aesop, Homer, and Attic prose. Used for word-level alignment with translations.
- [Diogenes](https://github.com/pjheslin/diogenes): Morphological analyses for 400K+ Greek word forms. Each entry maps an inflected form to its lemma, part of speech, and full grammatical analysis (case, number, gender, tense, voice, mood, person).
#### Definitions
- [LSJ](https://lsj.gr/): 116,000+ dictionary entries from the definitive Ancient Greek lexicon (9th edition, 1940). Full scholarly definitions with citations, etymology, and usage notes. The "gold standard" comprehensive Greek definitions.
#### Claude AI
- **Content**: AI-generated contextual glosses that analyze word meaning within specific passages. Used to disambiguate words with multiple meanings (e.g., "λόγος" as "word" vs "argument" vs "reason" depending on context).
### Building the Tool I Wish Existed
Ultimately I'm building this because I want it to exist. I now use the spaced-repetition feature every day and am learning the words from the _Iliad_ and Aesop's _Fables_ using it. I will continue to add features I (and others) think are useful, I'm sure there are plenty of mistakes to correct and improvements to be made that I'm missing, please join our Discord or send an email to <support@lyceum.quest>.
### Gratitude for Previous Work
Ross Scaife, Herculaneum project, Loeb Library, Athenaze, Latinium
History dies under two conditions, I think:
1. A failure to _preserve_ the present. This is the job of the people who were alive during the events.
2. The failure to _breathe new life_ into it, to unearth the bones and tombs and scrolls of the past. That is the job of posterity.
We can't do anything about (1), but I believe it is our duty to pursue (2). The majority of the debates we have about X or Y social or political or philosophical issues are not new -- they were, for the most part, already debated in Ancient Greece, and with a few exceptions, it's debatable to what extent we improved on the groundwork they laid for any given topic.
Emerson said history must be rediscovered

View File

@@ -1,22 +0,0 @@
---
title: The First Monotheist
description: Zoroaster founded the world's first monotheistic religion, thereby opening a new chapter in world history.
date: 2023-09-21
tags: religion
---
I'm continuing to read [_Decline and Fall of the Roman Empire_](https://www.amazon.com/Decline-Empire-Volumes-Everymans-Library/dp/0307700763/ref=sr_1_5?crid=1G5JJM2BIH17N&keywords=decline+and+fall+of+the+roman+empire&qid=1695350465&sprefix=decline+and+%2Caps%2C85&sr=8-5&ufe=app_do%3Aamzn1.fos.f5122f16-c3e8-4386-bf32-63e904010ad0) by Edward Gibbon, and I learned another fun series of facts I thought worth writing about. I'd heard of [Zoroastrianism](https://en.wikipedia.org/wiki/Zoroastrianism), and knew that it had something to do with Persia, but I'd failed to remember from World History class (shocker!) that it was _the first_ monotheistic religion. Given that most people today, if they believe (or more likely, _claim to believe_, in God) they tend to believe there's only _one_. This is an extremely important development in the history of human thought, and may be owed to Zoroaster.
He's an extremely ancient figure, so much so that no one really knows when he lived, typically placing him somewhere between 1500 to 1000 BC. Zoroastrianism was the religion of the ancient Persians, and dominated much of the Middle East for a millennium, until being overtaken by Islam.
Aside from many of its very interesting ideas about free will, heaven and hell, creation and anti-creation, etc., one idea I found especially refreshing to find in minds that _long_ predate Jesus, Socrates, Confucius, and even the [first philosopher](https://en.wikipedia.org/wiki/Thales_of_Miletus) -- was the rejection of asceticism; the abstinence of sensual pleasures which often also leads to the withdrawal of societal participation and sense of social responsibility. Zoroastrianism apparently differs directly with the founders of many of our modern religions in this regard. To quote Gibbon:
> But there are some remarkable instances, in which Zoroaster lays aside the prophet, assumes the legislator, and discovers a liberal concern for private and public happiness, seldom to be found among the grovelling or visionary schemes of superstition. Fasting and celibacy, the common means of purchasing the Divine favour, he condemns with abhorrence, as a criminal rejection of the best gifts of Providence. The saint, in the Magian religion, is obliged to beget children, to plant useful trees, to destroy noxious animals, to convey water to the dry lands of Persia, and to work out his salvation by pursuing all the labours of agriculture.
One may wonder how much pointless human suffering was caused by the elevation of self-inflicted pain to an act smiled upon by God. This isn't to diminish the merits of well-directed sacrifice -- a sacrifice to some social purpose -- just sacrifice _for sacrifice's sake_.
Continuing with Gibbon:
> We may quote from the Zendavesta a wise and benevolent maxim, which compensates for many an absurdity. 'He who sows the ground with care and diligence, acquires a greater stock of religious merit, than he could gain by the repetition of ten thousand prayers.'
While the rejection of asceticism doesn't exist without the opposing danger of hedonism, and while I'm sure a closer look at Zoroastrianism would reveal many strange features, it is interesting to see a religion that _explicitly_ celebrates the application of our efforts toward positive social utility. If you're going to be judged by God, it's nice to be _judged by whether you do good in the world_, instead of being judged simply by the degree to which you abstain from participating in its evils.

View File

@@ -1,14 +0,0 @@
---
title: Reading, Writing, and Civilization
description: Gibbon on the importance of reading and writing to civilization, and what distinguished the Romans from the 'barbarians', i.e. the Germans
date: 2023-09-26
tags: reading
---
The importance of reading seems to need more and more explaining these days as people convince themselves they have no need for it (or because they simply have a hard time sitting still). Every once in awhile I'll stumble across yet another explanation for why reading, and by reading I mean reading _great books_, is so important.
Here's Gibbon on the importance of reading and writing to civilization, and what distinguished the Romans from the "barbarians", i.e. the Germans:
> The Germans, in the age of Tacitus, were unacquainted with the use of letters; and the use of letters is the principal circumstance that distinguishes a civilized people from a herd of savages incapable of knowledge or reflection. Without that artificial help, the human memory soon dissipates or corrupts the ideas intrusted to her charge; and the nobler faculties of the mind, no longer supplied with models or with materials, gradually forget their powers; the judgement becomes feeble and lethargic, the imagination languid or irregular. Fully to apprehend this important truth, let us attempt, in an improved society, to calculate the immense distance between the man of learning and the _illiterate_ peasant. The former, by reading and reflection, multiplies his own experience, and lives in distant ages and remote countries; whilst the latter, rooted to a single spot, and confined to a few years of existence, surpasses, but very little, his fellow-labourer the ox in the exercise of his mental faculties. The same, and even a greater, difference will be found between nations than between individuals; and we may safely pronounce that, without some species of writing, no people has ever preserved the faithful annals of their history, ever made any considerable progress in the abstract sciences, or ever possessed, in any tolerable degree of perfection, the useful and agreeable arts of life.
-- _Decline and Fall of the Roman Empire, Ch. IX, p. 242_

View File

@@ -1,89 +0,0 @@
---
title: Toward A Better Site
description: A more thoughtful approach to personal site development
date: 2025-01-29
tags: web blog
---
> We shall not cease from exploration
> And the end of all our exploring
> Will be to arrive where we started
> And know the place for the first time.
- [T.S. Eliot, from “Little Gidding,” Four Quartets. Originally published 1943.](https://www.columbia.edu/itc/history/winter/w3206/edit/tseliotlittlegidding.html)
This past year, I've experimented with several different methods of creating and managing a personal website. I didn't merely want the most convenient solution, or the solution most likely to buy popularity more easily via its platform or network effects. I wanted a solution that was in line with:
- 1. A growing set of principles I want to practice see more of in software I use and create.
- 2. A set of very unique and specific features I desired.
The pursuit of principles and features, of course, give way to the brutal constraints of reality to some degree. I don't have infinite time. And so the desire to obtain the features I want while maintaining those principles must be balanced with the constraints of time and effort. After experimenting with several iterations of website-building, I have in many ways "arrived where I started", but now feel more confident, more sure-footed, more "aligned", and more excited about what even a simple little personal website could be.
### First Site — A DIY Compromise
I started out as most Gen-Z developers start, I think. You want to be a developer, you need a portfolio. You want to be a _web developer_? Your portfolio is your website. And so my first website was just that. I built it during many skipped classes in college, while I was still painstakingly and inefficiently learning HTML, CSS, and Javascript. It was designed to be artsy and beautiful and to get me a job as a web developer. And so I didn't post or write about anything, just listed what I felt would be most impressive to recruiters. But, after proving to yourself that you can indeed get a job and you can indeed make enough to pay your rent and not be a broke college kid forever, you start to realize there's more important things in life than just trying to impress recruiters. You realize that a personal website can be so much more. It can be a _reflection of yourself_.
And so, with that in mind, I built my first "real" attempt at a personal site. I built it in [Sveltekit](https://svelte.dev/) (which was in unstable version 3 at the time), used the [MDSveX](https://github.com/pngwn/MDsveX) library so I could write my articles in [Markdown](https://en.wikipedia.org/wiki/Markdown). This was a proper blogging site that also listed projects and "About Me" and "Contact" and whatnot, however there were several problems:
- **Hosting**: It was hosted on Netlify, which meant that there were often cold starts when visiting it. This also meant that I wasn't hosting the site myself, which wasn't a big deal to me at the time, but developed into one over the course of the year.
- **Features**: The site was low on features and bells and whistles, but this was mainly because I just didn't take the time to implement them. The fact that everything was written in Sveltekit meant that I could have, but I wasn't taking the time to really think about what I wanted implemented and then come up with a good plan to do it.
- **Preprocessing the Markdown**: I didn't quite understand how to set up the preprocessor in MDSveX properly, which does the job of converting the Markdown to HTML when building the site. What I _think_ was happening was this resulted in pages having to convert the Markdown to HTML on the fly during runtime, and so the bigger the article, the slower the page load, but I never truly investigating this since I decided to redo the website from the ground up anyway. To the end user, this just felt like the page was frozen for awhile, which was really annoying UX.
- **Content and Organization**: I wasn't sure of how I wanted things organized, and I only had a few articles, blogs, and quotes on there. The way I would write my content was by writing the article in Markdown directly on the site and then just committing and pushing the code to Github, and from there it would build and deploy to Netlify automagically. That was nice, but I would prefer to be able to write the article in another program (so that I could have "draft" versions of it and whatnot) and then just commit + push when ready in a single command/click sort of way.
All these problems contributed to a year of experimentation that left my actual ability to write things a bit in limbo.
### Substack
I started out by just wanting something that worked, and worked well, for _writing_, without having to worry about anything else. Substack is the obvious choice for this. It's famously anti-censorship, has an intuitive UI, a large audience, most of my own subscriptions worth reading are from Substack, easy subscription tiers and connection to Stripe for payments, easy email subscriptions. What's not to like?
There was one thing that just ended up being a deal breaker for me. Probably a bizarre requirement to most people, but I just couldn't in good conscience do it. At the time, I was considering requiring subscriptions and payments for some of the articles I wrote, and on Substack, there's _no option for bitcoin payments_. The saddest thing of all is, it appears that there _was_ a way to pay with bitcoin while [Substack was using OpenNode's API](https://opennode.com/blog/bitcoin-payments-with-substack/), but don't any longer. I suppose that's a price of vendor lock-in. It is now Substack's [official policy](https://support.substack.com/hc/en-us/articles/360037862551-I-can-t-use-Stripe-Do-you-accept-PayPal-crypto-clamshells-etc) to _not_ support any bitcoin/crypto payments. I work in the Lightning Network _because_ the payments use case of bitcoin is one of if not it's most exciting aspect. It's probably one of the strongest ways bitcoin can positively influence how we use the internet. To only allow credit card subscriptions, and not bitcoin lightning micropayments, for paywalled articles if I did any would be sad and hypocritical of me. There are other reasons of course, like the fact that Substack isn't open source and that I wouldn't actually own my own data. The idea that my blog dies when the company dies was another major negative.
So what other options were there that fit my increasingly stringent criteria?
### Ghost
Enter [Ghost](https://ghost.org), a website which markets itself as essentially the more [principled and feature-rich version of Substack](https://ghost.org/vs/substack/) (which I believe they are). They are fully open source and you can run the blog software locally yourself, deploy it on a VPS of your choice, setup your own email provider, etc. If you choose to pay for them to host it for you, it's only $9 a month and that includes the ability to bring your own domain, email subscriptions, etc. A huge improvement to me, and a project and team that I was (and still am) very excited about.
They even have a marketplace of templates and integrations, and given their open-source spirit I was hopeful they'd have some sort of bitcoin integration on there — and there is one...that you have to pay for. It's called [Scribsat](https://scribsat.com/); here's the Ghost Forum [post](https://forum.ghost.org/t/a-new-bitcoin-payments-integration-scrib/36125/4) about it. They charge a monthly fee steeper than Ghost's hosting fees itself to allow you to accept bitcoin lightning payments on Ghost. That didn't really fit the bill for me, either, and I don't want to be forced to depend on another service to accept the payments for me. For what it's worth, this does seem to be a [hot topic](https://forum.ghost.org/t/bitcoin-payments/10514/37) on the Ghost Forum, and I hope that someone will take up the call of developing something people can use for this. Ghost is a great tool and company, [used by Bitcoiners](https://blog.lopp.net/), and would be powerful to many if properly combined with bitcoin payments.
I considered working on this myself, but there is significant overhead that I just didn't and don't have the time (for now) to take up.
For awhile, the lack of bitcoin-based payment options was worth it, and I paid for a full year's subscription even though I wasn't sure if I'd use it for long (I didn't, but I consider it a tithe to open-source software). I loved the writing and publishing experience, which was extremely simple, and there were a good number of default themes to choose from. However, none that I _loved_, and I found myself longing for more customizability. For most bloggers who can pay a few bucks a month or have the wherewithal to self-host, I would definitely recommend Ghost.
### Inspiration Draws From Disparate Sources
> Shall I tell you the secret of the true scholar? It is this: Every man I meet is my master in some point, and in that I learn of him.
- [Ralph Waldo Emerson, _Greatness_](https://wist.info/emerson-ralph-waldo/65482/)
My website was hosted on Ghost for a couple months, and it actually allowed me to write an article or two and post a quote here and there, and I was starting to give up on monetizing it (was I ever really doing it for that anyway?).
But around this time I was coming into greater contact with online presences that helped me to think differently, and though they all differ radically in occupation, style, and personal beliefs, they share an energy for creating good things meant to _last_; a passion for thinking long-term and for following their hearts over easy money and fame. A desire to achieve greatness in some form or other, and to have the revenues of their considerable efforts paid in something deeper than cash.
One of those I stumbled across was [Luke Smith](https://lukesmith.xyz/), a thoroughly original person, whose unique admixture of Twitter-like meme humor, dead-serious power usage of FOSS-only tech, deep historical, philosophical, and linguistic knowledge, and opinionated take on Christianity fascinated me, even if I didn't agree with many of his opinions and means of presenting that information. It's rare to find a person occupying all those particular spheres simultaneously, particularly in technology. From him I learned to ask for more from my software, to seek and value the right to and practice of _ownership_, whether that's in owning your own land, owning your own money (bitcoin, being debt-free), or owning your own technology to the highest degree possible. He even [removed all credit card-based ways to donate to him](https://lukesmith.xyz/updates/retiring-fiat-donation-portal/) on his website, as they weren't in keeping with his principles. I used his tutorial website on becoming an [Internet Landlord](https://landchad.net/) by self-hosting a website on your own DNS, for example, to build and host the version of this site at the time of writing. Another thing that is important to him is to have technology be a _tool you use_, rather than you be _its' tool_. To that end, he helped encourage me to try to build and use software that will last, and focus less on the ephemeral. Not an easy pursuit in the software world, open source or otherwise!
Another figure who influenced this blog is [Henrik Karlsson](https://www.henrikkarlsson.xyz/), whose article [_A blog post is a very long and complex search query to find fascinating people and make them route interesting stuff to your inbox_](https://www.henrikkarlsson.xyz/p/search-query) had a deep impact on me and the direction of my thinking about how a blog should be made. It is excellent, both the idea and the read.
[Zig](https://ziglang.org/) creator Andrew Kelley, also comes to mind, whose article [_Why We Can't Have Nice Things_](https://andrewkelley.me/post/why-we-cant-have-nice-software.html) helped me realize the incredible waste that often goes into making products worse when money alone is the goal.
[Visakan Veerasamy](https://visakanv.com/)'s article [_We Were Voyagers_](https://visakanv.substack.com/p/we-were-voyagers) is also an amazing and deeply encouraging read, and while it didn't directly influence how I built this website, it further influenced the "do something useful to others with your life in a spirit of gratitude" approach I'm trying to take to life in general, and will have an effect on the prose and content of the site if nothing else.
But the final major influence was [gwern.net](https://gwern.net/), whose beautiful and carefully crafted website design is matched by its rich content and elegant prose. It's filled with a hundred little features, optimizations, and design decisions that make it a joy to use. For example, it has custom [page previews](https://www.mediawiki.org/wiki/Page_Previews) which allow you to see a preview of where a link will take you when hovering over it. It has tables of contents and footnotes for each article, and the footnotes become sidenotes on wide screens for convenience. He takes the time to design artsy [dropcaps](https://en.wikipedia.org/wiki/Initial) for each article. And something I found very interesting that I've never seen before, is his extensive use of local archives to fight against [linkrot](https://en.wikipedia.org/wiki/Link_rot), which is an important problem if we wish to preserve something of ourselves for the future. Read his [about page](https://gwern.net/about) for the site to learn more.
All told, Gwern's website appears to me an exercise in how to create an optimal personal website for the long-run. He summarizes the fundamental question of building a site from the link above:
> What does it take to present, for the long-term, complex, highly-referenced, link-intensive, long-form text online as effectively as possible, while conserving the readers time & attention?
This version of my site, though still nascent, is based on a fuzzy mesh of these influences, and I will make the attempt to improve it and make it more resilient and higher quality over time.
### Knowing the Place for the First Time
Who was I kidding? I'm a _web developer_. I should've known from the beginning it would feel wrong to me to outsource that task, despite the work it may have taken. So after a year of exploration, I'm back at doing the same thing I did before — building the website in Sveltekit and MDsveX. But hopefully this time better, and having learned a lot about how to practically achieve that in addition to learning my own reasons for trying to do so in the first place.
Before, building my own website was a cool thing to put on my resume. But this time I have my _reasons_.
### What Now?
My primary reason for building this site is to document everything I do that I think might be of use to someone else (or my future self). I want something that I built myself and which I have full control over, whose format I have the freedom to modify or restructure at any time (which I couldn't do in my previous methods). I want something that allows me to publish articles, blogs, daily journals, quotes I find interesting, projects I'm working on, what have you, in as frictionless a manner as possible while still retaining maximum expressive possibilities. Which, in essence, means I have to build things myself, and that there will be a high up-front cost to doing so.
So be it!

View File

@@ -1,43 +0,0 @@
---
title: Hard Reset Asahi Linux After Boot Error and Data Backup
description: dnf update may cause a boot into a black screen. Here's how you can reset your computer.
date: 2025-02-12
tags: linux software
---
[Asahi Linux](https://asahilinux.org/) is a port of Linux on Apple Silicon (the M-series chips).
I've been using it fairly happily for awhile, but it booted into a black screen recently. I had a flash drive plugged in and I had also installed new programs using `dnf` (The Fedora package manager, which is the distro Asahi uses), and after some reddit searching it appears that the problem was caused by something having to do with `dnf update`. After restarting the computer, it showed the Apple logo, Asahi logo, and Fedora logo in sequence as usual, and then never booted past a black screen. Unfortunately I wasn't really able to "fix" it, but I was at least able to get into a terminal screen, mount a flash drive, backup my data, and then do a hard reset.
On boot, press and hold FN + CTRL + OPTION + F2. This should take you into a terminal-based login screen. Login with your normal user credentials. Then plugin a flash drive. Identify the device name by typing `lsblk`. It should appear as `/dev/sda` or something of the like. You should be able to identify it by its' file size (e.g. 32GB, etc.). Then, mount the device. First, you should create a mount point using `mkdir`, such as in `/mnt`:
Create a mount point:
```
sudo mkdir `/mnt/mydrive`
```
Mount the device:
```
sudo mount /dev/sda /mnt/mydrive
```
Copy files from your `home` directory (or wherever you have data stored you want to keep) to the flashdrive:
```
cp -r ~/* /mnt/mydrive
```
Unmount and eject the drive when done:
```
sudo umount /mnt/mydrive
sudo eject /dev/sda
```
### Repartition the Hard Drive
Now, you'll need to do a complete uninstall/reinstall of Asahi Linux by repartitioning the hard drive on your Mac.
I followed [this tutorial video](https://www.youtube.com/watch?v=nMnWTq2H-N0&t=7s) to help me repartition the Mac properly, and it worked like a charm. Then, I was able to go through the same process of reinstalling Asahi again and using my Apple Silicon machine with Linux.

View File

@@ -1,53 +0,0 @@
---
title: File Synchronization For All Your Devices with Syncthing
description: How to synchronize files across many devices automagically
date: 2025-02-12
tags: open-source software
---
I use the [TUI](https://en.wikipedia.org/wiki/Text-based_user_interface)-based tool [nb](https://github.com/xwmx/nb) for writing things. But I have a few problems:
1. I use it on a lot of different computers and often want to add to notes that only exist on computer A while I'm on computer B.
2. Since everything I write on my website is in Markdown, I want to have the ability to have what I place in that notebook automatically update the website (or at least update it after running some command)
Solving Problem 1 will be the focus in this article. In a subsequent post, I plan to detail solving problem 2.
## Requirements
I want a system that _automatically_ syncs files across devices that have it installed on and connected to at an interval I define when the machine comes online.
## Syncthing
After prompting [Perplexity.ai](https://www.perplexity.ai/) about the issue, two methods which appealed to me are [Syncthing](https://syncthing.net/) and Git. Using Git would've been more manual and time-consuming, and since Syncthing is a very popular tool and fully open source, I went with that option.
## Installing
Syncthing has a great ["Getting Started"](https://docs.syncthing.net/intro/getting-started.html) page that gave me everything I needed to know to use it. Just download the app on two devices. Select the architecture you're using from their releases page [here](https://github.com/syncthing/syncthing/releases). Follow their Getting Started page and you should have all the basics to get up and running.
## Helpful Notes
### Use Cases
Syncthing is not really meant to be a backup software like [rsync](https://linux.die.net/man/1/rsync), because changes made on one machine will synchronize to the other machines, and therefore if one machine deletes a file it will also be deleted on the other machine. It's meant to be a _sharing_ software keep documents on different computers synchronized, as the name suggests. Which means that it should really be used for things like media libraries or as an alternative to cloud storage solutions like Box or Google Drive, or when needing to edit documents on multiple machines, as in my case with `nb`.
### Mobile Devices
I wanted one of my devices to be my phone, so just note that for mobile (i.e. by mobile I mean _Android_ or a security-hardened derivative like [GrapheneOS](https://grapheneos.org/) — sorry iPhone fans, but their anti open source ethos precludes you from using this one) you have to use a [fork](https://github.com/Catfriend1/syncthing-android) of their [now deprecated Android app](https://github.com/syncthing/syncthing-android). You can download the apk file [here](https://github.com/Catfriend1/syncthing-android/releases).
### Folder Types
There are [three types](https://docs.syncthing.net/users/foldertypes.html) of folders:
- Send Only
- Receive Only
- Send and Receive
This is useful for sharing files while still setting some basic permissions for them. Send Only is great for sharing with people/devices that you don't want to be able to modify your local copy via their modifications; Receive Only is the opposite — it can only _be shared with_, and yet changes from the sending folder will not modify changes in the receiving folder.
### The FAQ is Very Helpful
Initially when using Syncthing I was confused about a few things (Should I use this for backups? Why does syncing take so long? How does it work? Will it ever connect to anything other than my own machines and can they see my data?). The [FAQ](https://docs.syncthing.net/users/faq.html#what-is-syncthing) answered all my questions very nicely and in detail, so be sure to give that a read.
### Note For Obsidian Users
[Obsidian](https://obsidian.md/) is an (unfortunately closed-source) note-taking app that pairs nicely with `nb`. I use `nb` on all my desk/laptop machines and use Obsidian on mobile since trying to use a terminal on mobile would be a terrible experience. After installing Syncthing-Fork on your mobile device and syncing the relevant folder, you can open that folder as your Vault in Obsidian. Before making any changes in Obsidian, however, you should disable a setting in Syncthing. Click on the synced folder, scroll down to "Watch for changes", and disable it. This will make it so changes to that synced directory will only attempt to sync changes to the others on an hourly basis. Obsidian saves files to disk extremely frequently, basically on every keystroke, and this caused Syncthing to behave strangely or simply not sync any changes made from Obsidian to the other devices. This is a bit unfortunate because there's a possibility that you could update the same file on your mobile device and another machine within that hour, causing a conflict. But fortunately Syncthing will save both versions of the file in a "conflict" file and you will have both versions so that there's no content loss.

View File

@@ -1,149 +0,0 @@
---
title: Automating Site Deployment for an `nb` -> Github -> VPS Setup
description: ""
date: 2025-02-13
tags: open-source software self-hosting
---
As mentioned in the blog post on [syncing my `nb` notebook across devices using Syncthing](../blog/syncthing-nb.md), I wanted a way to auto-publish to my site after finishing a piece of writing. What I ended up coming up with was having two notebooks: _draft_ and _final_, which were structurally the same as the markdown folder in the actual website code. When done with a piece in _draft_, I `cp` it to the appropriate place in `final`, then run a bash syncing script which copies all files from _final_ to the corresponding markdown folder in my Sveltekit website repo, `git commit`'s the results, and pushes it to the remote repository. Here is the bash script I'm using, in case anyone else finds it useful. Note the environment variables which must be defined before running. It's really just a directory copying + `git commit` script:
```bash
#!/bin/bash
# Script to copy files from nb drafts to site code, then commit and push
# --- Check environment variables ---
if [ -z "$SITE_GIT_REPO_DIR" ]; then
echo "ERROR: SITE_GIT_REPO_DIR is not set."
exit 1
fi
if [ -z "$NB_FINAL_DRAFT_DIR" ]; then
echo "ERROR: NB_FINAL_DRAFT_DIR is not set."
exit 1
fi
if [ -z "$NB_FINAL_DRAFT_IMAGES_DIR" ]; then
echo "ERROR: NB_FINAL_DRAFT_IMAGES_DIR is not set."
exit 1
fi
if [ -z "$SITE_CODE_MD_DIR" ]; then
echo "ERROR: SITE_CODE_MD_DIR is not set."
exit 1
fi
if [ -z "$SITE_CODE_MD_IMAGES_DIR" ]; then
echo "ERROR: SITE_CODE_MD_IMAGES_DIR is not set."
exit 1
fi
# --- Copy files ---
# Copy markdown files
echo "Copying files from $NB_FINAL_DRAFT_DIR to $SITE_CODE_MD_DIR..."
cp -r "$NB_FINAL_DRAFT_DIR"/* "$SITE_CODE_MD_DIR"/
# Check if the copy command was successful.
if [ $? -ne 0 ]; then
echo "ERROR: Failed to copy files from $NB_FINAL_DRAFT_DIR to $SITE_CODE_MD_DIR."
exit 1
fi
# Copy image files
echo "Copying files from $NB_FINAL_DRAFT_IMAGES_DIR to $SITE_CODE_MD_IMAGES_DIR..."
cp -r "$NB_FINAL_DRAFT_IMAGES_DIR"/* "$SITE_CODE_MD_IMAGES_DIR"/
# Check if the copy command was successful.
if [ $? -ne 0 ]; then
echo "ERROR: Failed to copy files from $NB_FINAL_DRAFT_IMAGES_DIR to $SITE_CODE_MD_IMAGES_DIR."
exit 1
fi
# --- Git Commit and Push ---
# Navigate to the Git repository directory
cd "$SITE_GIT_REPO_DIR"
# Check if it's a git repository
if [ ! -d ".git" ]; then
echo "ERROR: $SITE_GIT_REPO_DIR is not a git repository."
exit 1
fi
# Add the changed files (only the md and images directories)
echo "Adding files to git..."
git add "$SITE_CODE_MD_DIR" "$SITE_CODE_MD_IMAGES_DIR"
# Check if there are any changes to commit
if ! git diff --cached --quiet --exit-code; then
echo "Committing changes to git..."
git commit -m "Update content"
# Push the changes to the remote repository
echo "Pushing changes to remote origin..."
git push origin master
# Check if the push command was successful.
if [ $? -ne 0 ]; then
echo "ERROR: Failed to push changes to remote origin."
exit 1
fi
echo "Successfully updated remote repository."
else
echo "No changes to commit."
fi
echo "Script completed."
exit 0
```
## Automating Deployments to VPS
I also wanted to automate deployments to the VPS I was using on every push. So essentially, my website would update every time the script ran. So I put this `deploy.yml` in `.github/workflows`:
```yml
name: Deploy to VPS
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm install
- name: Build SvelteKit app
run: npm run build
- name: Deploy to VPS
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
password: ${{ secrets.VPS_PASSWORD }}
source: "build/*"
target: "/var/www/blu"
rm: true
strip_components: 1
```
Note that the `VPS_HOST`, `VPS_USER`, and `VPS_PASSWORD` must be defined repository/environment secrets in Github. Also note the action we're using at the end to do the actual deploy, `appleboy/scp-action`. Two parameters were important for what I was trying to do. First, `rm: true`, which deletes the target folder (`/var/www/blu` in this case) before uploading the data. In the scenario where I delete or move a file in the new build, the old file would've remained present without that parameter set, which I didn't want.
Second, the `strip_components: 1` is also important here. Without it, the `build` directory was getting placed inside `/var/www/blu`, instead of the _contents_ of the build directory moving into `blu`. So `strip_components` specifies that while I want to use `build/*` as the _source_ of my files, I only want _what's in it_, i.e. `build/*`, so don't put anything after the first slash (e.g. `build`) into `/var/www/blu`.
That's it! Now every time I update the content of the site (such as with this post), I just copy it over to _final_, then run the sync script, and voila!

View File

@@ -1,64 +0,0 @@
---
title: <bitcoin-qr/> - A Zero-Dependency Web Component for Stylish BIP-21 Payments
description: A QR code web component for Bitcoin on-chain, Lightning, and unified BIP-21 payments
date: 2025-02-21
tags: bitcoin
---
<span class="flex justify-center w-[100px]">
<img src="/images/blog/bitcoin-qr/qr.webp" alt="Bitcoin QR Example" width="594" height="596" loading="lazy">
</span>
When developing a Bitcoin payment flow, there are multiple ways a user can expect to be able to pay. They might want to pay an on-chain address or Lightning invoice, they may be scanning a QR Code from their phone, copy/pasting from a wallet, or using a [WebLN](https://www.webln.guide/) browser extension. Creating an intuitive interface that also captures all the possible ways a user can pay is one of the fundamental UX challenges of developing an application that can receive payments in Bitcoin.
This flexibility opens up many [exciting use cases](https://blu.cx/articles/bitcoin/micropayments), but often comes at the cost of being able to easily develop a smooth experience for the user. The greater the developer's cognitive load, the more difficult it is to create intuitive UX.
This project aims to provide everything needed to allow standard Bitcoin & Lightning Network payments out of the box. It handles creating the proper [URIs](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) from just an address or invoice, and favors creating unified URIs whenever possible. Styles are highly customizable and images can be embedded. It also includes polling functionality -- a callback can be passed as a property of the element to periodically check for payment.
Check it out on [Github](https://github.com/thebrandonlucas/bitcoin-qr) and feel free to play around with the options and create your own [here](https://bitcoin-qr.blu.cx).
In addition, because it's a [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components), you should just be able to drop it into any framework and have it work, whether it's pure HTML, Sveltekit, React, making it very versatile. Examples are included in the Github repo.
Here's a quick demo of how to use it in Sveltekit:
Install:
```sh
yarn add bitcoin-qr
```
Then:
```svelte
<script lang="ts">
import { defineCustomElements } from 'bitcoin-qr/loader';
defineCustomElements();
</script>
<bitcoin-qr
id="qr"
width="300"
height="300"
bitcoin="bc1p4mqqpxdyg3l0tvpv9xaesw5rsqv7fqpcn9ydawx3e8yr4enmc3qq7m59vd"
lightning="lno1zrxq8pjw7qjlm68mtp7e3yvxee4y5xrgjhhyf2fxhlphpckrvevh50u0q2a8y2qt9jeu3pj7p3u58rxs98yu93dxjlmy3eqp24qze09anfr6cqszm40686ujqq7t9jsjjf42tl2pryv3ds9f4hm6hysk7lvswdm70ntsqvlgc5nm4px6k3q2wmj4xun784sss2jn00q0w4dhydmln92wvash6jckffu0wmetwptj38f5g09k8pznuk4wq2df046yv0c5m3dakwgfz5ths2ymm3z6vnehvxj6urcarufa7uecwqqst9x4063hdpxq4frmupnsam9ahg"
parameters="amount=0.00001&label=bitcoin-qr.blu.cx&message=bitcoin-qr.blu.cx"
image="./assets/bitcoin.svg"
is-polling="false"
poll-interval="1000"
type="svg"
corners-square-color="#000000"
corners-dot-color="#000000"
corners-square-type="extra-rounded"
dots-type="classy-rounded"
dots-color="#000000"
debug="true"
poll-callback={callbackExample}
/>
```
I built this about a year ago but didn't post about it because there was more I wanted to do and test before putting it out there. But there's always something else to do, isn't there?
If you find any issues, feature requests, or suggestions for improvement, feel free to [create an issue](https://github.com/thebrandonlucas/bitcoin-qr/issues/new) in Github.
Hope this saves you some time!

View File

@@ -1,180 +0,0 @@
---
title: Stack Programs Like Legos with Nix!
description: Learn how to stitch programs together reprodicibly with Nix
date: 2025-11-21
tags: nix software open-source
---
Nix was made to solve the _software deployment problem_, concisely defined by creator Eelco Dolstra thus:
> [The software deployment problem] is about getting computer programs from one machine to another—and having
> them still work when they get there.
>
> The Purely Functional Software Deployment Model, Eelco Dolstra
Nix allows you to setup software on your computer in such a way that your setup is _reproducible_, meaning your setup on machine A can be _exactly_ the same as your setup on machine B -- as long as you have Nix.
To most people, learning Nix is a pain, due to the new concepts, sparse and outdated documentation, and community infighting.
But I think Nix can make using computers _fun_ and _powerful_ and _less painful_, once you learn how to handle its' edges.
One fun way we can use Nix is to stitch together programs like Legos and have them interact with each other in a reproducible way. Let's create an example!
First [install Nix](https://nixos.org/download/) if you haven't.
Say we have a fun little `bash` script like this:
```sh
#!/usr/bin/env bash
# filename: pokefortune.sh
message="${1:-}"
pokemon="${2:-slowking}"
# Silence Perl locale warnings.
export LC_ALL=C
# Generate a fortune if user did not pass a message.
if [[ -z "$message" ]]; then
message=$(fortune)
fi
echo "$message" | pokemonsay -p "$pokemon" -n
```
It optionally takes in a message and a [Pokédex](https://pokemondb.net/pokedex/all) number, and prints out that Pokémon and message using the `pokemonsay` program. If either the message or the Pokemon aren't specified, it uses a default Pokemon and the program `fortune` to generate the message.
Save this in a file `pokefortune.sh`, make it executable, then run it:
```sh
# Make the bash script executable
chmod +x pokefortune.sh
./pokefortune.sh
```
If you're like most people, you probably don't have `pokemonsay` or `fortune` installed on your system, so you'll likely see something like this:
```sh
./pokefortune.sh: line 6: fortune: command not found
./pokefortune.sh: line 7: pokemonsay: command not found
```
Therefore this script, which works on my system because I have these programs installed, isn't _reproducible_ on your system. Let's create a Nix _derivation_ to make it so. Create a file called `pokefortune.nix` and copy the following:
```nix
# filename: pokefortune.nix
{
pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz") { },
}:
pkgs.writeShellScriptBin "pokefortune" ''
message="''${1:-}"
pokemon="''${2:-slowking}"
# Silence Perl locale warnings.
export LC_ALL=C
# Generate a fortune if user did not pass a message.
if [[ -z "$message" ]]; then
message=''$(${pkgs.fortune}/bin/fortune)
fi
echo $message | ${pkgs.pokemonsay}/bin/pokemonsay -p "$pokemon" -n
''
```
Now run:
```sh
nix-build pokefortune.nix
```
This may take awhile, especially if this is your first time running Nix. Let's look at the output:
```sh
nix-build src/scripts/nix/pokefortune.nix
unpacking 'https://github.com/NixOS/nixpkgs/archive/nixos-24.05.tar.gz' into the Git cache...
this derivation will be built:
/nix/store/vg4zmghqcnhjbs8kqhx04xixvm36d3ik-pokefortune.drv
these 9 paths will be fetched (2.65 MiB download, 26.91 MiB unpacked):
/nix/store/0vdf5mpd762bw53rgl5nkmhvzq8n4m0d-file-5.45
/nix/store/77cdqhqprqbciyhzsnzmsk7azbk2xv6r-fortune-mod-3.20.0
/nix/store/d0i8idmbb4jji9ml01xsqgykrbvm7dss-gnu-config-2024-01-01
/nix/store/0554jm1l1qw1pcfqsliw91hnifn11w8m-gnumake-4.4.1
/nix/store/2wp235bg03gykpixd9v2nyxp08w8xq8a-patchelf-0.15.0
/nix/store/c5x32idp600dklz9n25q38lk78j5vwxb-pokemonsay-1.0.0
/nix/store/pba53n11na87fs4c20mp8yg4j7qx1by2-recode-3.7.14
/nix/store/zix67r268ihi4c362zw7c0989z12jmy7-stdenv-linux
/nix/store/2329271b42wh6b6yhl7jmjyi0cs4428b-update-autotools-gnu-config-scripts-hook
copying path '/nix/store/d0i8idmbb4jji9ml01xsqgykrbvm7dss-gnu-config-2024-01-01' from 'https://cache.nixos.org'...
copying path '/nix/store/c5x32idp600dklz9n25q38lk78j5vwxb-pokemonsay-1.0.0' from 'https://cache.nixos.org'...
copying path '/nix/store/0vdf5mpd762bw53rgl5nkmhvzq8n4m0d-file-5.45' from 'https://cache.nixos.org'...
copying path '/nix/store/0554jm1l1qw1pcfqsliw91hnifn11w8m-gnumake-4.4.1' from 'https://cache.nixos.org'...
copying path '/nix/store/2wp235bg03gykpixd9v2nyxp08w8xq8a-patchelf-0.15.0' from 'https://cache.nixos.org'...
copying path '/nix/store/pba53n11na87fs4c20mp8yg4j7qx1by2-recode-3.7.14' from 'https://cache.nixos.org'...
copying path '/nix/store/2329271b42wh6b6yhl7jmjyi0cs4428b-update-autotools-gnu-config-scripts-hook' from 'https://cache.nixos.org'...
copying path '/nix/store/zix67r268ihi4c362zw7c0989z12jmy7-stdenv-linux' from 'https://cache.nixos.org'...
copying path '/nix/store/77cdqhqprqbciyhzsnzmsk7azbk2xv6r-fortune-mod-3.20.0' from 'https://cache.nixos.org'...
building '/nix/store/vg4zmghqcnhjbs8kqhx04xixvm36d3ik-pokefortune.drv'...
/nix/store/vsv2spw517cwq791fl3f8iymm6hshhyq-pokefortune
```
Nix fetched the packages we specified in our `.nix` file: `pokemonsay`, `fortune`, and some we didn't specify: such as `file`, `patchelf`, and `recode`, which one or both of the other two packages depends on. Then it copied them locally, built a binary, and placed it at `/nix/store/vsv2spw517cwq791fl3f8iymm6hshhyq-pokefortune/bin`. We can confirm this by running it:
```sh
/nix/store/vsv2spw517cwq791fl3f8iymm6hshhyq-pokefortune/bin
```
You should see something like this:
<img src="/images/blog/nix-programs-as-legos/img_2_pokefortune.webp" alt="pokefortune result" width="800" height="1240" loading="lazy">
To be reproducible, Nix ensures that it knows about every single dependency needed to create a package at all times. Because of that, you can do fun things like this:
```sh
# Quickly enter a shell with the dependencies to create and display images.
nix-shell -p graphviz chafa
# Query the derivation's dependency graph, create a .png from it and display it:
nix-store --query --graph $(nix-build pokefortune.nix) | \
dot -Tpng -o pokefortune-dependency-graph.png && chafa pokefortune-dependency-graph.png
```
Resulting in a view of every single dependency (the "closure") that our `pokefortune` program requires:
<!-- <div style="text-align: center;"> -->
<img src="/images/blog/nix-programs-as-legos/img_3_pokefortune_dependency_graph.webp" alt="pokefortune dependency graph" width="800" height="634" loading="lazy">
<!-- <img src="assets/introduction/intro_img_3_pokefortune_dependency_graph.png" alt="pokefortune result" height="600"> -->
<!-- </div> -->
How cool is that! We can see the whole dependency tree for anything packaged with Nix!
You have just done something very powerful with Nix: You've created a reproducible derivation that anyone who has the package manager installed on their system can use, which they couldn't before.
This is just a taste of what Nix can do, but I hope that the potential is clear. As Farid Zakaria says in his blog post "Learn Nix the Fun Way":
> Hopefully, seeing the fun things you can do with Nix might inspire you to push through the hard parts.
>
> There is a golden pot 💰 at the end of this rainbow 🌈 awaiting you.
>
> - <https://fzakaria.com/2024/07/05/learn-nix-the-fun-way>
---
_This article is directly inspired by Farid Zakaria's blog post [**Learn Nix the Fun Way**](https://fzakaria.com/2024/07/05/learn-nix-the-fun-way). Check out his excellent blog [here](https://fzakaria.com/)._
_Special thanks to [Russell Weas](https://github.com/russweas) and [veracius](https://veracius.dev) for their input to this article and code_
---
References:
- <https://edolstra.github.io/pubs/phd-thesis.pdf>
- <https://web.archive.org/web/20171017151526/http://aptitude.alioth.debian.org/doc/en/pr01s02.html>
- <https://fzakaria.com/2024/07/05/learn-nix-the-fun-way>
- <https://zero-to-nix.com/start/nix-run/>

View File

@@ -1,21 +0,0 @@
---
title: January 2025
description: Journal
tags: journal
---
---
_2025-01-21_ - _Accountless AI Editors Paid with Bitcoin_
[ppq.ai](https://ppq.ai/) is a website that allows you to use pretty much any AI model using bitcoin micropayments (via the Lightning Network) instead of having to apply for a subscription or pay with a credit card. I hate that the modern web requires email accounts and either subscriptions or ads for everything, and love the idea of using micropayments for non-recurring and/or anonymous and/or just plain more convenient purchases online.
This is one of my favorite applications of micropayments I've seen so far. Excitingly, they have an API key in addition to the web interface, which meant that I was able to get [neovim](https://neovim.io/) working like the [Cursor](https://www.cursor.com/) code editor using the [avante](https://github.com/yetone/avante.nvim) neovim plugin by customizing my config! So now, I finally have a way to _accountlessly_, _privately_, and _subscription-less-ly_ pay for a service I very much want to use. Exciting times! I will gladly pay for this service now for my AI code-editing needs, over paying flat rates for subscription-based things like Cursor. I will try to do a writeup on how to do this soon.
---
_2025-01-20 - Inauguration Day_
- Idea: **Quote Scrolls** - Looping scrolls with quotes on them that people can sift through. Would be good for museums to be able to collate a bunch of famous things said about/by the place/person on exhibit.
- I wonder if there is a portable version of Markdown files? Something like a PDF, which includes all embedded image or other data, but also has the nice edit-ability properties of a Markdown file. One of the troubles with Markdown now is figuring out how to export/import them around. Do you just zip the .md and image/video files referenced in the same directory? What if the path to the desired image or video wasn't in the same directory when you wrote it? This could perhaps be something as simple as a standardized and agreed-upon way to put referenced files of different types in folders of the same name (e.g. a "videos" and "images" directory standard co-located with the .md file).

View File

@@ -1,101 +0,0 @@
---
title: February 2025
description: Journal
tags: journal
---
_2025-02-20_
- _Thought_: Perhaps the next browser just be an LLM that cites it's sources. That's it. I literally just open the browser and see a chatbox, can switch between LLM backends, and can use my own locally-hosted one if I'd like. For the ones I can't buy, I just pay a bitcoin lightning invoice to use. That's it!
- _Thought_: I wonder how much it would be to build a desktop computer that could run Deepseek 1776? Much cheaper than buying a laptop with equivalent specs, I suppose?
- It would be cool to do an aggregate of the most popular tools rated by experts in each domain in software is. I.e. frontend developers love [Svelte](https://svelte.dev/), backend developers love [Rust](https://www.rust-lang.org/), SysAdmins love, I dunno, [tmux?](https://github.com/tmux/tmux/wiki), etc. There should be some criteria that we develop for what makes software "high-ranking", such as it's survival rate ("vim" and "emacs" are both still around and extremely popular, so they get high scores). Similar to the way in economics we have the measure the quality of a currency by its fungibility, divisibility, portability, durability, scarcity.
- _accountless.io or Awesome Accountless_: Maintain a repo/website of services which do not require accounts/respect privacy/are free and open source etc., for their use.
- Today I ran and connected to a Minecraft server self hosting on my local NixOS device running over a Wireguard VPN. The server exists in the `minecraft-server` package, but you have to first do two things in your /etc/nixos/configuration.nix file:
- Add the lines:
```nix
nixpkgs.config = {
allowedUnfree = ["minecraft-server"]
}
```
By default, NixOS does not include Free and Open-Source Software (FOSS). So for any package that you try to download that doesn't isn't FOSS (which includes Minecraft 🥲), you'll get this error. You can explicitly define which packages you'd like to have as unfree in an array as done above, or you can just generically do `nixpkgs.config.allowUnfree = true;`. I opted for specificity as I want to include as little unfree software as possible ;)
- The second issue, which I got tripped up on for awhile, was the firewall. NixOS has configuration options for letting certain ports past the firewall. The `minecraft-server` port is 25565 (which you can see if you observe the server logs), so we want to allow packets through both (I believe both are necessary, anyway) the UDP and TCP port `25565`, like so:
```nix
nixpkgs.firewall.allowedTCPPorts = [ 25565 ];
nixpkgs.firewall.allowedUDPPorts = [ 25565 ];
```
After that's done, remember to run `sudo nixos-rebuild switch` to rebuild your NixOS configuration, and you should now be able to connect, assuming that server is reachable via your computer.
- I also managed to get Nextcloud running on my NixOS server, using [this guide](https://nixos.wiki/wiki/Nextcloud) and some help from [ppq.ai](https://ppq.ai) (Grok 2, in this case). Here is the setup I used:
```nix
environment.etc."nextcloud-admin-pass".text = "PWD";
services.nextcloud = {
enable = true;
package = pkgs.nextcloud30;
hostName = "10.8.0.2"; # An example of an IP assigned to the device by Wireguard VPN. This way it's accessible from other devices.
config.adminpassFile = "/etc/nextcloud-admin-pass";
config.dbtype = "sqlite";
};
```
There are a couple of deviations here from the original post. First, I needed to upgrade to pkgs.nextcloud30 instead of pkgs.nextcloud28, as the docs recommend (it's officially deprecated and `nixos-rebuild switch` will hassle you about it). You also have to change `"PWD"` to a password that's allowed by [Nextcloud's default password policy](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_password_policy.html) as mentioned in the article above, which, in my case, just amounted to creating a secure password with a password manager and them using that.
Also, be sure to later enable the TCP ports 80 (HTTP) and 443 (HTTPs):
```nix
nixpkgs.firewall.allowedTCPPorts = [ 80 443 ];
```
I was able to access the Nextcloud UI at `10.8.0.2` after this! However, I got stuck in password hell for reasons I cannot fathom (it _seemed_ like it was loading my password correctly, only to tell me after over 10 seconds that the password was wrong). What I _think_ was happening was that Nextcloud wasn't accepting the original password I used, which was something stupid simple like `password123`, and it was failing Nextcloud's password rules. But then when I went to change it, something about the database was already screwed up or my IP address was just banned and I couldn't login even after having set a good password. In the end, I just ended up nuking all the Nextcloud setup via `sudo rm -rf /var/lib/nextcloud` (careful with that command!), and then I re-ran `sudo nixos-rebuild switch` which re-setup the config with a good password from the start this time. And wouldn't you know it, things worked! I could login with the default username of `root` and the password I had set in the config above.
By the way, another neat trick I learned is that you can look up what options exist for a `nix` service by using the `nixos-option` command. By running `nixos-option services.nextcloud.config`, I found you could set an admin username of your choice as well via `config.adminuser`.
---
_2025-02-18_
- I now have:
- A VPS running Wireguard VPN which my devices are connected to
- An Apple Silicon (Macbook M1) computer running NixOS
- The NixOS Macbook running a Jellyfin server for my media
- The Jellyfin server can successfully be accessed by any computer connected to the VPN
- Example of why I love open-source. I was having an issue today where my Jellyfin server would keep disconnecting from my other devices, seemingly sporadically, after awhile. Often when checking the logs I would see an error related to websockets, but didn't understand why it was occurring. After browsing the [Jellyfin Github Issues](https://github.com/jellyfin/jellyfin/issues) for a bit [I discovered](https://github.com/jellyfin/jellyfin/issues/13379) someone who very likely is running into the same issue as me. It likely has something to do with running Jellyfin on Apple Silicon, which I am, despite the OS being NixOS. This person opened the issue last month, and the maintainers responded saying they had resolved it in the upcoming release. That release happened two days ago, and the maintainers responsible for pushing that update to the Nix package manager are now behind, so I literally can't update it yet. But because the project was open-source, I could find that I had the same problem as someone else, and rest assured that I simply have to update my software when the release becomes available. And since the Nix package for Jellyfin is only one patch version behind, I expect that to be very soon.
- _Idea_. [ppq.ai](https://ppq.ai) allows you to access many different AI models using micropayments. It's amazing. The only thing I'd like now is a command line tool that I can use their API with.
---
_2025-02-15_
- Thought - in general, we really need more things that are just "type these values into a list and run a program against it". NixOS's idea of just having a configuration file that defines an operating system is brilliant and is something we should have been doing for much longer in the computing world, and more programs and systems should operate this way. Instead of having to "re-setup" your computer every time you buy a new one (or relying on cleverly closed-source solutions like Apple has thus far successfully managed to do), you just plop in your config, sync your data, and _boom!_, you're back in business. Over the years your computer would become more and more personalized to _you_. As a side note, it seems this means NixOS can sort of be a playground with which to create the best operating systems (you can configure anything pre-built).
- _Idea_ - Feedify - Turn a set of Markdown files into an RSS Feed. We really need to make RSS Great Again. It would appear, at a superficial glance, that it is just architecturally much more user friendly, privacy-respecting, and self-hostable in every way. Why don't we all use it? And, for what it's worth, XMPP for messaging?
- Thought - I got this idea from [Luke Smith](https://lukesmith.xyz/), but wouldn't it be awesome to have browsers that could be constructed from config files as well?
- _Installing NixOS on Apple Silicon_: (Note: I went ahead and _uninstalled_ asahi linux to simplify the process, since these docs seem to assume a clean installl). I followed the instructions [here](https://github.com/tpwrules/nixos-apple-silicon/blob/main/docs/uefi-standalone.md), _carefully_, and was successfully able to run (headless, at least) NixOS on Apple Sillicon.
---
_2025-02-14 - Valentine's Day. ssh-copy-id_
This lovely Valentine's, I learned about a cool command for automating `ssh` logins to another computer.
`ssh-copy-id` sets up the `ssh` keypair exchange for you automatically by just running it against your `username@host-ip` like so:
```bash
ssh-copy-id root@123.456.789
```
Then, it'll ask you for your password, and voila! You should now have `ssh` setup between your computer and the host such that the next time you run `ssh root@123.456.789` it will just auto-log you in, no passwords required!
---

View File

@@ -1,44 +0,0 @@
---
title: "Writing"
tags: software
---
# [**Writing**](/writing)
---
## **Articles**
- ### [Fear to Attempt](/posts/articles/2025-06-20-fear-to-attempt)
- ### [Payjoin for a Better Bitcoin Future](/posts/articles/2023-10-31-payjoin-better-future)
- ### [Micropayments and the Lightning Network](/posts/articles/2023-03-10-micropayments)
---
## **Blog**
- ### [Nix for Fun and Profit: Programs as Lego's](/posts/blog/2025-11-21-nix-programs-as-legos)
- ### [`<bitcoin-qr/>` - A Zero-Dependency Web Component for Stylish BIP-21 Payments](/posts/blog/2025-02-21-bitcoin-qr)
- ### [Automating Site Deployment for an `nb` -> Github -> VPS Setup](/posts/blog/2025-02-13-automate-site-deploy)
- ### [Hard Reset Asahi Linux After Boot Error and Data Backup](/posts/blog/2025-02-12-asahi-boot-fix)
- ### [File Synchronization For All Your Devices with Syncthing](/posts/blog/2025-02-12-syncthing-nb)
- ### [Toward a Better Site](/posts/blog/2025-01-29-blog-revamped)
- ### [Reading, Writing, and Civilization](/posts/blog/2023-09-26-reading-writing-civilization)
- ### [The First Monotheist](/posts/blog/2023-09-21-worlds-first-monotheist)
---
## **Journal**
- ### [February 2025](/posts/journal/2025-02-01-feb25)
- ### [January 2025](/posts/journal/2025-01-01-jan25)

View File

@@ -1,439 +0,0 @@
---
title: "Quotes"
tags: software
---
# **Quotes**
---
I keep a collection of quotes I find interesting from various sources. In general, I try to keep these to things I can directly reference, so I write the book, link, or other source I read it in
---
2025-08-05
> Someone who asked, 'why believe what is true?' or 'why want what is good?' has failed to understand the nature of reasoning. He doesn't see that, if we are to justify our beliefs and desires at all, then our reasons must be anchored in the true and the good.
- Roger Scruton, Beauty
---
2025-08-04
> The stock exchange is a poor substitute for the Holy Grail.
- [Joseph Schumpeter, quoted from 'Capitalism Buries Its Undertakers' by Robert Bellafoire](https://commonplace.substack.com/p/capitalism-buries-its-undertakers)
---
2025-08-04
> Is the existence of billionaires all that makes people question capitalism today? Or is it also the dull horror of realizing that for all our cherished economic freedom, there doesnt seem to be anything worth doing with that freedom besides ordering Uber Eats and watching porn?
- [Robert Bellafiore, Capitalism Buries Its Undertakers](https://commonplace.substack.com/p/capitalism-buries-its-undertakers)
---
2025-07-22
> And the men who hold high places
> Must be the ones to start
> To mould a new reality
> Closer to the Heart
> The Blacksmith and the Artist
> Reflect it in their art
> Forge their creativity
> Closer to the Heart
> Philosophers and Ploughmen
> Each must know his part
> To sow a new mentality
> Closer to the Heart
> You can be the Captain
> I will draw the Chart
> Sailing into destiny
> Closer to the Heart
- [Rush, Closer to the Heart](https://www.rush.com/songs/closer-to-the-heart/)
---
January 23, 2025
> Shall I be carried to the skies,
> On flowery beds of ease,
> While others fought to win the prize,
> And sailed through bloody seas?
- Laura Ingalls Wilder, _Little House in the Big Woods_, p. 96
---
September 17, 2024
> Only a few prefer liberty the majority seek nothing more than fair masters
- Sallust, Histories
---
September 17, 2024
> Human nature is universally imbued with a desire for liberty, and a hatred for servitude.
- Julius Caesar, Gallic Wars
---
September 12, 2024
> And I ask for your prayers that these vague and wandering thoughts of mine may some day become coherent and, having been so vainly cast in all directions, that they may direct themselves at last to the one, true, certain, and never-ending good.
- Petrarch, The Ascent of Mount Ventoux, April 26, 1336 at Malaucène
---
September 3, 2024
> It is inhuman to bless where one is cursed.
- Nietzsche, Beyond Good and Evil, Pt 4: Maxims and Interludes, #181
---
September 3, 2024
> The consequences of our actions take us by the scruff of the neck, altogether indifferent to the fact that we have 'improved' in the meantime.
- Nietzsche, Beyond Good and Evil, Pt 4: Maxims and Interludes, #179.
---
August 16, 2024
> Who can doubt that, were Rome to know itself once more, it would rise again?
- Petrarch, quoted from Petrarch: Everywhere a Wanderer by Christopher Celenza, Ch. II, p. 56
---
August 16, 2024
> Rome, soon to be destroyed, continued to laugh and play.
- Will Durant, The Age of Faith
---
August 16, 2024
> What makes the heart of the Christian heavy? The fact that he is a pilgrim, and longs for his own country.
- Saint Augustine, self-written epitaph, quoted from The Age of Faith by Will Durant, Ch. III, Part V: The Patriarch
---
August 13, 2024
> Once triumphant, the Church ceased to preach toleration
- Will Durant, The Age of Faith, Ch. III, Part II: The Heretics
---
August 13, 2024
> in 470 a general impoverishment of fields and cities, of senators and proletarians, depressed the spirits of a once great race to an epicurean cynicism that doubted all gods but Priapus, a timid childlessness that shunned the responsibilities of life, and an angry cowardice that denounced every surrender and shirked every martial task.
- Will Durant, The Age of Faith, Ch. II, Part V: The Fall of Rome
---
August 13, 2024
> To be ignorant of what occurred before you were born is to remain always a child.
- Cicero, Orator, 120
---
August 10, 2024
> All that is profound loves a mask; the very profoundest things even have a hatred for images and likenesses. Shouldnt the opposite be the only proper disguise to accompany the shame of a god?….Every profound spirit needs a mask; even more, a mask is continually growing around every profound spirit thanks to the constantly false, that is shallow interpretation of every word, every step, every sign of life he gives.
- Nietzsche, Beyond Good and Evil, Part 2
---
August 8, 2024
> What a monument of human smallness is this idea of the philosopher king. What a contrast between it and the simplicity and humaneness of Socrates, who warned the statesman against the danger of being dazzled by his own power, excellence, and wisdom, and who tried to teach him what matters most that we are all frail human beings. What a decline from this world of irony and reason and truthfulness down to Plato's kingdom of the sage whose magical powers raise him high above ordinary men; although not quite high enough to forgo the use of lies, or to neglect the sorry trade of every shaman the selling of spells, of breeding spells, in exchange for power over his fellow men
- Karl Popper, The Open Society and Its Enemies
---
August 4, 2024
> Even if we know how to educate tomorrows professional programmer, it is not certain that the society we are living in will allow us to do so. The first effect of teaching a methodology —rather than disseminating knowledge— is that of enhancing the capacities of the already capable, thus magnifying the difference in intelligence. In a society in which the educational system is used as an instrument for the establishment of a homogenized culture, in which the cream is prevented from rising to the top, the education of competent programmers could be politically impalatable.
- Edsger Dijkstra, The Humble Programmer
---
July 30, 2024
> Friendship is not to be sought for its wages, but because its revenue consists entirely in the love which it implies
- Cicero, On Friendship
---
July 30, 2024
> Direct self observation is not nearly sufficient for us to know ourselves: we need history, for the past flows on within us in a hundred waves. Indeed, we ourselves are nothing but that which at every moment we experience of this continual flowing.
- Nietzsche, 1878, Human, All Too Human
---
July 30, 2024
> Im increasingly certain that there are others like me in the world, alive right now, quietly suppressing themselves for social reasons. I hear from more of them every month. They suppress themselves because they dont personally know of any House of Wisdom that they could attend to fully be themselves in. Because the scale and scope of their interests dont quite correspond with that of those of the people around them, and they dont know if its worth opening up about their inner truths because they believe, accurately according to their past experience, that the likeliest outcome is that people will misunderstand them. A confused “huh?” is often the best you can hope for. Far better than being mocked, insulted, laughed at, dismissed.
> Over the years, Ive increasingly developed a sense of lightness, clarity, courage and conviction in realizing that these are my people. That when Im writing for the younger version of myself, and the future versions of myself, Im writing for them. For us. All of us. Im a me, but Im also a we. And there is a deep kinship in that, a deep sense of belonging. And I have decided that I am willing to endure any amount of mockery and misunderstanding from the people who dont get it, to be a bridge to the people who do. Because more than anything else, that is what I wish I had in my life. A space to understand and be understood. I found it first mainly in books. I have since found it in like-minded nerds. And I hope to share it with literally anybody else who wants it
- Visakan Veerasamy, We Were Voyagers
---
July 30, 2024
> Nobody worth hero-worshipping would want you to worship them. They would want you to become heroic yourself.
- Visakan Veerasamy, We Were Voyagers
---
July 30, 2024
> Meek young men grow up in libraries, believing it is their duty to accept the views which Cicero, which Locke, which Bacon, have given; forgetful that Cicero, Locke, and Bacon were only young men in libraries when they wrote these books.
- Ralph Waldo Emerson, The American Scholar
---
July 30, 2024
> The question of whether Machines Can Think is about as relevant as the question of whether Submarines Can Swim
- Edsger Dijkstra, 1984, The Threats to Computing Science
---
July 30, 2024
> We must be very careful when we give advice to younger people; sometimes they follow it!
- Edsger Dijkstra, The Humble Programmer
---
July 13, 2024
> We are living through an advice pandemic and nobody appears to have yet discovered an effective vaccine.
- Tom Cox, Can You Please Stop Telling Me To Live My Best Life Please
---
July 8, 2024
> Bless you prison, bless you for being in my life. For there, lying upon the rotting prison straw, I came to realize that the object of life is not prosperity as we are made to believe, but the maturity of the human soul.
- Aleksandr Solzhenitsyn, The Gulag Archipelago
---
June 30, 2024
> Our legacy is to fill the Universe with children who laugh more than we were allowed to.
- Noah Smith, Toward a Shallower Future
---
May 16, 2024
> Congregations love to be scolded, but not reformed
- Will Durant, The Age of Faith
---
May 16, 2024
> Educate the children and it won't be necessary to punish the men.
- Pythagoras
---
May 15, 2024
> [...] books are the main peer group of any thinker.
- Henrik Karlsson, On Having More Interesting Ideas
---
May 7, 2024
> [Gratitude] is not only the greatest of virtues, but the parent of all the others.
- Cicero, Defense of Cnaeus Plancius, Ch. 33, Section 80
---
May 3, 2024
> The object of life is not to be on the side of the majority, but to escape finding oneself in the ranks of the insane.
- Marcus Aurelius, Meditations
---
Apr 29, 2024
> You are carrying God about you, you poor wretch, and know it not.
- Epictetus, quoted from Caesar and Christ by Will Durant
---
Mar 30, 2024
> The evil was not in the bread and circuses, per se, but in the willingness of the people to sell their rights as free men for full bellies and the excitement of the games which would serve to distract them from the other human hungers which bread and circuses can never appease.
- Cicero
---
Mar 25, 2024
> The heritage that we can now more fully transmit is richer than ever before. It is richer than that of Pericles, for it includes all the Greek flowering that followed him; richer than Leonardos, for it includes him and the Italian Renaissance; richer than Voltaires, for it embraces all the French Enlightenment and its ecumenical dissemination. If progress is real despite our whining, it is not because we are born any healthier, better, or wiser than infants were in the past, but because we are born to a richer heritage, born on a higher level of that pedestal which the accumulation of knowledge and art raises as the ground and support of our being. The heritage rises, and man rises in proportion as he receives it.
> History is, above all else, the creation and recording of that heritage; progress is its increasing abundance, preservation, transmission, and use. To those of us who study history not merely as a warning reminder of mans follies and crimes, but also as an encouraging remembrance of generative souls, the past ceases to be a depressing chamber of horrors; it becomes a celestial city, a spacious country of the mind, wherein a thousand saints, statesmen, inventors, scientists, poets, artists, musicians, lovers, and philosophers still live and speak, teach and carve and sing. The historian will not mourn because he can see no meaning in human existence except that which man puts into it; let it be our pride that we ourselves may put meaning into our lives, and sometimes a significance that transcends death. If a man is fortunate he will, before he dies, gather up as much as he can of his civilized heritage and transmit it to his children. And to his final breath he will be grateful for this inexhaustible legacy, knowing that it is our nourishing mother and our lasting life.
- The Lessons of History, Will & Ariel Durant
---
Feb 23, 2024
> The road to serfdom consists of working exponentially harder for a currency growing exponentially weaker.
- Vijay Boyapati, The Bullish Case for Bitcoin
---
Feb 16, 2024
> Loneliness is a tax you have to pay to atone for a certain complexity of mind.
- Alain de Botton
---
Feb 16, 2024
> So many people today — and even professional scientists— seem to me like someone who has seen thousands of trees but has never seen a forest. A knowledge of the historic and philosophical background gives that kind of independence from prejudices of his generation from which most scientists are suffering. This independence created by philosophical insight is — in my opinion — the mark of distinction between a mere artisan or specialist and a real seeker after truth.
- Albert Einstein to Robert A. Thornton, 7 December 1944, EA 61-574
---
Feb 6, 2024
> I see now more clearly than ever before that even our greatest troubles spring from something that is as admirable and sound as it is dangerous -- from our impatience to better the lot of our fellows.
- Karl Popper, The Open Society and it's Enemies, preface to the second edition
---
Feb 5, 2024
> [...] the most unfortunate of men is he who has not learned how to bear misfortune [...] men ought to order their lives as if they were fated to live both a long and a short time, [and] wisdom should be cherished as a means of traveling from youth to old age, for it is more lasting than any other possession.
- Bias of Priene, quoted from The Life of Greece by Will Durant, Ch. VI The Great Migration
---
Feb 2, 2024
> [...] teenagers are always on duty as conformists.
- Paul Graham, Why Nerds are Unpopular
---
January 1, 2024
> Why, Oppenheimer knows about everything. He can talk to you about anything you bring up. Well, not exactly. I guess there are a few things he doesn't know about. He doesn't know anything about sports.
- General Leslie Groves, quoted from American Prometheus: The Triumph and Tragedy of J. Robert Oppenheimer, pp. 185-186.
---
December 22, 2023
> Life everywhere is life, life is in ourselves and not in the external. There will be people near me, and to be a human being among human beings, and remain one forever, no matter what misfortunes befall, not to become depressed, and not to falter -- this is what life is, herein lies its task.
- Fyodor Dostoevsky, in a letter to his brother, the day he was pardoned from execution by firing squad.
---
December 13, 2023
> Math constitutes the language through which alone we can adequately express the great facts of the natural world. And it allows us to portray the changes of mutual relationship that unfold in creation. It is the instrument through which the weak mind of man can most effectually read his creator's works.
- Ada Lovelace, quoted from The Innovators by Walter Isaacson, Ch. 1
---
December 9, 2023
> It is wrong to think that belief in freedom always leads to victory; we must always be prepared for it to lead to defeat. If we choose freedom, then we must be prepared to perish along with it.
> No, we do not choose political freedom because it promises us this or that. We choose it because it makes possible the only dignified form of human coexistence, the only form in which we can be fully responsible for ourselves. Whether we realize its possibilities depends on all kinds of things — and above all on ourselves.
- Karl Popper, On Freedom
---
December 7, 2023
> I think that there is only one way to science - or to philosophy, for that matter: to meet a problem, to see its beauty and fall in love with it; to get married to it and to live with it happily, till death do ye part - unless you should meet another and even more fascinating problem or unless, indeed, you should obtain a solution. But even if you do obtain a solution, you may then discover, to your delight, the existence of a whole family of enchanting, though perhaps difficult, problem children, for whose welfare you may work, with a purpose, to the end of your days.
- Karl Popper, Realism and the Aim of Science
---
December 7, 2023
> Hence, men who are governed by reason [...] desire for themselves nothing, which they do not also desire for the rest of mankind
- Spinoza, Part IV, Prop XVIII
---
September 26, 2023
> Among the nations who have adopted the Mosaic history of the world, the ark of Noah has been of the same use as was formerly to the Greeks and Romans the siege of Troy. On a narrow basis of acknowledged truth an immense but rude superstructure of fable has been erected,[...]
- Decline and Fall of the Roman Empire, Chapter IX, p. 240
---

View File

@@ -1,104 +0,0 @@
---
title: "Creations"
siteTitle: "Creations"
tags: software
---
# **Creations**
---
_More very fun things are on the way, stay tuned..._
<!-- --- -->
<!---->
<!-- ## [nix.fun](https://nix.fun) -->
<!---->
<!-- A work-in-progress website dedicated to helping people solve problems with [Nix](https://github.com/NixOS/nix). -->
---
## [Bitcoin QR Web Component: `bitcoin-qr`](https://bitcoin-qr-demo.netlify.app)
<picture>
<source srcset="/images/contributions/bitcoin-qr.avif" type="image/avif">
<img src="/images/contributions/bitcoin-qr.webp" alt="Image of bitcoin-qr samples" width="800" height="443" loading="lazy">
</picture>
_Add your company's image and style the QR to match!_
I created a [web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_Components) to make it easy to create BIP 21 compatible QR codes with a lot of developer-and-user-friendly defaults. One problem I consistently ran into when developing lightning applications was having to repeatedly build a QR code component with HTTP polling to check for payment, in addition to making many UX decisions about when to use BIP 21 for `bitcoin:` and `lightning:` URI prefixes and how to handle query params. Additionally, I found myself reimplementing a component that did all this in each framework (i.e. React, Svelte) I was using. As far as I know, everyone who's building UIs in bitcoin has to keep redoing this work.
I decided it would be valuable (to myself, if nobody else) to build a universal web component that came with all this functionality out of the box with maximum configuration but opinionated defaults, that could be used in any [framework](https://qr-code-styling.com) or in pure HTML. And for extra fun, it's built on a framework that allows a lot of styling customization!
Feedback on this would be very much appreciated, please feel free to [open an issue](https://github.com/thebrandonlucas/bitcoin-qr) if you find a problem or have any suggestions for improvement!
---
### Archive
**Old ideas and proof-of-concepts that never went anywhere (the vast majority of my projects) but that were very fun, interesting, or operating at the cutting edge in their time**
---
### [nostrlytics.com](https://github.com/thebrandonlucas/nostrlytics)
When the first major hype wave for [Nostr](https://nostr.com/) occurred and the Bitcoin community didn't know all the problems we'd face building on Nostr, I was learning the horrifyingly painful yet flexible [D3](https://d3js.org/) and decided to use a little bit of what I learned to build a little website with a chart that allowed you to input your public key and a relay websocket endpoint to view some basic statistics about your "profile".
Of course, all we Nostrlytes were taught a powerful lesson in network effects by the all-encompassing [Twitter/x.com](https://x.com) behemoth, and it hasn't really caught on to this day despite the huge amount of hype and developer effort in the Bitcoin community.
That said, I still believe that the public-key-based identity system, combined with Bitcoin micropayments for skin-in-the-game interactions and valueless bot-posts that thrive on X, Nostr is one of the simplest, freest, and most decentralized forms of communication we've invented that could actually work.
As behemoth centralized services continue to degrade and add anti-features due to their illusions of invincibility, these alternatives will hopefully become ever-more usable and appealing to broad audiences.
---
[Video LSAT](https://github.com/thebrandonlucas/video-lsat)
The first idea I was interested in when I discovered the magic of the [Lightning Network](https://lightning.network) was the idea of subscriptionless video streaming. The idea that you could simply pay-as-you-go, as opposed to the Subscription Hell of modernity, was hyper-appealing. People could save money, have no ads, and pay pennies to watch full length movies, and both creator and customer would be better off. It would utilize LSATs (now renamed [L402](https://www.l402.org/) after the HTTP status code) to accept Bitcoin Lightning payments to watch a video. That was the idea, anyway.
Aside from latency issues caused by the number of requests you'd need to do to make this work at the micro-scale (I was insane enough to try to do payments by the second), I found that the real problem, like with most things, wasn't technological. It wasn't that we didn't _know how_ to do it or that _nobody had tried_. It was the horrifying realization that most people are pretty complacently fine with their ads (if it means they get to consume "for free" -- as if their both their time and data didn't hold immense value) and their subscriptions (which they often forgot they were paying for after signing up). The surest sign to me that Americans have far too much money for their own good, despite our incessant griping, is that we have so little imagination and will for how life could be better in every way if we were willing to make the smallest up-front sacrifice.
The most common response I got from people when I proposed this idea to them was "Why would I pay for what I can get for free?", without realizing that we are selling little pieces of our souls this way, and that the costs of actually watching a video would be so small it would actually be cheaper relative to the time saved.
Anyway, a competing project that did it better anyway emerged around the same time, [lightning.video](https://lightning.video/), and essentially became half a porn site, half a Bitcoin site. Such is life in the Bitcoin world.
---
### [SatGPT](https://github.com/thebrandonlucas/satgpt)
In the earlier days of the GPTs, the only way you could use them was to have a subscription from a big provider like ChatGPT. Taking a cue from the video-lsat project above, I built a little system under which the server company could simply take an API key for themselves and allow users to "top up" an account anonymously by paying in bitcoin micropayments. Very fun project whose idea was supplanted and done better by [ppq.ai](https://ppq.ai/), which is a service I love and highly recommend.
---
### [Micropayments Demo](https://github.com/thebrandonlucas/micropayments)
When I was helping mentor at the 2023 MIT Bitcoin Hackathon, I built a simple demo app to show how to use micropayments with Lightning (on LND/Voltage nodes).
---
### [BLUCoin](https://github.com/thebrandonlucas/BLUCoin)
After reading Jimmy Song's [Programming Bitcoin](https://github.com/jimmysong/programmingbitcoin) book, I decided to build a minimal bitcoin-based cryptocurrency from scratch with Python, which was an immensely gratifying and difficult experience.
---
### [thebestme](https://github.com/thebrandonlucas/thebestme)
I made an attempt by building a mental health app in React Native designed to help people take control of their mental health by utilizing thought-challenging journaling techniques, habit tracking, and mood tracking.
---
### [Combat Deepfakes](https://github.com/thebrandonlucas/combat-deepfakes)
Back when [Dapps](https://ethereum.org/en/dapps) were all the rage with Ethereum and before I became disillusioned with it in favor of Bitcoin, [Deepfakes](https://en.wikipedia.org/wiki/Deepfake) were becoming a major concern as the first machine learning technology which could convincingly create fake face swapping videos. I realized we could use the blockchain to create a time-stamping system in which all that was needed to "prove" which video was the real one, was to hash the video and put it on the blockchain, and if another video came later attempting to claim _it_ was the real one, just compare the hash and timestamp of the original.
I thought this may become a cataclysmic problem in our present day, where perhaps politicians would be made to make proclamations of war or revenge porn videos (which are actually, sadly, real), but so far in 2025 this seems to have mainly been used to make funny meme videos and at worst cause very temporary political stirs which are quickly shut down, and in regards to the porn problem, so many people are either voluntarily naked online or have already had photos leaked anyway that the taboo of internet nudity associated with your face has been rapidly diluted, turning would-be reputation-enders into merely deeply embarrassing ephemeral mishaps.
So so far, this ended up not being anywhere near the problem I thought it would be, but perhaps the full ramifications of this have not yet come home to roost.
---
### [lightscameraalabama](https://lightscameraalabama.com)
While I was a student at University of Alabama I built a website in React.js to host historical videos for an Honors College program which encouraged students to make films about Alabama history.

View File

@@ -1,133 +0,0 @@
---
title: Contributions
tags: software
---
# **Contributions**
---
I have contributed to many projects, mostly open source. Here are some that I'm particularly proud of.
---
## [Voltage](https://www.voltage.cloud)
<picture>
<source srcset="/images/contributions/voltage-dash-5.avif" type="image/avif">
<img src="/images/contributions/voltage-dash-5.webp" alt="Voltage Dashboard" width="800" height="487" loading="lazy">
</picture>
I'm on the frontend engineering team at [Voltage](https://voltage.cloud/). We've built an easy to use Bitcoin Lightning Payments API. If you've interacted with our product at all from the browser, chances are I worked on it!
I am grateful to this company for cultivating a culture of giving back to the community through open source development via [FOSS Fridays](https://voltage.cloud/blog/foss-friday/foss-fridays-at-voltage). It has enabled me to connect more broadly with the Bitcoin community and expand my skills as a software engineer. Its a great team on a great [mission](https://voltage.cloud/about); the spirit of [definite optimism](https://boxkitemachine.net/posts/zero-to-one-peter-thiel-definite-vs-indefinite-thinking) is alive and well here.
---
## [Block Clock](https://github.com/voltagecloud/block-clock)
<picture>
<source srcset="/images/contributions/blockclock.avif" type="image/avif">
<img src="/images/contributions/blockclock.webp" alt="Block Clock Image" width="800" height="354" loading="lazy">
</picture>
The frontend and design team at Voltage built this `block-clock` web component that connects to Bitcoin Core to display the distribution of block times in a standard 12-hour clock face, using the beautiful designs from the [Bitcoin Core App](https://bitcoincore.app/?ref=blucas.ghost.iohttps://bitcoincore.app) project to do it. We use it in our own [Bitcoin Core Nodes](https://www.voltage.cloud/bitcoin-core) product, but we wanted to make this something that could be shared with the community. We built it as a web component so it's easy to run it in any browser environment, whether you're using frameworks like React or Sveltekit, or just want to drop it directly into an HTML page. If you have a Bitcoin Core node, give it a try!
Check out the [Github Repo](https://github.com/voltagecloud/block-clock) or watch our discussion with the [Bitcoin Design Community](https://www.youtube.com/watch?v=igKZ-IPlADY) to see how it works!
---
## [Payjoin](https://payjoin.org)
<picture>
<source srcset="/images/contributions/payjoin.avif" type="image/avif">
<img src="/images/contributions/payjoin.webp" alt="Payjoin Example Image" width="800" height="551" loading="lazy">
</picture>
Payjoin is a protocol designed to assist bitcoin scaling, help save fees, and preserve privacy, whose adoption by even a small minority of wallets could have dramatically positive effects for all bitcoin users.
I built the current version of [payjoin.org](https://payjoin.org) with help from [Dan Gould](https://dangould.dev). I'm also contributing to [Payjoin Dev Kit (PDK)](https://dangould.dev), a tiny library that helps wallets integrate Payjoin, and includes a reference implementation, `payjoin-cli`, that showcases its features.
---
## [Interactive Payjoin](https://github.com/thebrandonlucas/interactive-payjoin)
1st place sub-project at the [MIT Bitcoin Hackathon](https://mitbitcoin.devpost.com/). A proof-of-concept demo website showcasing the first instance of the use of [Payjoin Dev Kit](https://payjoindevkit.org/) in the browser by compiling the Rust library to [WASM](https://webassembly.org/).
---
## [bolt12.org](https://bolt12.org)
<picture>
<source srcset="/images/contributions/bolt12.org.avif" type="image/avif">
<img src="/images/contributions/bolt12.org.webp" alt="Image of Bolt12 Homepage" width="800" height="498" loading="lazy">
</picture>
BOLT 12 is a specification for implementing offers, which massively improves the lightning user experience by making QR codes:
- Reusable
- Smaller
- Capable of sending you money, like an ATM, as well as receiving
It generates these QRs in-band, as opposed to the out-of-band [LNURL](https://voltage.cloud/blog/lightning-network-faq/how-does-lnurl-work-enhancing-lightnings-user-experience) format, which requires a web server that generates invoices on behalf of your lightning node and is essentially a "hack" on previous limitations of lightning. We can make huge improvements to both the developer and user experience by adopting BOLT 12, and this website hopes to encourage its adoption.
I collaborated with master designer [@sbddesign](https://x.com/StephenDeLorme) to build the current version of bolt12.org.
---
## [Doppler](https://github.com/tee8z/doppler)
<picture>
<source srcset="/images/contributions/doppler.avif" type="image/avif">
<img src="/images/contributions/doppler.webp" alt="Image showing usage of Doppler" width="800" height="472" loading="lazy">
</picture>
Doppler is a Domain-Specific Language (DSL) created by [@tee8z](https://x.com/Tee8z) that allows you to write reusable scripts to create local [regtest](https://developer.bitcoin.org/examples/testing.html) (and [Mutinynet](https://blog.mutinywallet.com/mutinynet)!) environments in any configuration you like. The scripts allow for easy reproducibility of any scenario you can think of and dramatically expands the possibilities for testing bitcoin and lightning applications, improving on one of the main limitations of testing software like [Polar](https://lightningpolar.com) (also a great project).
Alongside [@tee8z](https://x.com/Tee8z), I built the first iteration of the frontend that allows you to visualize and build scripts using the Scratch block programming language.
---
## [Satogram](https://satogram.xyz)
<picture>
<source srcset="/images/contributions/satogram.avif" type="image/avif">
<img src="/images/contributions/satogram.webp" alt="Image Showing Satogram Logo" width="800" height="407" loading="lazy">
</picture>
Wouldn't it be nice to get paid to see ads, instead of today, where your data is harvested for profit and you get barraged with internet-polluting ads?
Satogram is a project that advertisers can use to pay to send ads over the Lightning Network. It's like spam email, except you're getting paid!
Satogram came out of a hackathon project at Tabconf 2023 led by [@BitcoinCoderBob](https://x.com/BitcoinCoderBob). I built the frontend at the hackathon. As of this writing, Satogram has been used to send a total of 1,515,896 advertisements!
---
## [Alby](https://getalby.com)
<picture>
<source srcset="/images/contributions/alby.avif" type="image/avif">
<img src="/images/contributions/alby.webp" alt="Image showing Alby homepage" width="800" height="453" loading="lazy">
</picture>
Alby is a popular lighting wallet browser extension that comes with a wide variety of innovative features. In my first real contributions to open source, I helped build the internationalization flow to allow the app to be translated to a variety of different languages. I also added the ability to [connect and make payments via signets such as Mutinynet](https://github.com/getAlby/lightning-browser-extension/pull/3128), to allow for easier testing of web applications with [WebLN](https://webln.guide). Thanks to the Alby team for taking the time to help me contribute to open source bitcoin in the first place, kicking off this crazy exciting adventure.
---
## [QRty](https://qr-ai.netlify.app)
<picture>
<source srcset="/images/contributions/qrty.avif" type="image/avif">
<img src="/images/contributions/qrty.webp" alt="Image showing QRty homepage" width="800" height="354" loading="lazy">
</picture>
This project used [stable diffusion](https://en.wikipedia.org/wiki/Stable_Diffusion) to generate artistic QR codes based on a prompt. Since the only options to generate these QR codes at the time were through subscription services, we thought it would be nice if people could make small payments per query with bitcoin (an idea [obviously befitting to AI services](https://hivemind.vc/ai) in general, but for whatever reason this idea hasn't broken through past us in the bitcoin bubble to mainstream consciousness yet).
It's no longer active due to relatively low use, but we were really proud of the result and you can still see some examples of scan-able codes we created on the site and in these Twitter posts. Built on Voltage with my friend [@LightningK0ala](https://x.com/LightningK0ala).
<picture>
<source srcset="/images/contributions/qr-walt.avif" type="image/avif">
<img src="/images/contributions/qr-walt.webp" alt="Image of Walter White as a QR Code" width="800" height="972" loading="lazy">
</picture>
_I wish I'd saved the prompt that generated this._

View File

@@ -1,20 +0,0 @@
---
title: "Talks & Events"
tags: software
---
# **Talks & Events**
---
## [MIT Bitcoin Hackathon 2025 1st Place Winner - Payjoin Integrations](https://x.com/satsie/status/1909081177765364080)
- The Payjoin Team won the 2025 MIT Bitcoin Hackathon by building proof of concept integrations for [boltz.exchange](https://boltz.exchange/), [Liana](https://wizardsardine.com/liana/), and the first known [implementation](https://github.com/thebrandonlucas/interactive-payjoin) of Payjoin in the browser using WASM bindings from Payjoin Dev Kit [PDK](payjoindevkit.org) to Javascript, which doubled as an interactive tutorial for how Payjoin works.
## [Supercharging Transactions with Async Payjoin - TABConf 6](https://www.youtube.com/watch?v=vPzvLxv0YfQ)
- I gave a talk on the origins, history, and implications of the newly developed [BIP 77](https://github.com/bitcoin/bips/blob/master/bip-0077.mediawiki) a.k.a Async Payjoin, and how this dramatic improvement in Payjoin UX opens the door to mass wallet adoption and thus huge financial and privacy savings for all of Bitcoin at large.
## [Micropayments and the Lightning Network - Voltage Workshop](https://www.youtube.com/watch?v=6Vq6foKst54&t=124s)
- I did a workshop on the history of micropayments on the web, why they failed, and how the invention of the [Lightning Network](https://lightning.network) on Bitcoin is making that original dream for the web a reality.

View File

@@ -1,48 +0,0 @@
---
title: "Technology"
tags: software
---
# **Technology**
---
## Software
### Terminal or TUI (Terminal UI) tools
I'm a growing fan of the speed, universality, and simplicity of terminal-based tools and use them more and more exclusively in my software repertoire.
- [neovim](https://neovim.io): The ultimate terminal-based editor, once you take the time to learn its ins and outs. The only thing lacking is when genuinely useful new proprietary software (i.e. AI coding agents) comes out, there's really no financial incentive for anyone to build on neovim vs the bought and paid for editors like [Windsurf](https://codeium.com/windsurf) or [Cursor](https://cursor.com) which most devs will use, so we have to wait for charitable hobbyists to create suitable integrations, and I've run into plenty of frustration when trying to get coding agents to work within `neovim`.
- [nb](https://xwmx.github.io/nb): A beautifully simple note-taking system for the terminal. Uses `git` to manage versions, has built-in file encryption, and much more. I use this with `neovim` for everything I write.
- [yazi](https://yazi-rs.github.io): An awesome file navigator that's very powerful and flexible. Stop using `cd <path>` to navigate everywhere!
- [`lazygit`](https://github.com/jesseduffield/lazygit): This is the best way to use `git` I have ever discovered. It truly makes things easy and doesn't require you to memorize all those weird commands. Also integrates with `neovim`
### Languages
- [Elm](https://elm-lang.org): This is one of the few (if only?) language I've used where I actually love the _language_ and not just what I'm building. Despite its sad lack of leadership and usage falling by the wayside, the idea of a language that makes websites never cause exceptions (in other words, if it compiles, it works) is a huge leap forward for web development. I don't know if I can go back to Javascript after having this experience, and may pursue one of Elm's actively maintained spiritual successors, such as [Gleam](https://gleam.run). This website is built in Elm, by the way.
### Package Management
- [Nix](https://nixos.org): Technically, Nix is a language, package manager, and operating system all at once, but it's purely functional guarantees have incredible downstream consequences which I write about at [nix.fun](https://nix.fun)
### Operating System
- [NixOS](https://nixos.org)
- [Hyprland](https://hyprland.org)
### Bitcoin
- [Sparrow Wallet](https://sparrowwallet.com)
- [SeedSigner](https://seedsigner.com)
### Hardware
- [GrapheneOS](https://grapheneos.org): The most secure smartphone in existence
- [ESP-32](https://www.espressif.com/en/products/socs/esp32)
- [Framework Laptop 13](https://frame.work)
- [Raspberry PI 0](https://www.raspberrypi.com/products/raspberry-pi-zero)
### Fun
- [btop](https://github.com/aristocratos/btop)

View File

@@ -1,102 +0,0 @@
---
title: Books
tags: journal
---
# **Books**
Below is a list of books and essays that have impacted me deeply:
---
## **History**
- _History_ by Ralph Waldo Emerson
### Ancient Greece
- _The Life of Greece_ by Will Durant
### Ancient Rome
- _Caesar and Christ_ by Will Durant
- _Decline and Fall of the Roman Empire_ by Edward Gibbon
### America
- _John Adams_ by David McCullough
- _Washington: A Life_ by Ron Chernow
- _Hamilton_ by Ron Chernow
- _Thomas Jefferson: The Art of Power_ by Jon Meacham
### Russia
- _The Gulag Archipelago_ by Aleksandr Solzhenitsyn
### Medicine
- _Awakenings_ by Oliver Sacks
---
## **Biography**
- _Surely Youre Joking, Mr. Feynman!_ by Richard Feynman
---
## **Philosophy**
- _Self-Reliance_ by Ralph Waldo Emerson
### Politics
- _The Open Society and its Enemies_ by Karl Popper
- _Beauty_ by Roger Scruton
### Roman
- _Meditations_ by Marcus Aurelius
- _On the Shortness of Life_ by Seneca
- _On Friendship_ by Cicero
### Christian
- _A Confession_ by Lev Tolstoy
- _Fear and Trembling_ by Kierkegaard
- _Answer to Job_ by Carl Jung
### Cognition
- _Gödel, Escher, Bach_ by Douglas Hofstadter
### Morality
- _Beyond Good and Evil_ by Friedrich Nietzsche
- _Mans Search for Meaning_ by Viktor Frankl
- _The Genealogy of Morals_ by Friedrich Nietzsche
---
## **Novels**
- _The Brothers Karamazov_ by Dostoevsky
- _Crime and Punishment_ by Fyodor Dostoevsky
## **Technology**
- _The Sovereign Individual_ by James Dale Davidson & William Rees-Mogg
### Bitcoin
- _Mastering the Lightning Network_ by Andreas Antonopoulos
- _Programming Bitcoin_ by Jimmy Song
- _Mastering Bitcoin_ by Andreas Antonopoulos
### Privacy
- _Extreme Privacy_ by Michael Bazzell
## **Psychology**
- _Modern Man in Search of a Soul_ by Carl Jung

View File

@@ -1,22 +0,0 @@
---
title: "Work"
tags: software
---
# **Work**
---
I started out by working at Chick-fil-A in the kitchen, then as a programmer for corporate.
Most of my work since then has been in Bitcoin: one of the most fascinating
technological developments of our time and the most promising of the emergent currencies.
I work at Voltage in pursuit of that curiosity, working on streamlining
payments via the Lightning Network.
I also work on numerous Bitcoin side projects, primarily Payjoin: a novel method
that makes transactions more scalable, cheap, private, efficient, and fun.
It is believed that if we can get even a small percentage of the total number of Bitcoin transactions
to use Payjoin, we can break the most common metric used to spy on people today: The Common-input Ownership Heuristic.

View File

@@ -1,79 +0,0 @@
// YAML-like frontmatter parser
// Parses the --- delimited frontmatter block from markdown files
pub type Frontmatter =
| Frontmatter(String, String, String, String)
// title date desc tagsRaw
pub type ParseResult =
| ParseResult(Frontmatter, String)
// front body
pub fn parse(content: String): ParseResult = {
let lines = String.lines(content);
// Skip first --- line, collect key: value pairs until next ---
let result = List.fold(lines, (false, false, "", "", "", "", ""), fn(acc: (Bool, Bool, String, String, String, String, String), line: String): (Bool, Bool, String, String, String, String, String) => {
let inFront = acc.0;
let pastFront = acc.1;
let title = acc.2;
let date = acc.3;
let desc = acc.4;
let tags = acc.5;
let body = acc.6;
if pastFront then
(inFront, pastFront, title, date, desc, tags, body + line + "\n")
else if String.trim(line) == "---" then
if inFront then
// End of frontmatter
(false, true, title, date, desc, tags, body)
else
// Start of frontmatter
(true, false, title, date, desc, tags, body)
else if inFront then {
// Parse key: value
match String.indexOf(line, ": ") {
Some(idx) => {
let key = String.trim(String.substring(line, 0, idx));
let rawVal = String.trim(String.substring(line, idx + 2, String.length(line)));
// Strip surrounding quotes if present
let val = if String.startsWith(rawVal, "\"") then
String.substring(rawVal, 1, String.length(rawVal) - 1)
else
rawVal;
if key == "title" then
(inFront, pastFront, val, date, desc, tags, body)
else if key == "date" then
(inFront, pastFront, title, val, desc, tags, body)
else if key == "description" then
(inFront, pastFront, title, date, val, tags, body)
else if key == "tags" then
(inFront, pastFront, title, date, desc, val, body)
else
(inFront, pastFront, title, date, desc, tags, body)
},
None => (inFront, pastFront, title, date, desc, tags, body)
}
} else
(inFront, pastFront, title, date, desc, tags, body)
});
let front = Frontmatter(result.2, result.3, result.4, result.5);
ParseResult(front, result.6)
}
pub fn getTitle(f: Frontmatter): String =
match f { Frontmatter(t, _, _, _) => t }
pub fn getDate(f: Frontmatter): String =
match f { Frontmatter(_, d, _, _) => d }
pub fn getDesc(f: Frontmatter): String =
match f { Frontmatter(_, _, d, _) => d }
pub fn getTagsRaw(f: Frontmatter): String =
match f { Frontmatter(_, _, _, t) => t }
pub fn getTags(f: Frontmatter): List<String> =
match f { Frontmatter(_, _, _, t) =>
if t == "" then []
else String.split(t, " ")
}

View File

@@ -1,729 +0,0 @@
type SiteConfig =
| SiteConfig(String, String, String, String, String, String, String)
type Frontmatter =
| Frontmatter(String, String, String, String)
type ParseResult =
| ParseResult(Frontmatter, String)
type Page =
| Page(String, String, String, String, String)
type FMState =
| FMState(Bool, Bool, String, String, String, String, String)
type BState =
| BState(String, String, Bool, String, String, Bool, String, Bool, String, Bool)
type TagEntry =
| TagEntry(String, String, String, String, String)
fn loadConfig(path: String): SiteConfig with {File} = {
let raw = File.read(path)
let json = match Json.parse(raw) {
Ok(j) => j,
Err(_) => match Json.parse("\{\}") {
Ok(j2) => j2,
},
}
let title = match Json.get(json, "siteTitle") {
Some(v) => match Json.asString(v) {
Some(s) => s,
None => "Site",
},
None => "Site",
}
let url = match Json.get(json, "siteUrl") {
Some(v) => match Json.asString(v) {
Some(s) => s,
None => "",
},
None => "",
}
let author = match Json.get(json, "author") {
Some(v) => match Json.asString(v) {
Some(s) => s,
None => "",
},
None => "",
}
let desc = match Json.get(json, "description") {
Some(v) => match Json.asString(v) {
Some(s) => s,
None => "",
},
None => "",
}
let contentDir = match Json.get(json, "contentDir") {
Some(v) => match Json.asString(v) {
Some(s) => s,
None => "content",
},
None => "content",
}
let outputDir = match Json.get(json, "outputDir") {
Some(v) => match Json.asString(v) {
Some(s) => s,
None => "_site",
},
None => "_site",
}
let staticDir = match Json.get(json, "staticDir") {
Some(v) => match Json.asString(v) {
Some(s) => s,
None => "static",
},
None => "static",
}
SiteConfig(title, url, author, desc, contentDir, outputDir, staticDir)
}
fn cfgTitle(c: SiteConfig): String =
match c {
SiteConfig(t, _, _, _, _, _, _) => t,
}
fn cfgDesc(c: SiteConfig): String =
match c {
SiteConfig(_, _, _, d, _, _, _) => d,
}
fn cfgContentDir(c: SiteConfig): String =
match c {
SiteConfig(_, _, _, _, cd, _, _) => cd,
}
fn cfgOutputDir(c: SiteConfig): String =
match c {
SiteConfig(_, _, _, _, _, od, _) => od,
}
fn cfgStaticDir(c: SiteConfig): String =
match c {
SiteConfig(_, _, _, _, _, _, sd) => sd,
}
fn fmFoldLine(acc: FMState, line: String): FMState =
match acc {
FMState(inFront, pastFront, title, date, desc, tags, body) => if pastFront then FMState(inFront, pastFront, title, date, desc, tags, body + line + "
") else if String.trim(line) == "---" then if inFront then FMState(false, true, title, date, desc, tags, body) else FMState(true, false, title, date, desc, tags, body) else if inFront then match String.indexOf(line, ": ") {
Some(idx) => {
let key = String.trim(String.substring(line, 0, idx))
let rawVal = String.trim(String.substring(line, idx + 2, String.length(line)))
let val = if String.startsWith(rawVal, "\"") then String.substring(rawVal, 1, String.length(rawVal) - 1) else rawVal
if key == "title" then FMState(inFront, pastFront, val, date, desc, tags, body) else if key == "date" then FMState(inFront, pastFront, title, val, desc, tags, body) else if key == "description" then FMState(inFront, pastFront, title, date, val, tags, body) else if key == "tags" then FMState(inFront, pastFront, title, date, desc, val, body) else acc
},
None => acc,
} else acc,
}
fn parseFrontmatter(content: String): ParseResult = {
let lines = String.lines(content)
let init = FMState(false, false, "", "", "", "", "")
let result = List.fold(lines, init, fmFoldLine)
match result {
FMState(_, _, title, date, desc, tags, body) => ParseResult(Frontmatter(title, date, desc, tags), body),
}
}
fn fmTitle(f: Frontmatter): String =
match f {
Frontmatter(t, _, _, _) => t,
}
fn fmDate(f: Frontmatter): String =
match f {
Frontmatter(_, d, _, _) => d,
}
fn fmTagsRaw(f: Frontmatter): String =
match f {
Frontmatter(_, _, _, t) => t,
}
fn fmTags(f: Frontmatter): List<String> =
match f {
Frontmatter(_, _, _, t) => if t == "" then [] else String.split(t, " "),
}
fn pgDate(p: Page): String =
match p {
Page(d, _, _, _, _) => d,
}
fn pgTitle(p: Page): String =
match p {
Page(_, t, _, _, _) => t,
}
fn pgSlug(p: Page): String =
match p {
Page(_, _, s, _, _) => s,
}
fn pgTags(p: Page): String =
match p {
Page(_, _, _, t, _) => t,
}
fn pgContent(p: Page): String =
match p {
Page(_, _, _, _, c) => c,
}
fn teTag(e: TagEntry): String =
match e {
TagEntry(t, _, _, _, _) => t,
}
fn teTitle(e: TagEntry): String =
match e {
TagEntry(_, t, _, _, _) => t,
}
fn teDate(e: TagEntry): String =
match e {
TagEntry(_, _, d, _, _) => d,
}
fn teSlug(e: TagEntry): String =
match e {
TagEntry(_, _, _, s, _) => s,
}
fn teSection(e: TagEntry): String =
match e {
TagEntry(_, _, _, _, s) => s,
}
fn escapeHtml(s: String): String = String.replace(String.replace(String.replace(s, "&", "&amp;"), "<", "&lt;"), ">", "&gt;")
fn escapeHtmlCode(s: String): String = String.replace(String.replace(String.replace(s, "&", "&amp;"), "<", "&lt;"), ">", "&gt;")
fn slugFromFilename(filename: String): String = if String.endsWith(filename, ".md") then String.substring(filename, 0, String.length(filename) - 3) else filename
fn formatDate(isoDate: String): String = {
if String.length(isoDate) < 10 then isoDate else {
let year = String.substring(isoDate, 0, 4)
let month = String.substring(isoDate, 5, 7)
let day = String.substring(isoDate, 8, 10)
let monthName = if month == "01" then "Jan" else if month == "02" then "Feb" else if month == "03" then "Mar" else if month == "04" then "Apr" else if month == "05" then "May" else if month == "06" then "Jun" else if month == "07" then "Jul" else if month == "08" then "Aug" else if month == "09" then "Sep" else if month == "10" then "Oct" else if month == "11" then "Nov" else "Dec"
year + " " + monthName + " " + day
}
}
fn basename(path: String): String =
match String.lastIndexOf(path, "/") {
Some(idx) => String.substring(path, idx + 1, String.length(path)),
None => path,
}
fn dirname(path: String): String =
match String.lastIndexOf(path, "/") {
Some(idx) => String.substring(path, 0, idx),
None => ".",
}
fn sortInsert(sorted: List<Page>, item: Page): List<Page> = insertByDate(sorted, item)
fn sortByDateDesc(items: List<Page>): List<Page> = List.fold(items, [], sortInsert)
fn insertByDate(sorted: List<Page>, item: Page): List<Page> = {
match List.head(sorted) {
None => [item],
Some(first) => if pgDate(item) >= pgDate(first) then List.concat([item], sorted) else match List.tail(sorted) {
Some(rest) => List.concat([first], insertByDate(rest, item)),
None => [first, item],
},
}
}
fn findClosingFrom(text: String, i: Int, len: Int, delim: String, delimLen: Int): Option<Int> = if i + delimLen > len then None else if String.substring(text, i, i + delimLen) == delim then Some(i) else findClosingFrom(text, i + 1, len, delim, delimLen)
fn findClosing(text: String, start: Int, len: Int, delim: String): Option<Int> = findClosingFrom(text, start, len, delim, String.length(delim))
fn isLetterOrSlash(ch: String): Bool = ch == "/" || ch == "!" || ch == "a" || ch == "b" || ch == "c" || ch == "d" || ch == "e" || ch == "f" || ch == "g" || ch == "h" || ch == "i" || ch == "j" || ch == "k" || ch == "l" || ch == "m" || ch == "n" || ch == "o" || ch == "p" || ch == "q" || ch == "r" || ch == "s" || ch == "t" || ch == "u" || ch == "v" || ch == "w" || ch == "x" || ch == "y" || ch == "z" || ch == "A" || ch == "B" || ch == "C" || ch == "D" || ch == "E" || ch == "F" || ch == "G" || ch == "H" || ch == "I" || ch == "J" || ch == "K" || ch == "L" || ch == "M" || ch == "N" || ch == "O" || ch == "P" || ch == "Q" || ch == "R" || ch == "S" || ch == "T" || ch == "U" || ch == "V" || ch == "W" || ch == "X" || ch == "Y" || ch == "Z"
fn processInlineFrom(text: String, i: Int, len: Int, acc: String): String = {
if i >= len then acc else {
let ch = String.substring(text, i, i + 1)
if ch == "*" then if i + 1 < len then if String.substring(text, i + 1, i + 2) == "*" then match findClosing(text, i + 2, len, "**") {
Some(end) => processInlineFrom(text, end + 2, len, acc + "<strong>" + processInline(String.substring(text, i + 2, end)) + "</strong>"),
None => processInlineFrom(text, i + 1, len, acc + ch),
} else match findClosing(text, i + 1, len, "*") {
Some(end) => processInlineFrom(text, end + 1, len, acc + "<em>" + processInline(String.substring(text, i + 1, end)) + "</em>"),
None => processInlineFrom(text, i + 1, len, acc + ch),
} else acc + ch else if ch == "_" then if i + 1 < len then match findClosing(text, i + 1, len, "_") {
Some(end) => processInlineFrom(text, end + 1, len, acc + "<em>" + processInline(String.substring(text, i + 1, end)) + "</em>"),
None => processInlineFrom(text, i + 1, len, acc + ch),
} else acc + ch else if ch == "`" then match findClosing(text, i + 1, len, "`") {
Some(end) => processInlineFrom(text, end + 1, len, acc + "<code>" + escapeHtml(String.substring(text, i + 1, end)) + "</code>"),
None => processInlineFrom(text, i + 1, len, acc + ch),
} else if ch == "!" then if i + 1 < len then if String.substring(text, i + 1, i + 2) == "[" then match findClosing(text, i + 2, len, "](") {
Some(end) => match findClosing(text, end + 2, len, ")") {
Some(urlEnd) => {
let imgTag = acc + "<img src=\"" + String.substring(text, end + 2, urlEnd) + "\" alt=\"" + String.substring(text, i + 2, end) + "\">"
processInlineFrom(text, urlEnd + 1, len, imgTag)
},
None => processInlineFrom(text, i + 1, len, acc + ch),
},
None => processInlineFrom(text, i + 1, len, acc + ch),
} else processInlineFrom(text, i + 1, len, acc + ch) else acc + ch else if ch == "[" then match findClosing(text, i + 1, len, "](") {
Some(end) => match findClosing(text, end + 2, len, ")") {
Some(urlEnd) => {
let linkTag = acc + "<a href=\"" + String.substring(text, end + 2, urlEnd) + "\">" + processInline(String.substring(text, i + 1, end)) + "</a>"
processInlineFrom(text, urlEnd + 1, len, linkTag)
},
None => processInlineFrom(text, i + 1, len, acc + ch),
},
None => processInlineFrom(text, i + 1, len, acc + ch),
} else if ch == "<" then if i + 1 < len then if isLetterOrSlash(String.substring(text, i + 1, i + 2)) then match findClosing(text, i + 1, len, ">") {
Some(end) => processInlineFrom(text, end + 1, len, acc + String.substring(text, i, end + 1)),
None => processInlineFrom(text, i + 1, len, acc + "&lt;"),
} else processInlineFrom(text, i + 1, len, acc + "&lt;") else acc + "&lt;" else if ch == ">" then processInlineFrom(text, i + 1, len, acc + "&gt;") else processInlineFrom(text, i + 1, len, acc + ch)
}
}
fn processInline(text: String): String = processInlineFrom(text, 0, String.length(text), "")
fn bsHtml(s: BState): String =
match s {
BState(h, _, _, _, _, _, _, _, _, _) => h,
}
fn bsPara(s: BState): String =
match s {
BState(_, p, _, _, _, _, _, _, _, _) => p,
}
fn bsInCode(s: BState): Bool =
match s {
BState(_, _, c, _, _, _, _, _, _, _) => c,
}
fn bsCodeLang(s: BState): String =
match s {
BState(_, _, _, l, _, _, _, _, _, _) => l,
}
fn bsCodeLines(s: BState): String =
match s {
BState(_, _, _, _, cl, _, _, _, _, _) => cl,
}
fn bsInBq(s: BState): Bool =
match s {
BState(_, _, _, _, _, bq, _, _, _, _) => bq,
}
fn bsBqLines(s: BState): String =
match s {
BState(_, _, _, _, _, _, bl, _, _, _) => bl,
}
fn bsInList(s: BState): Bool =
match s {
BState(_, _, _, _, _, _, _, il, _, _) => il,
}
fn bsListItems(s: BState): String =
match s {
BState(_, _, _, _, _, _, _, _, li, _) => li,
}
fn bsOrdered(s: BState): Bool =
match s {
BState(_, _, _, _, _, _, _, _, _, o) => o,
}
fn flushPara(html: String, para: String): String =
if para == "" then html else html + "<p>" + processInline(String.trim(para)) + "</p>
"
fn flushBq(html: String, bqLines: String): String =
if bqLines == "" then html else html + "<blockquote>
" + convertMd(bqLines) + "</blockquote>
"
fn flushList(html: String, listItems: String, ordered: Bool): String =
if listItems == "" then html else {
let tag = if ordered then "ol" else "ul"
html + "<" + tag + ">
" + listItems + "</" + tag + ">
"
}
fn isOrderedListItem(line: String): Bool = {
let trimmed = String.trim(line)
if String.length(trimmed) < 3 then false else {
let first = String.substring(trimmed, 0, 1)
let isDigit = first == "0" || first == "1" || first == "2" || first == "3" || first == "4" || first == "5" || first == "6" || first == "7" || first == "8" || first == "9"
if isDigit then String.contains(trimmed, ". ") else false
}
}
fn processBlockLine(html: String, para: String, inList: Bool, listItems: String, ordered: Bool, line: String): BState = {
let trimmed = String.trim(line)
if trimmed == "" then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3, "", false, "", "", false, "", false, "", false)
} else if String.startsWith(trimmed, "#### ") then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3 + "<h4>" + processInline(String.substring(trimmed, 5, String.length(trimmed))) + "</h4>
", "", false, "", "", false, "", false, "", false)
} else if String.startsWith(trimmed, "### ") then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3 + "<h3>" + processInline(String.substring(trimmed, 4, String.length(trimmed))) + "</h3>
", "", false, "", "", false, "", false, "", false)
} else if String.startsWith(trimmed, "## ") then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3 + "<h2>" + processInline(String.substring(trimmed, 3, String.length(trimmed))) + "</h2>
", "", false, "", "", false, "", false, "", false)
} else if String.startsWith(trimmed, "# ") then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3 + "<h1>" + processInline(String.substring(trimmed, 2, String.length(trimmed))) + "</h1>
", "", false, "", "", false, "", false, "", false)
} else if trimmed == "---" || trimmed == "***" || trimmed == "___" then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3 + "<hr>
", "", false, "", "", false, "", false, "", false)
} else if String.startsWith(trimmed, "<") then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3 + trimmed + "
", "", false, "", "", false, "", false, "", false)
} else if String.startsWith(trimmed, "![") then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3 + "<p>" + processInline(trimmed) + "</p>
", "", false, "", "", false, "", false, "", false)
} else if inList then {
let h2 = flushList(html, listItems, ordered)
BState(h2, para + trimmed + " ", false, "", "", false, "", false, "", false)
} else BState(html, para + trimmed + " ", false, "", "", false, "", false, "", false)
}
fn blockFoldLine(state: BState, line: String): BState = {
let html = bsHtml(state)
let para = bsPara(state)
let inCode = bsInCode(state)
let codeLang = bsCodeLang(state)
let codeLines = bsCodeLines(state)
let inBq = bsInBq(state)
let bqLines = bsBqLines(state)
let inList = bsInList(state)
let listItems = bsListItems(state)
let ordered = bsOrdered(state)
if inCode then if String.startsWith(line, "```") then {
let codeHtml = if codeLang == "" then "<pre><code>" + codeLines + "</code></pre>
" else "<pre><code class=\"language-" + codeLang + "\">" + codeLines + "</code></pre>
"
BState(html + codeHtml, "", false, "", "", false, "", false, "", false)
} else BState(html, para, true, codeLang, codeLines + escapeHtmlCode(line) + "
", false, "", false, "", false) else if String.startsWith(line, "```") then {
let h2 = flushPara(html, para)
let h3 = flushBq(h2, bqLines)
let h4 = flushList(h3, listItems, ordered)
let lang = String.trim(String.substring(line, 3, String.length(line)))
BState(h4, "", true, lang, "", false, "", false, "", false)
} else if String.startsWith(line, "> ") then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
let bqContent = String.substring(line, 2, String.length(line))
BState(h3, "", false, "", "", true, bqLines + bqContent + "
", false, "", false)
} else if String.trim(line) == ">" then {
let h2 = flushPara(html, para)
let h3 = flushList(h2, listItems, ordered)
BState(h3, "", false, "", "", true, bqLines + "
", false, "", false)
} else if inBq then {
let h2 = flushBq(html, bqLines)
processBlockLine(h2, para, inList, listItems, ordered, line)
} else if String.startsWith(line, "- ") then {
let h2 = flushPara(html, para)
let item = String.substring(line, 2, String.length(line))
BState(h2, "", false, "", "", false, "", true, listItems + "<li>" + processInline(item) + "</li>
", false)
} else if isOrderedListItem(line) then {
let h2 = flushPara(html, para)
let dotIdx = match String.indexOf(line, ". ") {
Some(idx) => idx,
None => 0,
}
let item = String.substring(line, dotIdx + 2, String.length(line))
BState(h2, "", false, "", "", false, "", true, listItems + "<li>" + processInline(item) + "</li>
", true)
} else processBlockLine(html, para, inList, listItems, ordered, line)
}
fn parseBlocks(text: String): String = {
let lines = String.lines(text)
let init = BState("", "", false, "", "", false, "", false, "", false)
let final = List.fold(lines, init, blockFoldLine)
let h = flushPara(bsHtml(final), bsPara(final))
let h2 = flushBq(h, bsBqLines(final))
flushList(h2, bsListItems(final), bsOrdered(final))
}
fn convertMd(markdown: String): String = parseBlocks(markdown)
fn htmlHead(title: String, description: String): String = "<!doctype html><html lang=\"en\"><head>" + "<title>" + title + "</title>" + "<meta charset=\"utf-8\">" + "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">" + "<meta name=\"description\" content=\"" + description + "\">" + "<meta property=\"og:title\" content=\"" + title + "\">" + "<meta property=\"og:description\" content=\"" + description + "\">" + "<meta property=\"og:type\" content=\"website\">" + "<meta property=\"og:url\" content=\"https://blu.cx\">" + "<meta property=\"og:image\" content=\"https://blu.cx/images/social-card.png\">" + "<meta property=\"og:site_name\" content=\"Brandon Lucas\">" + "<meta name=\"twitter:card\" content=\"summary_large_image\">" + "<meta name=\"twitter:title\" content=\"" + title + "\">" + "<meta name=\"twitter:description\" content=\"" + description + "\">" + "<link rel=\"canonical\" href=\"https://blu.cx\">" + "<link rel=\"preload\" href=\"/fonts/EBGaramond-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"\">" + "<link rel=\"preload\" href=\"/fonts/UnifrakturMaguntia-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"\">" + "<link href=\"/styles.css\" rel=\"stylesheet\" type=\"text/css\">" + "<link href=\"/highlight/tokyo-night-dark.min.css\" rel=\"stylesheet\" type=\"text/css\">" + "<script src=\"/highlight/highlight.min.js\" defer=\"\"></script>" + "<script>document.addEventListener('DOMContentLoaded', function() \{ hljs.highlightAll(); \});</script>" + "</head>"
fn htmlNav(): String = "<a href=\"/\"><img src=\"/images/favicon.webp\" alt=\"Narsil Logo\" width=\"59\" height=\"80\"></a>"
fn htmlFooter(): String = "<div class=\"footer flex flex-col gap-2 items-center\">" + "<div><a href=\"/\"><img alt=\"Narsil Favicon\" src=\"/images/favicon.webp\" width=\"59\" height=\"80\"></a></div>" + "<div class=\"flex gap-2\">" + "<a href=\"https://github.com/thebrandonlucas\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path fill=\"#fff\" d=\"M7.999,0.431c-4.285,0-7.76,3.474-7.76,7.761 c0,3.428,2.223,6.337,5.307,7.363c0.388,0.071,0.53-0.168,0.53-0.374c0-0.184-0.007-0.672-0.01-1.32 c-2.159,0.469-2.614-1.04-2.614-1.04c-0.353-0.896-0.862-1.135-0.862-1.135c-0.705-0.481,0.053-0.472,0.053-0.472 c0.779,0.055,1.189,0.8,1.189,0.8c0.692,1.186,1.816,0.843,2.258,0.645c0.071-0.502,0.271-0.843,0.493-1.037 C4.86,11.425,3.049,10.76,3.049,7.786c0-0.847,0.302-1.54,0.799-2.082C3.768,5.507,3.501,4.718,3.924,3.65 c0,0,0.652-0.209,2.134,0.796C6.677,4.273,7.34,4.187,8,4.184c0.659,0.003,1.323,0.089,1.943,0.261 c1.482-1.004,2.132-0.796,2.132-0.796c0.423,1.068,0.157,1.857,0.077,2.054c0.497,0.542,0.798,1.235,0.798,2.082 c0,2.981-1.814,3.637-3.543,3.829c0.279,0.24,0.527,0.713,0.527,1.437c0,1.037-0.01,1.874-0.01,2.129 c0,0.208,0.14,0.449,0.534,0.373c3.081-1.028,5.302-3.935,5.302-7.362C15.76,3.906,12.285,0.431,7.999,0.431z\"></path></svg></a>" + "<a href=\"https://x.com/brandonstlucas\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path fill=\"#fff\" d=\"M15.969,3.058c-0.586,0.26-1.217,0.436-1.878,0.515c0.675-0.405,1.194-1.045,1.438-1.809 c-0.632,0.375-1.332,0.647-2.076,0.793c-0.596-0.636-1.446-1.033-2.387-1.033c-1.806,0-3.27,1.464-3.27,3.27 c0,0.256,0.029,0.506,0.085,0.745C5.163,5.404,2.753,4.102,1.14,2.124C0.859,2.607,0.698,3.168,0.698,3.767 c0,1.134,0.577,2.135,1.455,2.722C1.616,6.472,1.112,6.325,0.671,6.08c0,0.014,0,0.027,0,0.041c0,1.584,1.127,2.906,2.623,3.206 C3.02,9.402,2.731,9.442,2.433,9.442c-0.211,0-0.416-0.021-0.615-0.059c0.416,1.299,1.624,2.245,3.055,2.271 c-1.119,0.877-2.529,1.4-4.061,1.4c-0.264,0-0.524-0.015-0.78-0.046c1.447,0.928,3.166,1.469,5.013,1.469 c6.015,0,9.304-4.983,9.304-9.304c0-0.142-0.003-0.283-0.009-0.423C14.976,4.29,15.531,3.714,15.969,3.058z\"></path></svg></a>" + "</div>" + "<div>Copyright (c) 2025 Brandon Lucas. All Rights Reserved.</div>" + "</div>"
fn htmlDocument(title: String, description: String, body: String): String = htmlHead(title, description) + "<body><main class=\"flex flex-col gap-8 w-[80%] items-center mx-auto my-20\">" + htmlNav() + body + htmlFooter() + "</main></body></html>"
fn htmlPostPage(title: String, date: String, tagsHtml: String, content: String): String = "<div class=\"page w-[80%] flex flex-col gap-8\">" + "<h1 class=\"text-4xl text-center font-bold w-full\">" + title + "</h1>" + "<div class=\"post-content flex flex-col gap-4 pb-12 border-b\">" + "<div class=\"post-metadata border-b mb-8 pb-8 items-center flex flex-col\">" + "<span>" + date + "</span>" + "<span>-</span>" + "<div>" + tagsHtml + "</div>" + "</div>" + "<div class=\"markdown\">" + content + "</div>" + "</div></div>"
fn tagToLink(tag: String): String = "<a href=\"/tags/" + tag + "\">" + tag + "</a>"
fn renderTagLinks(tags: List<String>): String = String.join(List.map(tags, tagToLink), ", ")
fn htmlPostEntry(title: String, date: String, url: String): String = "<div class=\"flex flex-col gap-1 border-b pb-4\">" + "<a href=\"" + url + "\" class=\"text-xl\">" + title + "</a>" + "<span class=\"text-gray-400\">" + date + "</span>" + "</div>"
fn htmlPostList(sectionTitle: String, postsHtml: String): String = "<div class=\"page w-[80%] flex flex-col gap-8\">" + "<h1 class=\"text-4xl text-center font-bold w-full\">" + sectionTitle + "</h1>" + "<div class=\"flex flex-col gap-4\">" + postsHtml + "</div></div>"
fn htmlHomePage(siteTitle: String, snippetsHtml: String): String = "<h1 class=\"unifrakturmaguntia-regular text-6xl text-center w-full\">" + siteTitle + "</h1>" + "<div class=\"font-bold text-4xl italic text-center w-full\">" + "Βράνδων Λουκᾶς" + "</div>" + "<h2 class=\"text-xl text-center flex flex-col\">" + "<span>Bitcoin Lightning Payments @ voltage.cloud</span>" + "<span>Bitcoin Privacy &amp; Scalability @ payjoin.org.</span>" + "<span>Love sovereign software &amp; history.</span>" + "<span>Learning Nix, Elm, Rust, Ancient Greek and Latin.</span>" + "</h2>" + "<div class=\"flex flex-col gap-4 w-full\">" + snippetsHtml + "</div>"
fn htmlSnippetCard(content: String): String = "<div class=\"flex flex-col gap-4 border border-gray-500 p-8 rounded-sm max-h-150 overflow-y-auto text-wrap break-words\">" + "<div><div class=\"markdown\">" + content + "</div></div>" + "</div>"
fn htmlTagPage(tagName: String, postsHtml: String): String = "<div class=\"page w-[80%] flex flex-col gap-8\">" + "<h1 class=\"text-4xl text-center font-bold w-full\">Tag: " + tagName + "</h1>" + "<div class=\"flex flex-col gap-4\">" + postsHtml + "</div></div>"
fn parseFile(path: String): Page with {File} = {
let raw = File.read(path)
let parsed = parseFrontmatter(raw)
match parsed {
ParseResult(front, body) => {
let title = fmTitle(front)
let date = fmDate(front)
let tags = fmTagsRaw(front)
let htmlContent = convertMd(body)
let filename = basename(path)
let slug = slugFromFilename(filename)
Page(date, title, slug, tags, htmlContent)
},
}
}
fn mapParseFiles(dir: String, files: List<String>): List<Page> with {File} =
match List.head(files) {
None => [],
Some(filename) => {
let page = parseFile(dir + "/" + filename)
match List.tail(files) {
Some(rest) => List.concat([page], mapParseFiles(dir, rest)),
None => [page],
}
},
}
fn readSection(contentDir: String, section: String): List<Page> with {File} = {
let dir = contentDir + "/" + section
if File.exists(dir) then {
let entries = File.readDir(dir)
let mdFiles = List.filter(entries, fn(e: String): Bool => String.endsWith(e, ".md"))
mapParseFiles(dir, mdFiles)
} else []
}
fn ensureDir(path: String): Unit with {File} = {
if File.exists(path) then () else {
let parent = dirname(path)
if parent != "." then if parent != path then ensureDir(parent) else () else ()
File.mkdir(path)
}
}
fn writePostPage(outputDir: String, section: String, page: Page, siteTitle: String, siteDesc: String): Unit with {File} = {
let slug = pgSlug(page)
let title = pgTitle(page)
let date = pgDate(page)
let tagsRaw = pgTags(page)
let content = pgContent(page)
let tags = if tagsRaw == "" then [] else String.split(tagsRaw, " ")
let tagsHtml = renderTagLinks(tags)
let formattedDate = formatDate(date)
let body = htmlPostPage(title, formattedDate, tagsHtml, content)
let pageTitle = title + " | " + siteTitle
let html = htmlDocument(pageTitle, siteDesc, body)
let dir = outputDir + "/posts/" + section + "/" + slug
ensureDir(dir)
File.write(dir + "/index.html", html)
}
fn writeSectionIndex(outputDir: String, section: String, pages: List<Page>, siteTitle: String, siteDesc: String): Unit with {File} = {
let sorted = sortByDateDesc(pages)
let postEntries = List.map(sorted, fn(page: Page): String => htmlPostEntry(pgTitle(page), formatDate(pgDate(page)), "/posts/" + section + "/" + pgSlug(page)))
let postsHtml = String.join(postEntries, "
")
let sectionName = if section == "articles" then "Articles" else if section == "blog" then "Blog" else if section == "journal" then "Journal" else section
let body = htmlPostList(sectionName, postsHtml)
let pageTitle = sectionName + " | " + siteTitle
let html = htmlDocument(pageTitle, siteDesc, body)
let dir = outputDir + "/posts/" + section
ensureDir(dir)
File.write(dir + "/index.html", html)
}
fn collectTagsForPage(section: String, page: Page): List<TagEntry> = {
let tagsRaw = pgTags(page)
let tags = if tagsRaw == "" then [] else String.split(tagsRaw, " ")
List.map(tags, fn(tag: String): TagEntry => TagEntry(tag, pgTitle(page), pgDate(page), pgSlug(page), section))
}
fn collectTags(section: String, pages: List<Page>): List<TagEntry> = {
let nested = List.map(pages, fn(page: Page): List<TagEntry> => collectTagsForPage(section, page))
List.fold(nested, [], fn(acc: List<TagEntry>, entries: List<TagEntry>): List<TagEntry> => List.concat(acc, entries))
}
fn addIfUnique(acc: List<String>, e: TagEntry): List<String> = if List.any(acc, fn(t: String): Bool => t == teTag(e)) then acc else List.concat(acc, [teTag(e)])
fn getUniqueTags(entries: List<TagEntry>): List<String> = List.fold(entries, [], addIfUnique)
fn tagEntryToHtml(e: TagEntry): String = htmlPostEntry(teTitle(e), formatDate(teDate(e)), "/posts/" + teSection(e) + "/" + teSlug(e))
fn writeOneTagPage(outputDir: String, tag: String, allTagEntries: List<TagEntry>, siteTitle: String, siteDesc: String): Unit with {File} = {
let entries = List.filter(allTagEntries, fn(e: TagEntry): Bool => teTag(e) == tag)
let postsHtml = String.join(List.map(entries, tagEntryToHtml), "
")
let body = htmlTagPage(tag, postsHtml)
let pageTitle = "Tag: " + tag + " | " + siteTitle
let html = htmlDocument(pageTitle, siteDesc, body)
let dir = outputDir + "/tags/" + tag
ensureDir(dir)
File.write(dir + "/index.html", html)
}
fn writeTagPagesLoop(outputDir: String, tags: List<String>, allTagEntries: List<TagEntry>, siteTitle: String, siteDesc: String): Unit with {File} =
match List.head(tags) {
None => (),
Some(tag) => {
writeOneTagPage(outputDir, tag, allTagEntries, siteTitle, siteDesc)
match List.tail(tags) {
Some(rest) => writeTagPagesLoop(outputDir, rest, allTagEntries, siteTitle, siteDesc),
None => (),
}
},
}
fn writeTagPages(outputDir: String, allTagEntries: List<TagEntry>, siteTitle: String, siteDesc: String): Unit with {File, Console} = {
let uniqueTags = getUniqueTags(allTagEntries)
writeTagPagesLoop(outputDir, uniqueTags, allTagEntries, siteTitle, siteDesc)
}
fn renderSnippetFile(snippetDir: String, filename: String): String with {File} = {
let raw = File.read(snippetDir + "/" + filename)
let parsed = parseFrontmatter(raw)
match parsed {
ParseResult(_, body) => htmlSnippetCard(convertMd(body)),
}
}
fn renderSnippets(snippetDir: String, files: List<String>): List<String> with {File} =
match List.head(files) {
None => [],
Some(f) => {
let card = renderSnippetFile(snippetDir, f)
match List.tail(files) {
Some(rest) => List.concat([card], renderSnippets(snippetDir, rest)),
None => [card],
}
},
}
fn writeHomePage(outputDir: String, contentDir: String, siteTitle: String, siteDesc: String): Unit with {File} = {
let snippetDir = contentDir + "/snippets"
let snippetEntries = if File.exists(snippetDir) then {
let entries = File.readDir(snippetDir)
List.filter(entries, fn(e: String): Bool => String.endsWith(e, ".md"))
} else []
let snippetCards = renderSnippets(snippetDir, snippetEntries)
let firstCard = match List.head(snippetCards) {
Some(c) => c,
None => "",
}
let restCards = match List.tail(snippetCards) {
Some(rest) => rest,
None => [],
}
let gridHtml = "<div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">" + String.join(restCards, "
") + "</div>"
let snippetsHtml = firstCard + "
" + gridHtml
let body = htmlHomePage(siteTitle, snippetsHtml)
let pageTitle = "Bitcoin Lightning Developer & Privacy Advocate | " + siteTitle
let html = htmlDocument(pageTitle, siteDesc, body)
File.write(outputDir + "/index.html", html)
}
fn writeAllPostPages(outputDir: String, section: String, pages: List<Page>, siteTitle: String, siteDesc: String): Unit with {File} =
match List.head(pages) {
None => (),
Some(page) => {
writePostPage(outputDir, section, page, siteTitle, siteDesc)
match List.tail(pages) {
Some(rest) => writeAllPostPages(outputDir, section, rest, siteTitle, siteDesc),
None => (),
}
},
}
fn main(): Unit with {File, Console, Process} = {
Console.print("=== blu-site: Static Site Generator in Lux ===")
Console.print("")
let cfg = loadConfig("projects/blu-site/config.json")
let siteTitle = cfgTitle(cfg)
let siteDesc = cfgDesc(cfg)
let contentDir = "projects/blu-site/" + cfgContentDir(cfg)
let outputDir = "projects/blu-site/" + cfgOutputDir(cfg)
let staticDir = "projects/blu-site/" + cfgStaticDir(cfg)
Console.print("Site: " + siteTitle)
Console.print("Content: " + contentDir)
Console.print("Output: " + outputDir)
Console.print("")
ensureDir(outputDir)
Console.print("Reading content...")
let articles = readSection(contentDir, "articles")
let blogPosts = readSection(contentDir, "blog")
let journalPosts = readSection(contentDir, "journal")
Console.print(" Articles: " + toString(List.length(articles)))
Console.print(" Blog posts: " + toString(List.length(blogPosts)))
Console.print(" Journal entries: " + toString(List.length(journalPosts)))
Console.print("")
Console.print("Writing post pages...")
writeAllPostPages(outputDir, "articles", articles, siteTitle, siteDesc)
writeAllPostPages(outputDir, "blog", blogPosts, siteTitle, siteDesc)
writeAllPostPages(outputDir, "journal", journalPosts, siteTitle, siteDesc)
Console.print("Writing section indexes...")
writeSectionIndex(outputDir, "articles", articles, siteTitle, siteDesc)
writeSectionIndex(outputDir, "blog", blogPosts, siteTitle, siteDesc)
writeSectionIndex(outputDir, "journal", journalPosts, siteTitle, siteDesc)
Console.print("Writing tag pages...")
let articleTags = collectTags("articles", articles)
let blogTags = collectTags("blog", blogPosts)
let journalTags = collectTags("journal", journalPosts)
let allTags = List.concat(List.concat(articleTags, blogTags), journalTags)
writeTagPages(outputDir, allTags, siteTitle, siteDesc)
Console.print("Writing home page...")
writeHomePage(outputDir, contentDir, siteTitle, siteDesc)
Console.print("Copying static assets...")
let copyCmd = "cp -r " + staticDir + "/* " + outputDir + "/"
Process.exec(copyCmd)
Console.print("")
Console.print("=== Build complete! ===")
let totalPages = List.length(articles) + List.length(blogPosts) + List.length(journalPosts)
Console.print("Total post pages: " + toString(totalPages))
Console.print("Output directory: " + outputDir)
}
let _ = run main() with {}

View File

@@ -1,364 +0,0 @@
// Pure Lux markdown-to-HTML converter
// Handles block-level and inline-level markdown elements
// === Inline processing ===
// Process inline markdown: **bold**, *italic*/_italic_, `code`, [links](url)
// Uses index-based scanning
pub fn processInline(text: String): String = {
let len = String.length(text);
processInlineFrom(text, 0, len, "")
}
fn processInlineFrom(text: String, i: Int, len: Int, acc: String): String = {
if i >= len then acc
else {
let ch = String.substring(text, i, i + 1);
// ** bold **
if ch == "*" then
if i + 1 < len then
if String.substring(text, i + 1, i + 2) == "*" then
// Look for closing **
match findClosing(text, i + 2, len, "**") {
Some(end) => {
let inner = String.substring(text, i + 2, end);
processInlineFrom(text, end + 2, len, acc + "<strong>" + processInline(inner) + "</strong>")
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
else
// Single * italic
match findClosing(text, i + 1, len, "*") {
Some(end) => {
let inner = String.substring(text, i + 1, end);
processInlineFrom(text, end + 1, len, acc + "<em>" + processInline(inner) + "</em>")
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
else
acc + ch
// _italic_
else if ch == "_" then
if i + 1 < len then
match findClosing(text, i + 1, len, "_") {
Some(end) => {
let inner = String.substring(text, i + 1, end);
processInlineFrom(text, end + 1, len, acc + "<em>" + processInline(inner) + "</em>")
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
else
acc + ch
// `code`
else if ch == "`" then
match findClosing(text, i + 1, len, "`") {
Some(end) => {
let inner = String.substring(text, i + 1, end);
processInlineFrom(text, end + 1, len, acc + "<code>" + inner + "</code>")
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
// [text](url)
else if ch == "[" then
match findClosing(text, i + 1, len, "](") {
Some(end) => {
let linkText = String.substring(text, i + 1, end);
match findClosing(text, end + 2, len, ")") {
Some(urlEnd) => {
let url = String.substring(text, end + 2, urlEnd);
processInlineFrom(text, urlEnd + 1, len, acc + "<a href=\"" + url + "\">" + processInline(linkText) + "</a>")
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
// ![alt](url) — images
else if ch == "!" then
if i + 1 < len then
if String.substring(text, i + 1, i + 2) == "[" then
match findClosing(text, i + 2, len, "](") {
Some(end) => {
let alt = String.substring(text, i + 2, end);
match findClosing(text, end + 2, len, ")") {
Some(urlEnd) => {
let url = String.substring(text, end + 2, urlEnd);
processInlineFrom(text, urlEnd + 1, len, acc + "<img src=\"" + url + "\" alt=\"" + alt + "\">")
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
else
processInlineFrom(text, i + 1, len, acc + ch)
else
acc + ch
// HTML entities: & < >
else if ch == "&" then
processInlineFrom(text, i + 1, len, acc + "&amp;")
else if ch == "<" then
// Check if this looks like an HTML tag — pass through
if i + 1 < len then
if isHtmlTagStart(text, i, len) then {
// Find closing >
match findClosing(text, i + 1, len, ">") {
Some(end) => {
let tag = String.substring(text, i, end + 1);
processInlineFrom(text, end + 1, len, acc + tag)
},
None => processInlineFrom(text, i + 1, len, acc + "&lt;")
}
} else
processInlineFrom(text, i + 1, len, acc + "&lt;")
else
acc + "&lt;"
else if ch == ">" then
processInlineFrom(text, i + 1, len, acc + "&gt;")
else
processInlineFrom(text, i + 1, len, acc + ch)
}
}
fn isHtmlTagStart(text: String, i: Int, len: Int): Bool = {
// Check if < is followed by a letter or / (closing tag)
if i + 1 >= len then false
else {
let next = String.substring(text, i + 1, i + 2);
next == "/" || next == "a" || next == "b" || next == "c" || next == "d" ||
next == "e" || next == "f" || next == "g" || next == "h" || next == "i" ||
next == "j" || next == "k" || next == "l" || next == "m" || next == "n" ||
next == "o" || next == "p" || next == "q" || next == "r" || next == "s" ||
next == "t" || next == "u" || next == "v" || next == "w" || next == "x" ||
next == "y" || next == "z" ||
next == "A" || next == "B" || next == "C" || next == "D" || next == "E" ||
next == "F" || next == "G" || next == "H" || next == "I" || next == "J" ||
next == "K" || next == "L" || next == "M" || next == "N" || next == "O" ||
next == "P" || next == "Q" || next == "R" || next == "S" || next == "T" ||
next == "U" || next == "V" || next == "W" || next == "X" || next == "Y" ||
next == "Z" || next == "!"
}
}
// Find a closing delimiter starting from position i
fn findClosing(text: String, start: Int, len: Int, delim: String): Option<Int> = {
let delimLen = String.length(delim);
findClosingFrom(text, start, len, delim, delimLen)
}
fn findClosingFrom(text: String, i: Int, len: Int, delim: String, delimLen: Int): Option<Int> = {
if i + delimLen > len then None
else if String.substring(text, i, i + delimLen) == delim then Some(i)
else findClosingFrom(text, i + 1, len, delim, delimLen)
}
// === Block-level processing ===
// State for block parser accumulator
// (blocks_html, current_paragraph_lines, in_code_block, code_lang, code_lines, in_blockquote, bq_lines, in_list, list_items, is_ordered)
pub type BlockState =
| BlockState(String, String, Bool, String, String, Bool, String, Bool, String, Bool)
fn bsHtml(s: BlockState): String = match s { BlockState(h, _, _, _, _, _, _, _, _, _) => h }
fn bsPara(s: BlockState): String = match s { BlockState(_, p, _, _, _, _, _, _, _, _) => p }
fn bsInCode(s: BlockState): Bool = match s { BlockState(_, _, c, _, _, _, _, _, _, _) => c }
fn bsCodeLang(s: BlockState): String = match s { BlockState(_, _, _, l, _, _, _, _, _, _) => l }
fn bsCodeLines(s: BlockState): String = match s { BlockState(_, _, _, _, cl, _, _, _, _, _) => cl }
fn bsInBq(s: BlockState): Bool = match s { BlockState(_, _, _, _, _, bq, _, _, _, _) => bq }
fn bsBqLines(s: BlockState): String = match s { BlockState(_, _, _, _, _, _, bl, _, _, _) => bl }
fn bsInList(s: BlockState): Bool = match s { BlockState(_, _, _, _, _, _, _, il, _, _) => il }
fn bsListItems(s: BlockState): String = match s { BlockState(_, _, _, _, _, _, _, _, li, _) => li }
fn bsOrdered(s: BlockState): Bool = match s { BlockState(_, _, _, _, _, _, _, _, _, o) => o }
// Flush accumulated paragraph
fn flushPara(html: String, para: String): String =
if para == "" then html
else html + "<p>" + processInline(String.trim(para)) + "</p>\n"
// Flush accumulated blockquote
fn flushBq(html: String, bqLines: String): String =
if bqLines == "" then html
else html + "<blockquote>\n" + convert(bqLines) + "</blockquote>\n"
// Flush accumulated list
fn flushList(html: String, listItems: String, ordered: Bool): String =
if listItems == "" then html
else {
let tag = if ordered then "ol" else "ul";
html + "<" + tag + ">\n" + listItems + "</" + tag + ">\n"
}
// Parse markdown blocks from text
pub fn parseBlocks(text: String): String = {
let lines = String.lines(text);
let init = BlockState("", "", false, "", "", false, "", false, "", false);
let final = List.fold(lines, init, fn(state: BlockState, line: String): BlockState => {
let html = bsHtml(state);
let para = bsPara(state);
let inCode = bsInCode(state);
let codeLang = bsCodeLang(state);
let codeLines = bsCodeLines(state);
let inBq = bsInBq(state);
let bqLines = bsBqLines(state);
let inList = bsInList(state);
let listItems = bsListItems(state);
let ordered = bsOrdered(state);
// Inside code block
if inCode then
if String.startsWith(line, "```") then {
// End code block
let codeHtml = if codeLang == "" then
"<pre><code>" + codeLines + "</code></pre>\n"
else
"<pre><code class=\"language-" + codeLang + "\">" + codeLines + "</code></pre>\n";
BlockState(html + codeHtml, "", false, "", "", false, "", false, "", false)
} else
BlockState(html, para, true, codeLang, codeLines + escapeHtmlCode(line) + "\n", false, "", false, "", false)
// Start code block
else if String.startsWith(line, "```") then {
let h2 = flushPara(html, para);
let h3 = flushBq(h2, bqLines);
let h4 = flushList(h3, listItems, ordered);
let lang = String.trim(String.substring(line, 3, String.length(line)));
BlockState(h4, "", true, lang, "", false, "", false, "", false)
}
// Blockquote line
else if String.startsWith(line, "> ") then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
let bqContent = String.substring(line, 2, String.length(line));
BlockState(h3, "", false, "", "", true, bqLines + bqContent + "\n", false, "", false)
}
else if String.trim(line) == ">" then {
// Empty blockquote continuation
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
BlockState(h3, "", false, "", "", true, bqLines + "\n", false, "", false)
}
// End of blockquote (non-bq line after bq lines)
else if inBq then {
let h2 = flushBq(html, bqLines);
// Re-process this line
processLine(BlockState(h2, para, false, "", "", false, "", inList, listItems, ordered), line)
}
// Unordered list item
else if String.startsWith(line, "- ") then {
let h2 = flushPara(html, para);
let item = String.substring(line, 2, String.length(line));
BlockState(h2, "", false, "", "", false, "", true, listItems + "<li>" + processInline(item) + "</li>\n", false)
}
// Ordered list item (starts with digit + ". ")
else if isOrderedListItem(line) then {
let h2 = flushPara(html, para);
let dotIdx = match String.indexOf(line, ". ") {
Some(idx) => idx,
None => 0
};
let item = String.substring(line, dotIdx + 2, String.length(line));
BlockState(h2, "", false, "", "", false, "", true, listItems + "<li>" + processInline(item) + "</li>\n", true)
}
else
processLine(state, line)
});
// Flush remaining state
let h = flushPara(bsHtml(final), bsPara(final));
let h2 = flushBq(h, bsBqLines(final));
let h3 = flushList(h2, bsListItems(final), bsOrdered(final));
h3
}
fn processLine(state: BlockState, line: String): BlockState = {
let html = bsHtml(state);
let para = bsPara(state);
let inList = bsInList(state);
let listItems = bsListItems(state);
let ordered = bsOrdered(state);
let trimmed = String.trim(line);
// Blank line — flush paragraph and list
if trimmed == "" then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
BlockState(h3, "", false, "", "", false, "", false, "", false)
}
// Heading
else if String.startsWith(trimmed, "# ") then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
let content = String.substring(trimmed, 2, String.length(trimmed));
BlockState(h3 + "<h1>" + processInline(content) + "</h1>\n", "", false, "", "", false, "", false, "", false)
}
else if String.startsWith(trimmed, "## ") then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
let content = String.substring(trimmed, 3, String.length(trimmed));
BlockState(h3 + "<h2>" + processInline(content) + "</h2>\n", "", false, "", "", false, "", false, "", false)
}
else if String.startsWith(trimmed, "### ") then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
let content = String.substring(trimmed, 4, String.length(trimmed));
BlockState(h3 + "<h3>" + processInline(content) + "</h3>\n", "", false, "", "", false, "", false, "", false)
}
else if String.startsWith(trimmed, "#### ") then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
let content = String.substring(trimmed, 5, String.length(trimmed));
BlockState(h3 + "<h4>" + processInline(content) + "</h4>\n", "", false, "", "", false, "", false, "", false)
}
// Horizontal rule
else if trimmed == "---" || trimmed == "***" || trimmed == "___" then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
BlockState(h3 + "<hr>\n", "", false, "", "", false, "", false, "", false)
}
// Raw HTML line (starts with <)
else if String.startsWith(trimmed, "<") then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
BlockState(h3 + trimmed + "\n", "", false, "", "", false, "", false, "", false)
}
// Image on its own line
else if String.startsWith(trimmed, "![") then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
BlockState(h3 + "<p>" + processInline(trimmed) + "</p>\n", "", false, "", "", false, "", false, "", false)
}
// Continuation of list (indented or sub-item)
else if inList then
if String.startsWith(line, " ") then {
// Indented content under a list item — append to last item
BlockState(html, para, false, "", "", false, "", true, listItems, ordered)
} else {
// Not a list item — flush list, treat as paragraph
let h2 = flushList(html, listItems, ordered);
BlockState(h2, para + trimmed + " ", false, "", "", false, "", false, "", false)
}
// Regular text — accumulate into paragraph
else
BlockState(html, para + trimmed + " ", false, "", "", false, "", false, "", false)
}
fn isOrderedListItem(line: String): Bool = {
let trimmed = String.trim(line);
if String.length(trimmed) < 3 then false
else {
let first = String.substring(trimmed, 0, 1);
let isDigit = first == "0" || first == "1" || first == "2" || first == "3" ||
first == "4" || first == "5" || first == "6" || first == "7" ||
first == "8" || first == "9";
if isDigit then String.contains(trimmed, ". ")
else false
}
}
// Escape HTML special chars in code blocks (no inline processing)
fn escapeHtmlCode(s: String): String =
String.replace(String.replace(String.replace(s, "&", "&amp;"), "<", "&lt;"), ">", "&gt;")
// === Main convert function ===
// Convert full markdown text to HTML
pub fn convert(markdown: String): String =
parseBlocks(markdown)

View File

@@ -1,251 +0,0 @@
[
{
"date": "2025-08-05",
"text": "Someone who asked, 'why believe what is true?' or 'why want what is good?' has failed to understand the nature of reasoning. He doesn't see that, if we are to justify our beliefs and desires at all, then our reasons must be anchored in the true and the good.",
"source": "Roger Scruton, Beauty"
},
{
"date": "2025-08-04",
"text": "The stock exchange is a poor substitute for the Holy Grail.",
"source": "Joseph Schumpeter, quoted from 'Capitalism Buries Its Undertakers' by Robert Bellafoire",
"link": "https://commonplace.substack.com/p/capitalism-buries-its-undertakers"
},
{
"date": "2025-08-04",
"text": "Is the existence of billionaires all that makes people question capitalism today? Or is it also the dull horror of realizing that for all our cherished economic freedom, there doesnt seem to be anything worth doing with that freedom besides ordering Uber Eats and watching porn?",
"source": "Robert Bellafiore, Capitalism Buries Its Undertakers",
"link": "https://commonplace.substack.com/p/capitalism-buries-its-undertakers"
},
{
"date": "2025-07-22",
"text": "And the men who hold high places\nMust be the ones to start\nTo mould a new reality\nCloser to the Heart\n\nThe Blacksmith and the Artist\nReflect it in their art\nForge their creativity\nCloser to the Heart\n\nPhilosophers and Ploughmen\nEach must know his part\nTo sow a new mentality\nCloser to the Heart\n\nYou can be the Captain\nI will draw the Chart\nSailing into destiny\nCloser to the Heart",
"source": "Rush, Closer to the Heart",
"link": "https://www.rush.com/songs/closer-to-the-heart/"
},
{
"date": "2025-01-23",
"text": "Shall I be carried to the skies, On flowery beds of ease, While others fought to win the prize, And sailed through bloody seas?",
"source": "Laura Ingalls Wilder, Little House in the Big Woods, p. 96"
},
{
"date": "2024-09-17",
"text": "Only a few prefer liberty the majority seek nothing more than fair masters",
"source": "Sallust, Histories"
},
{
"date": "2024-09-17",
"text": "Human nature is universally imbued with a desire for liberty, and a hatred for servitude.",
"source": "Julius Caesar, Gallic Wars"
},
{
"date": "2024-09-12",
"text": "And I ask for your prayers that these vague and wandering thoughts of mine may some day become coherent and, having been so vainly cast in all directions, that they may direct themselves at last to the one, true, certain, and never-ending good.",
"source": "Petrarch, The Ascent of Mount Ventoux, April 26, 1336 at Malaucène"
},
{
"date": "2024-09-03",
"text": "It is inhuman to bless where one is cursed.",
"source": "Nietzsche, Beyond Good and Evil, Pt 4: Maxims and Interludes, #181"
},
{
"date": "2024-09-03",
"text": "The consequences of our actions take us by the scruff of the neck, altogether indifferent to the fact that we have 'improved' in the meantime.",
"source": "Nietzsche, Beyond Good and Evil, Pt 4: Maxims and Interludes, #179"
},
{
"date": "2024-08-16",
"text": "Who can doubt that, were Rome to know itself once more, it would rise again?",
"source": "Petrarch, quoted from Petrarch: Everywhere a Wanderer by Christopher Celenza, Ch. II, p. 56"
},
{
"date": "2024-08-16",
"text": "Rome, soon to be destroyed, continued to laugh and play.",
"source": "Will Durant, The Age of Faith"
},
{
"date": "2024-08-16",
"text": "What makes the heart of the Christian heavy? The fact that he is a pilgrim, and longs for his own country.",
"source": "Saint Augustine, self-written epitaph, quoted from The Age of Faith by Will Durant, Ch. III, Part V: The Patriarch"
},
{
"date": "2024-08-13",
"text": "Once triumphant, the Church ceased to preach toleration",
"source": "Will Durant, The Age of Faith, Ch. III, Part II: The Heretics"
},
{
"date": "2024-08-13",
"text": "in 470 a general impoverishment of fields and cities, of senators and proletarians, depressed the spirits of a once great race to an epicurean cynicism that doubted all gods but Priapus, a timid childlessness that shunned the responsibilities of life, and an angry cowardice that denounced every surrender and shirked every martial task.",
"source": "Will Durant, The Age of Faith, Ch. II, Part V: The Fall of Rome"
},
{
"date": "2024-08-13",
"text": "To be ignorant of what occurred before you were born is to remain always a child.",
"source": "Cicero, Orator, 120"
},
{
"date": "2024-08-10",
"text": "All that is profound loves a mask; the very profoundest things even have a hatred for images and likenesses. Shouldnt the opposite be the only proper disguise to accompany the shame of a god?….Every profound spirit needs a mask; even more, a mask is continually growing around every profound spirit thanks to the constantly false, that is shallow interpretation of every word, every step, every sign of life he gives.",
"source": "Nietzsche, Beyond Good and Evil, Part 2"
},
{
"date": "2024-08-08",
"text": "What a monument of human smallness is this idea of the philosopher king. What a contrast between it and the simplicity and humaneness of Socrates, who warned the statesman against the danger of being dazzled by his own power, excellence, and wisdom, and who tried to teach him what matters most that we are all frail human beings. What a decline from this world of irony and reason and truthfulness down to Plato's kingdom of the sage whose magical powers raise him high above ordinary men; although not quite high enough to forgo the use of lies, or to neglect the sorry trade of every shaman the selling of spells, of breeding spells, in exchange for power over his fellow men",
"source": "Karl Popper, The Open Society and Its Enemies"
},
{
"date": "2024-08-04",
"text": "Even if we know how to educate tomorrows professional programmer, it is not certain that the society we are living in will allow us to do so. The first effect of teaching a methodology —rather than disseminating knowledge— is that of enhancing the capacities of the already capable, thus magnifying the difference in intelligence. In a society in which the educational system is used as an instrument for the establishment of a homogenized culture, in which the cream is prevented from rising to the top, the education of competent programmers could be politically impalatable.",
"source": "Edsger Dijkstra, The Humble Programmer"
},
{
"date": "2024-07-30",
"text": "Friendship is not to be sought for its wages, but because its revenue consists entirely in the love which it implies",
"source": "Cicero, On Friendship"
},
{
"date": "2024-07-30",
"text": "Direct self observation is not nearly sufficient for us to know ourselves: we need history, for the past flows on within us in a hundred waves. Indeed, we ourselves are nothing but that which at every moment we experience of this continual flowing.",
"source": "Nietzsche, 1878, Human, All Too Human"
},
{
"date": "2024-07-30",
"text": "Im increasingly certain that there are others like me in the world, alive right now, quietly suppressing themselves for social reasons. I hear from more of them every month. They suppress themselves because they dont personally know of any House of Wisdom that they could attend to fully be themselves in. Because the scale and scope of their interests dont quite correspond with that of those of the people around them, and they dont know if its worth opening up about their inner truths because they believe, accurately according to their past experience, that the likeliest outcome is that people will misunderstand them. A confused “huh?” is often the best you can hope for. Far better than being mocked, insulted, laughed at, dismissed. Over the years, Ive increasingly developed a sense of lightness, clarity, courage and conviction in realizing that these are my people. That when Im writing for the younger version of myself, and the future versions of myself, Im writing for them. For us. All of us. Im a me, but Im also a we. And there is a deep kinship in that, a deep sense of belonging. And I have decided that I am willing to endure any amount of mockery and misunderstanding from the people who dont get it, to be a bridge to the people who do. Because more than anything else, that is what I wish I had in my life. A space to understand and be understood. I found it first mainly in books. I have since found it in like-minded nerds. And I hope to share it with literally anybody else who wants it",
"source": "Visakan Veerasamy, We Were Voyagers"
},
{
"date": "2024-07-30",
"text": "Nobody worth hero-worshipping would want you to worship them. They would want you to become heroic yourself.",
"source": "Visakan Veerasamy, We Were Voyagers"
},
{
"date": "2024-07-30",
"text": "Meek young men grow up in libraries, believing it is their duty to accept the views which Cicero, which Locke, which Bacon, have given; forgetful that Cicero, Locke, and Bacon were only young men in libraries when they wrote these books.",
"source": "Ralph Waldo Emerson, The American Scholar"
},
{
"date": "2024-07-30",
"text": "The question of whether Machines Can Think is about as relevant as the question of whether Submarines Can Swim",
"source": "Edsger Dijkstra, 1984, The Threats to Computing Science"
},
{
"date": "2024-07-30",
"text": "We must be very careful when we give advice to younger people; sometimes they follow it!",
"source": "Edsger Dijkstra, The Humble Programmer"
},
{
"date": "2024-07-13",
"text": "We are living through an advice pandemic and nobody appears to have yet discovered an effective vaccine.",
"source": "Tom Cox, Can You Please Stop Telling Me To Live My Best Life Please"
},
{
"date": "2024-07-08",
"text": "Bless you prison, bless you for being in my life. For there, lying upon the rotting prison straw, I came to realize that the object of life is not prosperity as we are made to believe, but the maturity of the human soul.",
"source": "Aleksandr Solzhenitsyn, The Gulag Archipelago"
},
{
"date": "2024-06-30",
"text": "Our legacy is to fill the Universe with children who laugh more than we were allowed to.",
"source": "Noah Smith, Toward a Shallower Future"
},
{
"date": "2024-05-16",
"text": "Congregations love to be scolded, but not reformed",
"source": "Will Durant, The Age of Faith"
},
{
"date": "2024-05-16",
"text": "Educate the children and it won't be necessary to punish the men.",
"source": "Pythagoras"
},
{
"date": "2024-05-15",
"text": "[...] books are the main peer group of any thinker.",
"source": "Henrik Karlsson, On Having More Interesting Ideas"
},
{
"date": "2024-05-07",
"text": "[Gratitude] is not only the greatest of virtues, but the parent of all the others.",
"source": "Cicero, Defense of Cnaeus Plancius, Ch. 33, Section 80",
"link": "https://www.perseus.tufts.edu/hopper/text?doc=Perseus%3Atext%3A1999.02.0020%3Atext%3DPlanc.%3Achapter%3D33%3Asection%3D80"
},
{
"date": "2024-04-29",
"text": "You are carrying God about you, you poor wretch, and know it not.",
"source": "Epictetus, quoted from Caesar and Christ by Will Durant"
},
{
"date": "2024-03-30",
"text": "The evil was not in the bread and circuses, per se, but in the willingness of the people to sell their rights as free men for full bellies and the excitement of the games which would serve to distract them from the other human hungers which bread and circuses can never appease.",
"source": "Cicero"
},
{
"date": "2024-03-25",
"text": "The heritage that we can now more fully transmit is richer than ever before. It is richer than that of Pericles, for it includes all the Greek flowering that followed him; richer than Leonardos, for it includes him and the Italian Renaissance; richer than Voltaires, for it embraces all the French Enlightenment and its ecumenical dissemination. If progress is real despite our whining, it is not because we are born any healthier, better, or wiser than infants were in the past, but because we are born to a richer heritage, born on a higher level of that pedestal which the accumulation of knowledge and art raises as the ground and support of our being. The heritage rises, and man rises in proportion as he receives it. History is, above all else, the creation and recording of that heritage; progress is its increasing abundance, preservation, transmission, and use. To those of us who study history not merely as a warning reminder of mans follies and crimes, but also as an encouraging remembrance of generative souls, the past ceases to be a depressing chamber of horrors; it becomes a celestial city, a spacious country of the mind, wherein a thousand saints, statesmen, inventors, scientists, poets, artists, musicians, lovers, and philosophers still live and speak, teach and carve and sing. The historian will not mourn because he can see no meaning in human existence except that which man puts into it; let it be our pride that we ourselves may put meaning into our lives, and sometimes a significance that transcends death. If a man is fortunate he will, before he dies, gather up as much as he can of his civilized heritage and transmit it to his children. And to his final breath he will be grateful for this inexhaustible legacy, knowing that it is our nourishing mother and our lasting life.",
"source": "The Lessons of History, Will & Ariel Durant"
},
{
"date": "2024-02-23",
"text": "The road to serfdom consists of working exponentially harder for a currency growing exponentially weaker.",
"source": "Vijay Boyapati, The Bullish Case for Bitcoin"
},
{
"date": "2024-02-16",
"text": "Loneliness is a tax you have to pay to atone for a certain complexity of mind.",
"source": "Alain de Botton"
},
{
"date": "2024-02-16",
"text": "So many people today — and even professional scientists— seem to me like someone who has seen thousands of trees but has never seen a forest. A knowledge of the historic and philosophical background gives that kind of independence from prejudices of his generation from which most scientists are suffering. This independence created by philosophical insight is — in my opinion — the mark of distinction between a mere artisan or specialist and a real seeker after truth.",
"source": "Albert Einstein to Robert A. Thornton, 7 December 1944, EA 61-574"
},
{
"date": "2024-02-06",
"text": "I see now more clearly than ever before that even our greatest troubles spring from something that is as admirable and sound as it is dangerous -- from our impatience to better the lot of our fellows.",
"source": "Karl Popper, The Open Society and it's Enemies, preface to the second edition"
},
{
"date": "2024-02-05",
"text": "[...] the most unfortunate of men is he who has not learned how to bear misfortune [...] men ought to order their lives as if they were fated to live both a long and a short time, [and] wisdom should be cherished as a means of traveling from youth to old age, for it is more lasting than any other possession.",
"source": "Bias of Priene, quoted from The Life of Greece by Will Durant, Ch. VI The Great Migration"
},
{
"date": "2024-02-02",
"text": "[...] teenagers are always on duty as conformists.",
"source": "Paul Graham, Why Nerds are Unpopular"
},
{
"date": "2024-01-01",
"text": "Why, Oppenheimer knows about everything. He can talk to you about anything you bring up. Well, not exactly. I guess there are a few things he doesn't know about. He doesn't know anything about sports.",
"source": "General Leslie Groves, quoted from American Prometheus: The Triumph and Tragedy of J. Robert Oppenheimer, pp. 185-186"
},
{
"date": "2023-12-22",
"text": "Life everywhere is life, life is in ourselves and not in the external. There will be people near me, and to be a human being among human beings, and remain one forever, no matter what misfortunes befall, not to become depressed, and not to falter -- this is what life is, herein lies its task.",
"source": "Fyodor Dostoevsky, in a letter to his brother, the day he was pardoned from execution by firing squad"
},
{
"date": "2023-12-13",
"text": "Math constitutes the language through which alone we can adequately express the great facts of the natural world. And it allows us to portray the changes of mutual relationship that unfold in creation. It is the instrument through which the weak mind of man can most effectually read his creator's works.",
"source": "Ada Lovelace, quoted from The Innovators by Walter Isaacson, Ch. 1"
},
{
"date": "2023-12-09",
"text": "It is wrong to think that belief in freedom always leads to victory; we must always be prepared for it to lead to defeat. If we choose freedom, then we must be prepared to perish along with it. [...] No, we do not choose political freedom because it promises us this or that. We choose it because it makes possible the only dignified form of human coexistence, the only form in which we can be fully responsible for ourselves. Whether we realize its possibilities depends on all kinds of things — and above all on ourselves.",
"source": "Karl Popper, On Freedom"
},
{
"date": "2023-12-07",
"text": "I think that there is only one way to science - or to philosophy, for that matter: to meet a problem, to see its beauty and fall in love with it; to get married to it and to live with it happily, till death do ye part - unless you should meet another and even more fascinating problem or unless, indeed, you should obtain a solution. But even if you do obtain a solution, you may then discover, to your delight, the existence of a whole family of enchanting, though perhaps difficult, problem children, for whose welfare you may work, with a purpose, to the end of your days.",
"source": "Karl Popper, Realism and the Aim of Science"
},
{
"date": "2023-12-07",
"text": "Hence, men who are governed by reason [...] desire for themselves nothing, which they do not also desire for the rest of mankind",
"source": "Spinoza, Part IV, Prop XVIII"
},
{
"date": "2023-09-26",
"text": "Among the nations who have adopted the Mosaic history of the world, the ark of Noah has been of the same use as was formerly to the Greeks and Romans the siege of Troy. On a narrow basis of acknowledged truth an immense but rude superstructure of fable has been erected,[...]",
"source": "Decline and Fall of the Roman Empire, Chapter IX, p. 240"
}
]

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: Tokyo-night-Dark
origin: https://github.com/enkia/tokyo-night-vscode-theme
Description: Original highlight.js style
Author: (c) Henri Vandersleyen <hvandersleyen@gmail.com>
License: see project LICENSE
Touched: 2022
*/.hljs-comment,.hljs-meta{color:#565f89}.hljs-deletion,.hljs-doctag,.hljs-regexp,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-tag,.hljs-template-tag,.hljs-variable.language_{color:#f7768e}.hljs-link,.hljs-literal,.hljs-number,.hljs-params,.hljs-template-variable,.hljs-type,.hljs-variable{color:#ff9e64}.hljs-attribute,.hljs-built_in{color:#e0af68}.hljs-keyword,.hljs-property,.hljs-subst,.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#7dcfff}.hljs-selector-tag{color:#73daca}.hljs-addition,.hljs-bullet,.hljs-quote,.hljs-string,.hljs-symbol{color:#9ece6a}.hljs-code,.hljs-formula,.hljs-section{color:#7aa2f7}.hljs-attr,.hljs-char.escape_,.hljs-keyword,.hljs-name,.hljs-operator{color:#bb9af7}.hljs-punctuation{color:#c0caf5}.hljs{background:#1a1b26;color:#9aa5ce}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Some files were not shown because too many files have changed in this diff Show More