{"id":51442793,"url":"https://github.com/nisfeb/ecash","last_synced_at":"2026-07-05T13:01:32.589Z","repository":{"id":363235338,"uuid":"1261686253","full_name":"nisfeb/ecash","owner":"nisfeb","description":"A Cashu ecash mint implemented in pure Hoon as an Urbit Gall agent — NUTs 00–12, BDHKE, DLEQ, P2PK, Lightning (LNbits/LND).","archived":false,"fork":false,"pushed_at":"2026-06-08T03:37:29.000Z","size":217,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-08T04:11:08.513Z","etag":null,"topics":["bdhke","bitcoin","blind-signatures","cashu","chaumian-ecash","ecash","gall-agent","hoon","lightning","lightning-network","p2pk","privacy","secp256k1","urbit"],"latest_commit_sha":null,"homepage":null,"language":"hoon","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nisfeb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-07T02:51:11.000Z","updated_at":"2026-06-08T03:37:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nisfeb/ecash","commit_stats":null,"previous_names":["nisfeb/ecash"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/nisfeb/ecash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nisfeb%2Fecash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nisfeb%2Fecash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nisfeb%2Fecash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nisfeb%2Fecash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nisfeb","download_url":"https://codeload.github.com/nisfeb/ecash/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nisfeb%2Fecash/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35154748,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bdhke","bitcoin","blind-signatures","cashu","chaumian-ecash","ecash","gall-agent","hoon","lightning","lightning-network","p2pk","privacy","secp256k1","urbit"],"created_at":"2026-07-05T13:01:31.758Z","updated_at":"2026-07-05T13:01:32.581Z","avatar_url":"https://github.com/nisfeb.png","language":"hoon","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ecash\n\nA Cashu ecash mint implemented in Hoon, running as a Gall agent on Urbit.\n\n## Overview\n\nA fully functional [Cashu](https://cashu.space) mint that implements blind Diffie-Hellman key exchange (BDHKE) with real secp256k1 cryptography, entirely in Hoon. It supports minting, melting, and swapping ecash tokens with Lightning Network integration and an admin dashboard.\n\nThe mint is self-sovereign: it runs inside your Urbit identity, is accessible via your ship's HTTP interface, and stores all state in Urbit's event log (crash-recoverable). The cryptography is implemented in **pure Hoon** with no external dependencies or native jets for BDHKE operations.\n\nBeyond standard Cashu, the mint includes two extensions for non-value-bearing use cases:\n\n- **Credential token extension** — raw `/cred/v1` endpoints for issuing zero-value blind-signed tokens (power-user / library consumers).\n- **Services layer** — a higher-level access-control layer built on top of credentials. Each service is a named scope with its own keyset, optional expiration, issuance cap, and per-service API-key allowlist. The dashboard surfaces services as a distinct tab and visually delineates \"value-bearing\" (sats) vs \"access\" (services) everywhere.\n\n## Supported NUTs\n\n| NUT | Name | Status |\n|-----|------|--------|\n| 00 | Cryptography | BDHKE with secp256k1, DLEQ proofs |\n| 01 | Mint public keys | `GET /v1/keys` |\n| 02 | Keysets | `GET /v1/keysets`, `GET /v1/keys/{id}`, per-keyset `input_fee_ppk` |\n| 03 | Swap | `POST /v1/swap` |\n| 04 | Mint (bolt11 + self) | `POST /v1/mint/quote/{method}`, `POST /v1/mint/{method}` |\n| 05 | Melt (bolt11 + self) | `POST /v1/melt/quote/{method}`, `POST /v1/melt/{method}` |\n| 06 | Mint info | `GET /v1/info` |\n| 07 | Token state check | `POST /v1/checkstate` |\n| 10 | Well-known secrets | Structured secret format `[\"kind\", {nonce, data, tags}]` |\n| 11 | P2PK | Pay-to-public-key with Schnorr signatures, multisig, locktime, refund |\n| 12 | DLEQ proofs | Included in all mint/swap responses |\n\n## Project Structure\n\nThe mint and the access-control extensions are **two separate Gall agents**:\n`%ecash` (the value mint) and `%ecash-services` (zero-value credentials + the services\nlayer). They share the secp256k1/BDHKE crypto, whose single source of truth is `desk/lib`.\n\n```\ndesk/                    → installs as %ecash (the value mint)\n  app/ecash.hoon         Main Gall agent (Cashu /v1/* + /apps/ecash/admin)\n  app/dashboard.txt      Admin dashboard HTML/JS, imported via /* at build time\n  sur/ecash.hoon         Shared types (keyset, quote, ln-backend, ...)\n  lib/bdhke.hoon         BDHKE protocol, hash-to-curve, DLEQ proofs   (canonical)\n  lib/curve.hoon         secp256k1 point arithmetic (pure Hoon)       (canonical)\n  mar/txt.hoon           Override for %txt mark (handles raw HTML asset import)\n  tests/test.hoon        Hoon unit tests\ndesk-services/           → installs as %ecash-services (cred + services, non-value)\n  app/ecash-services.hoon  Serves /cred/v1/*, /services/v1/*, /apps/ecash-services/admin\n  sur/ecash-services.hoon  cred-keyset / service types\n  lib/{curve,bdhke}.hoon   generated from desk/lib (gitignored; `make sync-libs`)\ntest-e2e.mjs             End-to-end mint/swap/checkstate/melt flow (9 assertions)\ntest-vectors.mjs         NUT-00 hash-to-curve official test vectors (3 vectors)\ntest-p2pk.mjs            P2PK/NUT-11 coverage (8 tests)\ntest-cred.mjs            Credential extension (31 tests)\ntest-services.mjs        Services layer: creation, allowlist, expiry, replay (34 tests)\ntest-lightning.mjs       bolt11 Lightning integration (requires mock-lnbits)\nmock-lnbits.mjs          Mock LNbits server for Lightning testing\n```\n\n## Installation\n\nBuild both desks (requires [peru](https://github.com/buildinspace/peru)), then\ninstall on your ship:\n\n```bash\ngit clone https://github.com/nisfeb/ecash \u0026\u0026 cd ecash\n./build.sh          # builds dist/ (%ecash) and dist-services/ (%ecash-services)\n```\n\nIn the dojo, create and mount the desk; then deploy the built desk into the mount\nand commit:\n\n```\n|new-desk %ecash\n|mount %ecash\n```\n```bash\n./build.sh -p /path/to/your/pier/ecash    # copies the built desk into the mount\n```\n```\n|commit %ecash\n|install our %ecash\n```\n\nThe mint generates a keyset with 10 denominations (1, 2, 4, 8, 16, 32, 64, 128, 256, 512 sats) on first install — with Lightning off (`%none`) and the free `self` method disabled, so it's safe until you configure it.\n\nTo also run the zero-value credentials/access layer, install **`%ecash-services`** the same way:\n\n```\n|new-desk %ecash-services\n|mount %ecash-services\n```\n```bash\n./build.sh services -p /path/to/your/pier/ecash-services\n```\n```\n|commit %ecash-services\n|install our %ecash-services\n```\n\n**Running a public mint?** See [`docs/INSTALL.md`](docs/INSTALL.md) for the full\nwalkthrough — installing both desks, exposing the ship over HTTPS with a\nrate-limiting reverse proxy, configuring the Lightning backend, and the\npre-production safety checklist.\n\n---\n\n## Demo\n\n`demo.mjs` is a narrated, presentation-paced walkthrough of the whole ecash\nlifecycle against a running mint — useful for showing the system to others. In\nfive acts (Alice \u0026 Bob) it: meets the mint, deposits sats over Lightning to\nreceive blind-signed ecash, pays a peer via a swap, shows that double-spending is\nrejected, and cashes out back to Lightning with NUT-08 change. The crypto is real\nBDHKE; no real money moves (a mock LNbits backend simulates the Lightning side).\n\n```\nnpm run mock:lnbits        # start the mock Lightning backend on :3338\n# configure the mint to use it (admin), then:\nnpm run demo               # narrated, paced for a live audience\nnode demo.mjs --fast       # same flow, no pauses\nnode demo.mjs --amount 250 # vary Alice's deposit (12–1000 sats)\n```\n\nIf a prerequisite is missing (mint unreachable, no Lightning backend, mock not\nrunning) the demo prints the exact command to fix it instead of failing. Override\nendpoints with `SHIP_URL`, `MOCK_URL`, and `API_KEY` env vars.\n\n---\n\n## Cashu Protocol Endpoints\n\nAll standard Cashu endpoints are unauthenticated, served at `/v1/*`:\n\n| Method | Path | Description |\n|--------|------|-------------|\n| GET | `/v1/info` | Mint info (NUT-06) |\n| GET | `/v1/keys` | Active keyset public keys (NUT-01) |\n| GET | `/v1/keys/{keyset_id}` | Keys for specific keyset (NUT-02) |\n| GET | `/v1/keysets` | Keyset metadata (NUT-02) |\n| POST | `/v1/swap` | Swap tokens (NUT-03) |\n| POST | `/v1/mint/quote/{method}` | Create mint quote (NUT-04) |\n| GET | `/v1/mint/quote/{method}/{quote_id}` | Check mint quote (NUT-04) |\n| POST | `/v1/mint/{method}` | Mint tokens from paid quote (NUT-04) |\n| POST | `/v1/melt/quote/{method}` | Create melt quote (NUT-05) |\n| GET | `/v1/melt/quote/{method}/{quote_id}` | Check melt quote (NUT-05) |\n| POST | `/v1/melt/{method}` | Melt tokens to pay invoice (NUT-05) |\n| POST | `/v1/checkstate` | Check token spent state (NUT-07) |\n\n### Mint Methods\n\n- **`self`** — Instant minting/melting with no Lightning required. Useful for testing and on-ship token operations. **Disabled by default** on a value-bearing mint; enable it via the admin Settings (`self_method_enabled`) for testing.\n- **`bolt11`** — Lightning Network integration. Requires a configured Lightning backend.\n\n### Standard Flows\n\n**Minting tokens (deposit):**\n```\nPOST /v1/mint/quote/bolt11   {\"amount\": 100}\n→ {\"quote\": \"abc123\", \"request\": \"lnbc100n1...\", \"state\": \"UNPAID\", ...}\n\n# User pays the Lightning invoice, then:\nGET /v1/mint/quote/bolt11/abc123\n→ {\"state\": \"PAID\", ...}\n\nPOST /v1/mint/bolt11   {\"quote\": \"abc123\", \"outputs\": [{B_: \"02...\", amount: 1}, ...]}\n→ {\"signatures\": [{C_: \"03...\", amount: 1, dleq: {...}}, ...]}\n```\n\n**Melting tokens (withdraw):**\n```\nPOST /v1/melt/quote/bolt11   {\"request\": \"lnbc50n1...\"}\n→ {\"quote\": \"def456\", \"amount\": 50, \"fee_reserve\": 10, \"state\": \"UNPAID\", ...}\n\nPOST /v1/melt/bolt11   {\"quote\": \"def456\", \"inputs\": [{C: \"03...\", secret: \"...\", amount: 1, id: \"01...\"}, ...]}\n→ {\"state\": \"PAID\", \"payment_preimage\": \"abc...\"}\n```\n\n**Swapping tokens:**\n```\nPOST /v1/swap\n{\n  \"inputs\": [{C: \"03...\", secret: \"old-secret\", amount: 4, id: \"01...\"}],\n  \"outputs\": [{B_: \"02...\", amount: 2}, {B_: \"02...\", amount: 2}]\n}\n→ {\"signatures\": [{C_: \"03...\", amount: 2, dleq: {...}}, ...]}\n```\n\n**P2PK tokens (NUT-11):**\n```\n# Secret locks token to recipient's public key:\nsecret = '[\"P2PK\", {\"nonce\": \"abc\", \"data\": \"02recipient_pubkey...\", \"tags\": []}]'\n\n# Recipient signs SHA256(secret) with their private key to spend:\nwitness = '{\"signatures\": [\"schnorr_sig_hex\"]}'\n\nPOST /v1/swap\n{\"inputs\": [{C: \"...\", secret: \"...\", amount: 1, id: \"...\", witness: \"...\"}], \"outputs\": [...]}\n```\n\nSupports multisig (`n_sigs` + `pubkeys` tags), locktime, and refund keys.\n\n---\n\n## Lightning Backend\n\nThe mint supports two Lightning backends:\n\n- **LNbits** — `{type: \"lnbits\", url: \"...\", api_key: \"...\"}`\n- **LND** — `{type: \"lnd\", url: \"...\", macaroon: \"...\"}`\n\nConfigure via the admin dashboard, admin API, or dojo:\n```\n:ecash [%lnbits 'http://your-lnbits:5000' 'your-api-key']\n```\n\n### Settings\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `fee_reserve_pct` | 100 (1%) | Fee reserve percentage in basis points |\n| `fee_reserve_min` | 10 | Minimum fee reserve in sats |\n| `quote_ttl_secs` | 3600 | Quote time-to-live in seconds |\n| `self_method_enabled` | false | Enable the no-payment `self` mint/melt method (testing only) |\n\n---\n\n## Admin Dashboard\n\n`GET /apps/ecash/admin` serves a single-page admin UI (authenticated via the ship cookie) with six tabs:\n\n- **Overview** — Mint/melt quote summaries, active keyset, liability counters, settings form\n- **Keysets** — List, generate, activate/deactivate, set fees, view denomination keys\n- **Quotes** — Filterable list (all/mint/melt/unpaid/paid/issued), delete\n- **Tokens** — Spent counts, check secret/Y-point status\n- **Lightning** — Backend status, configure/remove, connection info\n- **Info** — NUT-06 mint name/description, edit form\n\nStats bar shows: tokens issued/spent, issued/redeemed/outstanding sats, LN backend, pending requests.\n\nService and credential management lives in the separate `%ecash-services` agent, which serves its own dashboard at `/apps/ecash-services/admin` (see the Credential and Services sections below).\n\n## Admin API\n\nAll admin endpoints require the ship's auth cookie. Unauthenticated requests receive `401 unauthorized`. Base path: `/apps/ecash/admin/api`\n\n### Read endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| GET | `/overview` | Mint stats (keysets, quotes, issued/redeemed sats, LN status) |\n| GET | `/keysets` | All keysets with full detail |\n| GET | `/keysets/{id}` | Single keyset detail |\n| GET | `/quotes` | All mint and melt quotes |\n| GET | `/spent` | Spent token counts |\n| GET | `/lightning` | Lightning backend status |\n| GET | `/info` | Mint name and description |\n| GET | `/settings` | Fee reserve and quote TTL settings |\n\n### Write endpoints\n\n| Method | Path | Body | Description |\n|--------|------|------|-------------|\n| POST | `/keysets/generate` | — | Generate new keyset (inactive) |\n| POST | `/keysets/activate` | `{id}` | Activate keyset (deactivates previous) |\n| POST | `/keysets/deactivate` | `{id}` | Deactivate keyset |\n| POST | `/keysets/set-fee` | `{id, fee}` | Set `input_fee_ppk` (recomputes keyset ID) |\n| POST | `/quotes/delete` | `{type, id}` | Delete a quote |\n| POST | `/spent/check` | `{secret}` or `{Y}` | Check if secret/Y-point is spent |\n| POST | `/lightning/configure` | `{type, url, api_key\\|macaroon}` | Configure LN backend |\n| POST | `/lightning/test` | — | Test LN connection |\n| POST | `/info/update` | `{name, description}` | Update mint name/description |\n| POST | `/settings` | `{fee_reserve_pct, fee_reserve_min, quote_ttl_secs}` | Update settings |\n| GET | `/services` | — | List all services with full allowlist keys |\n| GET | `/services/{name}` | — | Service detail including allowlist |\n| POST | `/services/create` | `{name, title, description, expires?, max_issuance?}` | Create service (auto-generates backing cred keyset) |\n| POST | `/services/update` | `{name, title?, description?, expires?, max_issuance?}` | Patch metadata |\n| POST | `/services/activate` | `{name}` | Reopen a deactivated service |\n| POST | `/services/deactivate` | `{name}` | Close issue/verify/redeem for a service |\n| POST | `/services/delete` | `{name}` | Delete (only when inactive and never issued) |\n| POST | `/services/allowlist/add` | `{name, key}` | Add an API access key to a service |\n| POST | `/services/allowlist/remove` | `{name, key}` | Remove an API access key |\n\n---\n\n## Credential Token Extension\n\n\u003e **Phase 3:** the credential and services layers run as a **separate `%ecash-services` agent**\n\u003e (desk `desk-services/`). Public endpoints are `/cred/v1/*` and `/services/v1/*`; the admin API\n\u003e base is `/apps/ecash-services/admin/api/`. Install with `|install our %ecash-services`. The\n\u003e sections below describe its behavior.\n\nThe credential extension enables issuing **zero-value tokens** that serve as access credentials rather than value-bearing ecash. This is a non-standard extension that does not affect Cashu protocol compliance.\n\n### Use Case\n\nA \"space\" or application gates access behind payment:\n\n1. **User pays** — sends real ecash tokens to the space application\n2. **Space redeems payment** — swaps/verifies the value tokens via the standard Cashu API\n3. **Space issues credentials** — requests N credential tokens (e.g., 30 daily login passes) from the mint via `/cred/v1/issue`\n4. **User presents credentials** — one token per day; the space app verifies via `/cred/v1/verify` and consumes via `/cred/v1/redeem`\n\n### Design Principles\n\n- Credential keysets are **completely separate** from value keysets\n- Credential keyset IDs use prefix `c0` (value keysets use `01`) — no collision possible\n- All credential tokens have `amount: 0` — non-zero amounts are rejected\n- Credential spent set is separate from the value token spent set\n- Same BDHKE + DLEQ cryptography as value tokens\n- No modification to any `/v1/*` Cashu endpoint\n\n### Credential Endpoints\n\nAccessible at `/cred/v1/*` (authenticated) and `/cred/v1/*` (when Eyre binding is active):\n\n| Method | Path | Description |\n|--------|------|-------------|\n| GET | `/cred/v1/keys` | Active credential keyset public keys |\n| GET | `/cred/v1/keys/{keyset_id}` | Specific credential keyset keys |\n| GET | `/cred/v1/keysets` | Credential keyset metadata (IDs + active status) |\n| POST | `/cred/v1/issue` | Issue credential tokens (blind sign) |\n| POST | `/cred/v1/verify` | Check validity + spent status (read-only) |\n| POST | `/cred/v1/redeem` | Verify and mark as spent |\n\n### Credential Admin Endpoints\n\nUnder `/apps/ecash-services/admin/api/cred/*`:\n\n| Method | Path | Description |\n|--------|------|-------------|\n| POST | `/cred/keysets/generate` | Generate new credential keyset (active by default) |\n| POST | `/cred/keysets/activate` | Activate credential keyset `{id}` |\n| POST | `/cred/keysets/deactivate` | Deactivate credential keyset `{id}` |\n| GET | `/cred/overview` | Credential stats (keysets, issued, spent) |\n\n### Credential Flow\n\n**1. Setup (one-time, by mint admin or space app):**\n```\nPOST /apps/ecash-services/admin/api/cred/keysets/generate\n→ {\"id\": \"c0abc...\", \"active\": true, \"keys\": {\"0\": \"02pubkey...\"}}\n```\n\n**2. Issuing credentials (space app, after verifying payment):**\n```javascript\n// Get credential keyset public key\nGET /cred/v1/keys\n→ {\"keysets\": [{\"id\": \"c0abc...\", \"keys\": {\"0\": \"02pubkey...\"}}]}\n\n// Create blinded outputs (client-side BDHKE)\n// For each credential:\n//   secret = random unique string\n//   Y = hashToCurve(secret)\n//   k = random blinding factor\n//   B_ = Y + k*G\noutputs = [\n  {\"B_\": \"02blinded...\", \"amount\": 0, \"id\": \"c0abc...\"},\n  // ... repeat for N credentials\n]\n\n// Request blind signatures\nPOST /cred/v1/issue\n{\"outputs\": [...]}\n→ {\"signatures\": [{\"C_\": \"02signed...\", \"amount\": 0, \"id\": \"c0abc...\", \"dleq\": {...}}, ...]}\n\n// Unblind each token (client-side):\n// C = C_ - k * pubkey\n```\n\n**3. Verifying a credential (space app, on each access request):**\n```\nPOST /cred/v1/verify\n{\"proofs\": [{\"C\": \"02unblinded...\", \"secret\": \"...\", \"amount\": 0, \"id\": \"c0abc...\"}]}\n→ {\"valid\": [{\"secret\": \"...\", \"valid\": true, \"spent\": false}]}\n```\n\n**4. Consuming a credential (space app, marks as permanently used):**\n```\nPOST /cred/v1/redeem\n{\"proofs\": [{\"C\": \"02unblinded...\", \"secret\": \"...\", \"amount\": 0, \"id\": \"c0abc...\"}]}\n→ {\"redeemed\": [{\"secret\": \"...\", \"redeemed\": true}]}\n```\n\nA redeemed credential cannot be redeemed again — the mint returns `credential-already-spent`.\n\n### Error Responses\n\n| Error | Cause |\n|-------|-------|\n| `credential-amount-must-be-zero` | Output has non-zero amount |\n| `missing-keyset-id` | Output missing `id` field |\n| `unknown-credential-keyset` | Keyset ID not found in credential keysets |\n| `credential-keyset-inactive` | Keyset has been deactivated |\n| `invalid-credential` | Signature verification failed |\n| `credential-already-spent` | Token was already redeemed |\n\n---\n\n## Services Layer (non-value-bearing access control)\n\nThe services layer builds on the credential extension to provide named, scoped access-control tokens. Where `/cred/v1/*` exposes raw credential keysets for power users, `/services/v1/*` gives each application its own named scope with policy: an expiration, an issuance cap, an optional API-key allowlist, and per-service metadata you can surface in a UI.\n\n### Model\n\nA **service** is a named wrapper around a dedicated `cred-keyset`:\n\n| Field | Meaning |\n|---|---|\n| `name` | URL slug (e.g. `chat`, `vip`, `api-tier-1`) |\n| `title` / `description` | Human-readable metadata |\n| `kind` | `single-use` (phase 1 / phase 2 only kind) |\n| `ks_id` | Backing credential keyset ID — auto-generated on `create`, never shared between services |\n| `active` | If false, issue/verify/redeem return 400 `service-inactive` |\n| `expires` | Optional hard cutoff (unix seconds). Past-expiry → 400 `service-expired` across all endpoints |\n| `max_issuance` | Optional cap on tokens ever issued; over-cap → 400 `service-issuance-cap-reached` |\n| `issued` / `redeemed` | Running counters |\n| `allowlist` | Set of plaintext API keys. Empty → `/issue` is public. Non-empty → caller must supply matching `access_key` in the issue body |\n\nBecause each service has a **dedicated keyset** auto-generated at `create` time, cross-service token reuse is impossible at the crypto layer: a chat token is signed by chat's private key and simply won't verify against vip's keyset. You don't need additional binding logic to enforce scoping.\n\n### Public Endpoints\n\nUnder `/services/v1/*` (unauthenticated):\n\n| Method | Path | Description |\n|---|---|---|\n| GET | `/services/v1/list` | Active services only. Each entry shows `name`, `title`, `description`, `kind`, `ks_id`, `active`, `issued`, `redeemed`, `allowlist_count`, `allowlist_required`, `expires`, `max_issuance`, `created`. No plaintext keys are ever returned on this endpoint. |\n| GET | `/services/v1/{name}` | Service detail (public view — same shape as list entry, no plaintext keys) |\n| POST | `/services/v1/{name}/issue` | Blind-sign outputs. Body: `{access_key?, outputs: [{B_, amount: 0, id}]}`. Returns `{signatures: [...]}`. Gated by allowlist when set. |\n| POST | `/services/v1/{name}/verify` | Verify proofs without spending. Body: `{proofs: [...]}`. Returns `{results: [{secret, valid, spent}]}`. Always public. |\n| POST | `/services/v1/{name}/redeem` | Verify and idempotently mark as spent. Body: `{proofs: [...]}`. Returns `{redeemed: [{secret, status}]}` where `status` is `fresh` (first redemption) or `replay` (already spent — retry after network drop). Always public. |\n\n### Redemption Semantics (idempotent replay)\n\nRedeem is safe to retry:\n\n- All proofs valid and fresh → 200, each marked `status: fresh`, `cred-spent` updated, `service.redeemed` incremented\n- All proofs valid, already spent via this service → 200, each marked `status: replay`, **no state change**\n- Any proof fails crypto verification (bad signature or wrong keyset) → 400 `invalid-service-token` for the whole batch\n\nCallers that want to know whether a token was first-use vs retry can inspect the per-token `status` field. Callers that don't care can treat any 200 as \"token accepted.\"\n\n### Allowlist Gating\n\nEach service has an `allowlist` set. When empty (the default on create), `/issue` is public — any caller can mint credentials for the service. When non-empty, the caller must include `access_key` in the request body, and its value must be in the allowlist, else 403 `service-access-denied`. `/verify` and `/redeem` are never gated — anyone with a valid token can use it.\n\nThe admin dashboard renders plaintext keys (admin-only). Public GET endpoints only expose `allowlist_count` and `allowlist_required`, never the keys themselves.\n\n```bash\n# Operator creates a gated service:\ncurl -X POST http://localhost:8080/apps/ecash-services/admin/api/services/create \\\n  -H \"Cookie: urbauth-~zod=...\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"vip\", \"title\": \"VIP\", \"description\": \"Paid tier access\"}'\n\n# Operator adds a key and hands it to an authorized client out of band:\ncurl -X POST http://localhost:8080/apps/ecash-services/admin/api/services/allowlist/add \\\n  -H \"Cookie: urbauth-~zod=...\" -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"vip\", \"key\": \"bus-secret-xyz\"}'\n\n# Authorized client mints credentials (no urbauth required, just the key):\ncurl -X POST http://localhost:8080/services/v1/vip/issue \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"access_key\": \"bus-secret-xyz\", \"outputs\": [{\"B_\": \"02...\", \"amount\": 0, \"id\": \"c0...\"}]}'\n\n# Anyone holding a valid VIP token can redeem — no key needed:\ncurl -X POST http://localhost:8080/services/v1/vip/redeem \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"proofs\": [{\"C\": \"02...\", \"secret\": \"...\", \"amount\": 0, \"id\": \"c0...\"}]}'\n```\n\n### Services Error Responses\n\n| Error | HTTP | Cause |\n|-------|------|-------|\n| `service-not-found` | 404 | `name` does not resolve to a service |\n| `service-inactive` | 400 | Service is deactivated |\n| `service-expired` | 400 | `expires` is set and in the past |\n| `service-issuance-cap-reached` | 400 | `max_issuance` would be exceeded by this issue call |\n| `service-access-denied` | 403 | Allowlist is non-empty and `access_key` is missing or wrong |\n| `invalid-service-token` | 400 | At least one proof failed crypto verification or was signed by a different keyset |\n| `service-already-exists` | 409 | Create called with a name that is already registered |\n| `service-has-issued-tokens` | 400 | Delete attempted on a service that ever issued a token (use deactivate instead) |\n| `deactivate-before-delete` | 400 | Delete attempted while `active=true` |\n\n---\n\n## Cryptography\n\n### secp256k1\n\nPublic keys are generated using `priv-to-pub:secp256k1:secp:crypto` from zuse (jet-accelerated). BDHKE point operations use pure Hoon arithmetic in `lib/curve.hoon` — scalar multiplication runs in **Jacobian coordinates** (one field inversion per scalar-mult instead of one per point op, ~7× faster than naive affine; `npm run bench`).\n\n### BDHKE (Blind Diffie-Hellman Key Exchange)\n\n```\nWallet:  Y = hashToCurve(secret)\n         B_ = Y + k*G                    (blinded message)\nMint:    C_ = privkey * B_               (blind signature)\nWallet:  C  = C_ - k*pubkey             (unblind)\nVerify:  C  == privkey * hashToCurve(secret)\n```\n\n### DLEQ Proofs\n\nEvery blind signature includes a DLEQ proof (Fiat-Shamir sigma protocol) proving the mint used the correct private key without revealing it.\n\n### Hash-to-Curve\n\nDomain-separated SHA-256 with counter-based retry:\n```\ndomain_sep = \"Secp256k1_HashToCurve_Cashu_\"\nmsg_hash = SHA256(domain_sep || secret_bytes)\nfor counter in 0..65535:\n  h = SHA256(msg_hash || counter_le_bytes)\n  try: return point_from_x(0x02 || h)\n```\n\n---\n\n## Testing\n\n**Prerequisites:** Node.js with `@noble/secp256k1`, `@noble/curves`, and `@noble/hashes`.\n\n```bash\nnpm install @noble/secp256k1 @noble/curves @noble/hashes\n```\n\n**End-to-end tests** (sat flows):\n```bash\nnode test-e2e.mjs          # mint / swap / checkstate / melt with change  (9)\nnode test-vectors.mjs      # NUT-00 hash-to-curve test vectors             (3)\nnode test-p2pk.mjs         # P2PK / NUT-11 coverage                        (8)\n```\n\n**Services tests** (the services layer):\n```bash\nnode test-services.mjs     # create/issue/verify/redeem, allowlist,\n                           # expiration, max-issuance, idempotent replay  (34)\n```\n\n**Credential tests** (generate a credential keyset first via the admin API):\n```bash\nnode test-cred.mjs         # 31 tests against /cred/v1/*\n```\n\n**Lightning tests** (requires mock LNbits):\n```bash\nnode mock-lnbits.mjs \u0026     # Start mock on port 3338\n# Configure mint: :ecash [%lnbits 'http://localhost:3338' 'test-api-key']\nnode test-lightning.mjs    # bolt11 integration coverage\n```\n\n**Security tests (Phase 1):**\n```bash\nURBAUTH_COOKIE=\u003cship-cookie\u003e npm run test:security\n# admin auth (401/200), legacy endpoints removed (404),\n# parse robustness (400 not crash), self-method gating (disabled→400)\n```\n\n**Wallet conformance (Phase 2):** drives the real [`@cashu/cashu-ts`](https://github.com/cashubtc/cashu-ts)\nlibrary through the full standard flow (loadMint → mint → swap → receive → melt) and verifies\nthe mint's NUT-12 DLEQ proofs. The mint's keyset IDs match cashu-ts's `deriveKeysetId` (NUT-02)\nand its DLEQ proofs pass `hasValidDleq`.\n```bash\nnode mock-lnbits.mjs \u0026                          # mock Lightning on :3338\nURBAUTH_COOKIE=\u003cship-cookie\u003e npm run test:conformance\n```\n\n**Hoon unit tests:**\n```\n-test /=ecash=/tests/test/hoon\n```\n\nThe JS suites assert mint/swap/melt value conservation, NUT-12 DLEQ, P2PK multisig, credentials, service scoping, admin auth, and parse robustness. Run `npm run test:all` for the full set.\n\n---\n\n## State\n\nThe `%ecash` agent state (version 13) contains (the credential/services fields moved to the\n`%ecash-services` agent; later migrations added the bolt11 melt-reconciliation state):\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `keysets` | `(map @t keyset)` | Value keysets (pubkeys, privkeys, unit, fee) |\n| `active-keyset` | `@t` | Currently active keyset ID |\n| `spent` / `spent-ys` | `(set @t)` | Spent secrets and Y-points (double-spend prevention) |\n| `counter` | `@ud` | Total value tokens issued |\n| `mint-quotes` / `melt-quotes` | `(map @t quote)` | Active and historical quotes |\n| `ln-config` | `ln-backend` | Lightning backend (lnbits/lnd/none) |\n| `pending` | `(map @ta pending-req)` | In-flight Lightning HTTP requests |\n| `total-issued-sats` / `total-redeemed-sats` | `@ud` | Liability tracking |\n| `mint-name` / `mint-description` | `@t` | NUT-06 mint metadata |\n| `fee-reserve-pct` / `fee-reserve-min` | `@ud` | Melt fee reserve config |\n| `quote-ttl-secs` | `@ud` | Quote expiry duration |\n| `melt-change` | `(map @t (list json))` | NUT-08 change signatures keyed by melt quote-id |\n| `self-method-enabled` | `?` | Whether the no-payment `self` mint/melt method is enabled (default off) |\n\nState migrations are handled automatically across all versions (state-6 through state-13). The\nstate-10→11 migration drops the `cred-keysets` / `cred-spent` / `cred-counter` / `services`\nfields, which now live in the `%ecash-services` agent (`state-0` there); later migrations add\nthe bolt11-melt reconciliation state (`pending`/`melt-inflight`). Historical versions before\nstate-6 have been dropped from the migration chain since there are no known deployments at those\nversions.\n\n## License\n\n[PolyForm Noncommercial License 1.0.0](LICENSE.md) — source-available; free for noncommercial\nuse. See `LICENSE.md` for terms.\n\n## Security\n\nThis mint handles value. It went through multiple adversarial security audits; the threat model\nand operator procedures (especially Lightning melt reconciliation and stuck-payment recovery)\nare documented in [`docs/operator-runbook.md`](docs/operator-runbook.md). Before running against\nreal value, read the runbook, start on a freshly-generated keyset, and do a small live shakedown\nagainst your Lightning backend.\n\n---\n\n*Built on Urbit vere-4.3, zuse kelvin 409*\n*Cashu protocol: https://cashu.space*\n*secp256k1 (browser): @noble/secp256k1 v1.7.1 via esm.sh*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnisfeb%2Fecash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnisfeb%2Fecash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnisfeb%2Fecash/lists"}