From 3ddc292dfc48e8b0e397b11d27ca0d9aa817451d Mon Sep 17 00:00:00 2001 From: teahubxyz Date: Fri, 16 Oct 2026 10:42:18 +0000 Subject: [PATCH] ventures/slyng: README, the skill for agents, the font license, and the build tools tools/build.sh runs on the refresh runner: the tests, then the two screenshots, served by tools/serve.mjs (node only, so the build needs node, curl and Chrome and nothing else). tools/og.html is the share card it shoots. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ww3hgdnVzTeB85eSsuPLuF --- ventures/slyng/README.md | 79 ++++++++++++++++++++++ ventures/slyng/fonts/GUST-FONT-LICENSE.txt | 30 ++++++++ ventures/slyng/fonts/MANIFEST-SLYNG.txt | 13 ++++ ventures/slyng/skill.md | 67 ++++++++++++++++++ ventures/slyng/tools/build.sh | 50 ++++++++++++++ ventures/slyng/tools/bundle.sh | 15 ++++ ventures/slyng/tools/og.html | 29 ++++++++ ventures/slyng/tools/serve.mjs | 28 ++++++++ 8 files changed, 311 insertions(+) create mode 100644 ventures/slyng/README.md create mode 100644 ventures/slyng/fonts/GUST-FONT-LICENSE.txt create mode 100644 ventures/slyng/fonts/MANIFEST-SLYNG.txt create mode 100644 ventures/slyng/skill.md create mode 100644 ventures/slyng/tools/build.sh create mode 100644 ventures/slyng/tools/bundle.sh create mode 100644 ventures/slyng/tools/og.html create mode 100644 ventures/slyng/tools/serve.mjs diff --git a/ventures/slyng/README.md b/ventures/slyng/README.md new file mode 100644 index 0000000..d90ef64 --- /dev/null +++ b/ventures/slyng/README.md @@ -0,0 +1,79 @@ +# SLYNG + +Sling sats and notes on nostr: https://slyng.yolopreneur.lol/ + +Aim at someone you follow, pull back for more, let go to send. Sats go as a zap (NIP-57) from your own wallet; the +note goes private and gift-wrapped (NIP-17). A yolo venture on [yolopreneur.lol](https://yolopreneur.lol/), +October 2026. The Y in the name is the slingshot. + +## What it is + +One static page, no backend, no dependencies. This folder is the site; `tests/` and `tools/` come along. + +- `index.html`, `style.css`, `app.js`: the page. The slingshot fills the first screen; below it the story, how it + works, the agent link, plans, the fine print. +- `sling.js`: the slingshot. The pull is a polar coordinate around the fork: its angle picks who on the fence, its + length picks the amount on the ladder (a note, then 21, 100, 210, 1,000, 2,100, 5,000, 10,000, 21,000 sats). Mouse, + touch and keyboard (arrows aim and stretch, Enter lets go). The geometry is exported for the tests. +- `lib/crypto.js`: SHA-256, HMAC, HKDF, secp256k1 (BIP-340 Schnorr and ECDH), ChaCha20, NIP-44 v2, and NIP-04 for + wallets that still need it. Checked against the BIP-340 and NIP-44 test vectors. +- `lib/nostr.js`: bech32 and NIP-19, events, signers (a key in the page, or a NIP-07 extension), a relay pool that + answers NIP-42 AUTH, NIP-05 lookups. +- `lib/zap.js`: lightning addresses, LNURL-pay, zap requests and receipts, and the check that an invoice is for exactly + the amount pulled before anything is paid. +- `lib/wallet.js`: the sender's wallet, WebLN (a browser extension) or Nostr Wallet Connect (NIP-47). +- `lib/dm.js`: private notes, sealed and gift-wrapped (NIP-17, NIP-59). +- `skill.md`, `llms.txt`: for agents. An agent can aim (a link with `#to=…&sats=…¬e=…`); only a person lets go. +- `fonts/`: Slyng Condensed and Slyng Text, cut from TeX Gyre Heros Condensed and TeX Gyre Adventor (GUST Font + License; `fonts/MANIFEST-SLYNG.txt` says what changed). +- `tools/devnet.mjs`: a nostr network on localhost for the tests: a relay with optional AUTH, a lightning address + server that publishes zap receipts, and a Nostr Wallet Connect wallet. + +## Decisions + +- **Static, no server, no Cloudflare costs.** The page talks to nostr relays and to recipients' lightning address + servers from the browser. The Content-Security-Policy allows scripts, styles and fonts from the page only, and + connections to `https:` and `wss:` (plus localhost, for the devnet). +- **Non-custodial.** Sats move from the sender's wallet to the recipient's; SLYNG never holds them. The invoice amount + is read from the BOLT-11 prefix and must match the pull, or nothing is paid. +- **A sling lands whole or not at all.** With sats and a note, the wallet pays first and the note goes only if the + payment did. The stone is in the air for 2.6 seconds, and catching it back cancels everything. +- **Zaps are public, notes are private.** The zap request carries no comment; the note travels separately, gift + wrapped, to the recipient's NIP-17 inbox relays. +- **Demo first.** Until someone signs in, four made-up people stand on the fence and the sats are pretend; nothing + leaves the page. Signing in is a nostr extension (preferred), a key made here, or a pasted nsec. +- **Verify everything.** Every event from a relay is checked (id and signature) before it's used, so a relay can't + swap in someone else's lightning address. + +## Run it + +```sh +node --test tests/*.test.js # vectors, notes, zaps, the slingshot, and the devnet flow +node tools/serve.mjs # in this folder, then open http://127.0.0.1:8000/ +node tools/devnet.mjs --auth # a local relay, lightning address server and wallet; prints test keys +python3 tests/smoke.py # Chromium (Playwright), desktop and phone: the demo, and a live sling via the devnet +tools/build.sh # the tests, then shots/desktop.png and shots/og.png (needs Chrome or Chromium) +``` + +To sling by hand against the devnet, open `http://127.0.0.1:8000/?relays=ws://127.0.0.1:` and sign in with one +of the keys it printed (paste the `sk` as a secret key). + +## Ship it + +SLYNG lives where the yolo ventures live: `ventures/slyng/` on the gh-pages branch of teahubxyz/time, served at +https://slyng.yolopreneur.lol/. To ship a change, commit this folder as `ventures/slyng/` to a branch of +teahubxyz/time (say `venture/slyng`) and run Time's refresh workflow (Actions -> refresh) with slug `slyng` and from +`venture/slyng`. It runs `tools/build.sh` on the runner, lands the folder on gh-pages in one commit, and asks Teahub +pages to pull. + +Or as a bundle: `tools/bundle.sh` makes `slyng.tgz` and its sha256 for the seed workflow, when the bundle can reach +the runner (a Boring blob download link). + +## Next + +- Three front doors: slyng.app (notes), slyng.money (sats), slyng.ing (the market), maybe slyng.my for listings + (`slyng.my/oldbike`, with `?via=` on a repost so the sale splits to whoever brought the buyer). +- The market: stalls for local and digital goods, offers slung at a stall and countered, the deal desk as stall-keeper. +- Files: sling a file and the key to open it. +- Reach: for notes, a longer pull goes further (one person, a group, everyone). +- Remote signers (NIP-46). A Whop app. diff --git a/ventures/slyng/fonts/GUST-FONT-LICENSE.txt b/ventures/slyng/fonts/GUST-FONT-LICENSE.txt new file mode 100644 index 0000000..60b5500 --- /dev/null +++ b/ventures/slyng/fonts/GUST-FONT-LICENSE.txt @@ -0,0 +1,30 @@ +% This is a preliminary version (2006-09-30), barring acceptance from +% the LaTeX Project Team and other feedback, of the GUST Font License. +% (GUST is the Polish TeX Users Group, http://www.gust.org.pl) +% +% For the most recent version of this license see +% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt +% or +% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt +% +% This work may be distributed and/or modified under the conditions +% of the LaTeX Project Public License, either version 1.3c of this +% license or (at your option) any later version. +% +% Please also observe the following clause: +% 1) it is requested, but not legally required, that derived works be +% distributed only after changing the names of the fonts comprising this +% work and given in an accompanying "manifest", and that the +% files comprising the Work, as listed in the manifest, also be given +% new names. Any exceptions to this request are also given in the +% manifest. +% +% We recommend the manifest be given in a separate file named +% MANIFEST-.txt, where is some unique identification +% of the font family. If a separate "readme" file accompanies the Work, +% we recommend a name of the form README-.txt. +% +% The latest version of the LaTeX Project Public License is in +% http://www.latex-project.org/lppl.txt and version 1.3c or later +% is part of all distributions of LaTeX version 2006/05/20 or later. + diff --git a/ventures/slyng/fonts/MANIFEST-SLYNG.txt b/ventures/slyng/fonts/MANIFEST-SLYNG.txt new file mode 100644 index 0000000..bc055f3 --- /dev/null +++ b/ventures/slyng/fonts/MANIFEST-SLYNG.txt @@ -0,0 +1,13 @@ +The fonts in this folder are derived works of the TeX Gyre fonts by the GUST e-foundry, distributed under the +GUST Font License (GUST-FONT-LICENSE.txt in this folder; it refers to the LaTeX Project Public License 1.3c, +https://www.latex-project.org/lppl.txt). + +As the license requests, the derived fonts and their files have new names: + + slyng-condensed-bold.woff "Slyng Condensed Bold" from TeX Gyre Heros Cn Bold (texgyreheroscn-bold.otf) + slyng-text-regular.woff "Slyng Text Regular" from TeX Gyre Adventor Regular (texgyreadventor-regular.otf) + slyng-text-bold.woff "Slyng Text Bold" from TeX Gyre Adventor Bold (texgyreadventor-bold.otf) + +What changed: the glyph set was cut down to Latin-1 plus common punctuation, the OpenType features to kern, liga, +tnum, lnum and pnum, the name table to the names above, and the files were converted to WOFF. The outlines are +unchanged. Made for SLYNG (https://slyng.yolopreneur.lol/) in October 2026. diff --git a/ventures/slyng/skill.md b/ventures/slyng/skill.md new file mode 100644 index 0000000..dd62ac6 --- /dev/null +++ b/ventures/slyng/skill.md @@ -0,0 +1,67 @@ +--- +name: slyng +description: Sling sats and private notes to someone on nostr, the way SLYNG does. Either make a SLYNG link that a person opens, checks and lets go of, or, with your own nostr key and wallet, send the zap and the note yourself. +--- + +# SLYNG, for agents + +SLYNG (https://slyng.yolopreneur.lol/) is a slingshot for nostr. A person aims at someone they follow, pulls back +for more sats, and lets go: the sats go as a zap (NIP-57) from their own wallet, and the note goes as a private, +gift-wrapped message (NIP-17). An agent can aim. Only a person lets go. + +## 1. Make a link (the usual way) + +Give the person a link that opens SLYNG aimed at someone, with the note tied on and the amount marked on the band: + +``` +https://slyng.yolopreneur.lol/#to=&sats=¬e= +``` + +- `to`: an `npub1…`, an `nprofile1…`, a 64-character hex public key, or a NIP-05 name such as `ben@example.com`. +- `sats`: a whole number from 1 to 21,000. The band has rings at 21, 100, 210, 1,000, 2,100, 5,000, 10,000 and + 21,000 sats; SLYNG marks the ring closest to what you ask and says so if it isn't exact. Leave it out for a note on + its own. +- `note`: the words, URL-encoded, up to 1,000 characters. Leave it out to send sats on their own. + +Example, for 2,100 sats and a thank-you to ben@example.com: + +``` +https://slyng.yolopreneur.lol/#to=ben%40example.com&sats=2100¬e=thanks%20for%20the%20records +``` + +Nothing is sent when the link opens. The person reads it, pulls back to the marked ring and lets go, or doesn't. +Tell them what the link does in one line when you hand it over. + +## 2. Sling by yourself (when you have your own key and wallet) + +Only when the person you work for asked you to send sats, and only the amount they named. + +1. **Find them.** Fetch the recipient's kind 0 (profile: `lud16` or `lud06`), kind 10050 (relays for private + messages) and kind 10002 (relay list) from a few relays: wss://relay.damus.io, wss://nos.lol, + wss://relay.primal.net. Verify every event's id and signature before you use it; a forged profile with someone + else's lightning address is how sats get stolen. +2. **Get an invoice (NIP-57).** `lud16` `name@domain` means `GET https://domain/.well-known/lnurlp/name`. If the answer + has `allowsNostr: true` and a `nostrPubkey`, sign a kind 9734 zap request with tags + `["relays", …]`, `["amount", ""]`, `["lnurl", ""]`, `["p", ""]` + and `["client", "slyng"]`, then `GET ?amount=&nostr=&lnurl=`. + Without `allowsNostr`, call the callback with `amount` alone (a plain lightning payment, no receipt). +3. **Check the invoice.** The amount in the invoice's prefix must equal what you asked for (`lnbc2100n1…` is + 210,000 millisats, 210 sats). If it doesn't, pay nothing. +4. **Pay** from your own wallet, for example with Nostr Wallet Connect (NIP-47 `pay_invoice`). +5. **Send the note (NIP-17).** Make a kind 14 message (not signed) with a `["p", ""]` tag. Seal it: a kind 13 + event, signed by you, whose content is the message encrypted to them with NIP-44. Wrap it: a kind 1059 event, + signed by a one-time key, whose content is the seal encrypted to them with NIP-44, with a `["p", ""]` tag. + Give both the seal and the wrap a `created_at` up to two days in the past. Publish the wrap to their kind 10050 + relays, and a second wrap addressed to yourself to your own. +6. **Watch for the catch.** Their lightning address server publishes a kind 9735 receipt that tags them (`p`) and you + (`P`); its `description` is your zap request. + +House rules: a sling lands whole or not at all, so pay first and send the note only if the payment went through. +Zaps are public (anyone can see who sent how much to whom); notes are not. Never put anything private in a zap +request's content. + +## Files + +- https://slyng.yolopreneur.lol/ : the slingshot +- https://slyng.yolopreneur.lol/skill.md : this file +- https://slyng.yolopreneur.lol/llms.txt : the list, for agents diff --git a/ventures/slyng/tools/build.sh b/ventures/slyng/tools/build.sh new file mode 100644 index 0000000..e9d4ec7 --- /dev/null +++ b/ventures/slyng/tools/build.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# tools/build.sh: SLYNG's made files, made in place: the tests, then shots/desktop.png and shots/og.png. +# +# Time's refresh workflow runs this on the runner (teahubxyz/time, Actions -> refresh, slug slyng); it runs anywhere +# else too. Needs node 22+, curl, and Chrome or Chromium: set CHROME=/path/to/chrome if it isn't found, and +# CHROME_FLAGS=--no-sandbox when running as root. The fuller check is tests/smoke.py (Playwright), run by hand. +set -eu +cd "$(dirname "$0")/.." + +node --test tests/*.test.js + +CHROME=${CHROME:-} +for c in "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" "$(command -v chromium || true)" \ + "$(command -v chromium-browser || true)" "$(command -v google-chrome || true)" "$(command -v google-chrome-stable || true)"; do + if [ -z "$CHROME" ] && [ -n "$c" ] && [ -x "$c" ]; then CHROME=$c; fi +done +[ -n "$CHROME" ] || { echo "no Chrome or Chromium found; set CHROME" >&2; exit 1; } + +PORT=${PORT:-8793} +node tools/serve.mjs "$PORT" . >/dev/null 2>&1 /dev/null || true' EXIT INT TERM +n=0 +until curl -fs -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/"; do + n=$((n + 1)); [ "$n" -lt 40 ] || { echo "the local server on port $PORT never answered" >&2; exit 1; }; sleep 0.5 +done + +shot() { # shot OUT.png [chrome args] URL + out=$1; shift + prof=$(mktemp -d) + rm -f "$out" + # shellcheck disable=SC2086 + "$CHROME" --headless=new --hide-scrollbars --no-first-run --no-default-browser-check --disable-background-networking \ + --disable-component-update --user-data-dir="$prof" --virtual-time-budget=6000 --screenshot="$out" \ + ${CHROME_FLAGS:-} "$@" >"$prof.log" 2>&1 & + pid=$! + n=0 + while kill -0 "$pid" 2>/dev/null && [ ! -s "$out" ]; do # some Chromes write the shot and never exit + n=$((n + 1)); [ "$n" -lt 120 ] || break; sleep 0.5 + done + sleep 1 + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + [ -s "$out" ] || { echo "Chrome made no screenshot for $out:" >&2; tail -5 "$prof.log" >&2; exit 1; } + rm -rf "$prof" "$prof.log" +} +mkdir -p shots +shot "$PWD/shots/desktop.png" --window-size=1440,900 "http://127.0.0.1:$PORT/" +shot "$PWD/shots/og.png" --window-size=1200,630 "http://127.0.0.1:$PORT/tools/og.html" +echo "built: tests passed, shots/desktop.png and shots/og.png" diff --git a/ventures/slyng/tools/bundle.sh b/ventures/slyng/tools/bundle.sh new file mode 100644 index 0000000..6346935 --- /dev/null +++ b/ventures/slyng/tools/bundle.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# tools/bundle.sh [OUT]: OUT/slyng.tgz (slyng/ at the top) and its sha256, for Time's seed workflow +# (teahubxyz/time, Actions -> seed), when the bundle can reach the runner (a Boring blob download link). +set -eu +cd "$(dirname "$0")/.." +OUT=${1:-/tmp/slyng-dist} +export COPYFILE_DISABLE=1 +rm -rf "$OUT/slyng" "$OUT/slyng.tgz" +mkdir -p "$OUT/slyng" +for f in *; do [ "$f" = "$(basename "$OUT")" ] || cp -R "$f" "$OUT/slyng/"; done +find "$OUT/slyng" \( -name .DS_Store -o -name __pycache__ \) -prune -exec rm -rf {} + +tar -czf "$OUT/slyng.tgz" -C "$OUT" slyng +sum=$( (shasum -a 256 "$OUT/slyng.tgz" 2>/dev/null || sha256sum "$OUT/slyng.tgz") | cut -d' ' -f1) +echo "$sum" > "$OUT/slyng.tgz.sha256" +echo "bundle $OUT/slyng.tgz: $(wc -c < "$OUT/slyng.tgz" | tr -d ' ') bytes, sha256 $sum" diff --git a/ventures/slyng/tools/og.html b/ventures/slyng/tools/og.html new file mode 100644 index 0000000..38148e3 --- /dev/null +++ b/ventures/slyng/tools/og.html @@ -0,0 +1,29 @@ + + + + +SLYNG share card + + + +
+ +

SLNG

+

Sling sats and notes on nostr.

+

slyng.yolopreneur.lol

+
+ + diff --git a/ventures/slyng/tools/serve.mjs b/ventures/slyng/tools/serve.mjs new file mode 100644 index 0000000..b508987 --- /dev/null +++ b/ventures/slyng/tools/serve.mjs @@ -0,0 +1,28 @@ +// tools/serve.mjs [port] [folder]: serve the page on 127.0.0.1, for the build's screenshots and for trying it locally. +// Static files only, nothing outside the folder; no dependencies beyond node. + +import { createServer } from 'node:http'; +import { readFile, stat } from 'node:fs/promises'; +import { extname, join, resolve, sep } from 'node:path'; + +const port = Number(process.argv[2] || 8000); +const root = resolve(process.argv[3] || '.'); +const TYPES = { + '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', + '.woff': 'font/woff', '.woff2': 'font/woff2', '.txt': 'text/plain; charset=utf-8', '.md': 'text/markdown; charset=utf-8', +}; + +createServer(async (req, res) => { + try { + let file = resolve(join(root, decodeURIComponent(new URL(req.url, 'http://localhost').pathname))); + if (file !== root && !file.startsWith(root + sep)) throw new Error('outside the folder'); + if ((await stat(file)).isDirectory()) file = join(file, 'index.html'); + const body = await readFile(file); + res.writeHead(200, { 'content-type': TYPES[extname(file)] || 'application/octet-stream', 'cache-control': 'no-store' }); + res.end(req.method === 'HEAD' ? undefined : body); + } catch { + res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }); + res.end('not found\n'); + } +}).listen(port, '127.0.0.1', () => console.log(`serving ${root} at http://127.0.0.1:${port}/`));