-

@ df67f9a7:2d4fc200
2025-03-13 00:23:46
> For over a year, I have been developing “webs of trust onboarding and discovery” tools for Nostr. With additional funding, I hope to continue this endeavor in 2025. Here’s the story so far…
## What I’m Building
More than simply a “list of follows follows”, “web of trust” implementations will power user discovery, content search, reviews and reccomendations, identity verification and access to all corners of the “trusted” Nostr network as it scales. Without relying on a central “trust authority” to recommend people and content for us, sovereign Nostr users will leverage many forms of “relative trust” derived from our own “web” of natural interactions, “simply” by normalizing and scoring these interactions. The problem is, Nostr doesn’t have an extensible library for performing these “web of trust” calculations and delivering standardized reccomendations to any client … until now.
I have built a developer library by which clients and relays can offer “webs of trust” score calculations for any user. Primarily, I am also building a “social onboarding” client, which will leverage this library to provide “webs of trust” powered recommendations for new users at their crucial “first interaction” touchpoint.
- [Meet Me On Nostr](https://nostrmeet.me) (onboarding client) : This is my first project on Nostr, which I started a year ago with seed funding from [@druid](https://primal.net/druid). This “social onboarding” client will leverage in person relationships, QR invites, and advocate recommendations to improve new user retention. Currently, it creates new accounts with encrypted keys upon scanning any user’s invite. Last summer, without additional funding or a reliable WoT engine to produce recommendations, I “paused” development of this project.
- [GrapeRank Engine](https://github.com/Pretty-Good-Freedom-Tech/graperank-nodejs) (developer library) : Working with [@straycat](https://primal.net/straycat) last fall, I built an open source and extensible library for Nostr developers to integrate “web of trust” powered reccomendations into their products and services. The power of GrapeRank is that it can generate different recommendations for different use cases (not just “web of trust” from “follows and mutes”), configurable easily by developers and by end users. This library is currently in v0.1, “generating and storing usable scores” without NIP standard outputs for Nostr clients.
- [My Grapevine](https://grapevine.my) (algo dashboard) : In addition, I’ve just now wrapped up the demo release of a web client by which users and developers can explore the power of the GrapeRank Engine.
## Potential Impact
Webs of Trust is how Nostr scales. But so far, “web of trust” recommendations on Nostr have been implemented ad-hoc by clients with no consistency and little choice for end users. The “onboarding and discovery” tools I am developing promise to :
- Establish “sovereignty” for webs of trust users, by stimulating a “free market of choices” with open source libraries, enabling any developer to implement WoT powered recommendations with ease.
- Accelerate the isolation of “bots and bad actors”, and improve the “trustiness” of Nostr for everyone else, by streamlining the onboarding of “real world” trusted people directly into established “webs of trust”.
- Improve “discoverability of users and content” across all clients, by providing an extensible algo engine with “unlimited” NIP standard outputs, allowing any client to consume and take advantage of WoT powered recommendations, even as these NIPs are still in flux.
- Pave the way for “global Nostr adoption”, where WoT powered recommendations (and searches) are consistently available for any user across a wide variety of clients.
## Timeline & Milestones
2025 roadmap for “Webs of Trust Onboarding and Discovery” :
- [Meet Me On Nostr](https://nostrmeet.me/) (onboarding client) : MVP release : “scan my QR for instant account and DM with me on Nostr”.
- [GrapeRank Engine ](https://github.com/Pretty-Good-Freedom-Tech/graperank-nodejs)(developer library) : 1.0 release : “output WoT scores to Nostr NIPs and other stuff” for consumption by clients and relays.
- [My Grapevine](https://grapevine.my/) (algo dashboard) : 1.0 release : “usable dashboard with API endpoints” for end users to configure and consume GrapeRank scores on their own clients and relays.
- [Meet Me On Nostr](https://nostrmeet.me/) (onboarding client) : 1.0 release : first integration with My Grapevine, offering “follow and app recommendations for invited users”, customizable per-invite for Nostr advocates.
## Funding
In February 2024, I received a one time donation from [@druid](https://primal.net/druid) to start the “Meet Me On Nostr” client.
In May 2024, I applied for an OpenSats grant to fund further development of “Meet Me On Nostr”. This was denied.
In September 2024, [@straycat](https://primal.net/straycat) offered to fund me for three months, to develop the “GrapeRank Engine” and “My Grapevine” demo client around his algorithm design.
I have a [Geyser Fund page](https://geyser.fund/project/nostrmeetme)
Please reach out via DM if you are interested to fund part of this or any related project.
-

@ ddf03aca:5cb3bbbe
2025-03-12 18:49:00
Welcome to Built with Cashu-TS, a series dedicated to crafting cool applications powered by Cashu and its TypeScript library, Cashu-TS. In this first post, we'll dive into creating a tiny, personal Lightning Address server!
> [!NOTE]
> Quick note: To keep things concise and easy to follow, the examples provided here aren't production-grade code. I'll clearly highlight spots where I've intentionally simplified or taken shortcuts.
## What we are building
Today we are building a Lightning Address server. The server is responsible for returning a Lightning Invoice whenever someone tries to pay your Lightning Address. The exact flow is described in LUD16, but here is a quick rundown:
1. User enters your Lightning Address into their wallet
2. Wallet constructs the matching URL as per LUD16 and sends a GET request
3. Server creates a JSON response with some metadata (min amount, max amount, callback url, etc.) and returns it
4. Wallet displays metadata and upon user interaction sends a second SET request to the callback url including the specified amount.
5. Server fetches an invoice for the requested amount and returns it
Usually the invoices are fetched from a Lightning Node. But today we are using a Cashu mint as our Lightning provider.
## Setup the project
Our Lightning Address server will be written in TypeScript using the express framework. First we got to initialise a new project and install our dependencies.
```sh
mkdir tiny-lud16
cd tiny-lud16
npm init
npm i express cors @cashu/cashu-ts
npm i -D typescript esbuild @types/node @types/cors @types/express
```
### Adding a build script
Because we are using TypeScript we need to add a build step to execute our code (recent versions of node support direct execution of node, but this is the "traditional" way). We are using esbuild to compile our code to JavaScript
> [!NOTE]
> esbuild does not check types. If you want to make sure your code typechecks use `tsc`
**build.js**
```js
#!/usr/bin/env node
const esbuild = require("esbuild");
esbuild
.build({
outdir: "dist/",
format: "cjs",
platform: "node",
entryPoints: ["src/index.ts"],
bundle: true,
sourcemap: "external",
})
.then(() => {
console.log("Server built sucessfully");
});
```
Now we can build our project using `node build.js` and then run our project with `node dist/index.js`
## Configuration
Before we start working on our web server we need to set some options. For this we create `/src/config.ts`
- `USERNAME` will be the address part in front of the `@`.
- `HOSTNAME` is the URL (including the protocol) the server will run on
- `MINT_URL` is the URL of the mint that we want to use to generate invoices and receive token from.
- `MIN_AMOUNT` and `MAX_AMOUNT` are LNURL specific settings that define the range of amounts in mSats that we want to allow.
> [!NOTE]
> Because the smalles amount in the `sat` unit in Cashu is 1 Sat, `MIN_AMOUNT` can not be smaller than 1000
```ts
export const USERNAME = "egge";
export const HOSTNAME = " https://test.test";
export const MINT_URL = " https://mint.minibits.cash/Bitcoin";
export const MIN_AMOUNT = 1000;
export const MAX_AMOUNT = 10000;
```
## Adding some utility
To keep our request handler clean, we will put some of the utility functions in a separate file `src/utils.ts`.
```ts
import { HOSTNAME, MAX_AMOUNT, MIN_AMOUNT, USERNAME } from "./config";
export function createLnurlResponse() {
return {
callback: `${HOSTNAME}/.well-known/lnurlp/${USERNAME}`,
maxSendable: MAX_AMOUNT,
minSendable: MIN_AMOUNT,
metadata: JSON.stringify([
["text/plain", "A cashu lightning address... Neat!"],
]),
tag: "payRequest",
};
}
export function isValidAmount(amountInSats: number) {
return (
amount >= MIN_AMOUNT && amount <= MAX_AMOUNT && Number.isInteger(amount)
);
}
```
The `createLnurlResponse` function creates the response for the first call to our LNURL endpoint. This structure is defined in LUD16 and in our case it does not rely on any state, other than the configuration constants we defined in `src/config.ts`. This object contains the metadata that is the response of step 3 in our flow.
The `isValidAmount` function helps us determine whether the amount we will receive in Step 4 is valid. We check whether it is within the boundaries of our `MIN_AMOUNT` and `MAX_AMOUNT`. Because we will convert the requested amount from mSats into sats, we need to check whether this converted amount is an integer.
## Adding out wallet backend
This blog series is about awesome Cashu use cases, so of course our "Lightning backend" is a mint. We are using the `@cashu/cashu-ts` npm package to streamline Cashu interaction.
```ts
import {
CashuMint,
CashuWallet,
getEncodedToken,
Proof,
} from "@cashu/cashu-ts";
import { MINT_URL } from "./config";
import { resolve } from "path";
import { existsSync, mkdirSync, writeFileSync } from "fs";
const mint = new CashuMint(MINT_URL);
const wallet = new CashuWallet(mint);
export async function createInvoiceAndHandlePayment(amount: number) {
const { quote, request } = await wallet.createMintQuote(amount);
const interval = setInterval(async () => {
const stateRes = await wallet.checkMintQuote(quote);
if (stateRes.state === "PAID") {
const proofs = await wallet.mintProofs(amount, quote);
clearInterval(interval);
const token = turnProofsIntoToken(proofs);
saveTokenLocally(token);
}
}, 10000);
return request;
}
function turnProofsIntoToken(proofs: Proof[]) {
return getEncodedToken({ mint: MINT_URL, proofs });
}
function saveTokenLocally(token: string) {
const tokenDirPath = resolve(__dirname, "../token");
if (!existsSync(tokenDirPath)) {
mkdirSync(tokenDirPath);
}
writeFileSync(resolve(tokenDirPath, `${Date.now()}_token.txt`), token);
}
```
The first thing we do here is instantiating a CashuWallet class from Cashu-TS. This class will take care of the Cashu operations required to create an invoice and mint tokens.
Then we create a utility function that will handle our invoice creation and later make sure to check whether an invoice was paid. `wallet.createMintQuote` will talk to the mint to create a mint quote. The mint returns a `MintQuoteReponse` that includes the ID of the quote as well as the invoice (`request`) that needs to be paid before the Cashu proofs can be minted. This `request` is what we will return to the payer later. Once the mint quote is created we will start polling the mint for it's payment state using `wallet.checkMintQuote`. As soon as the state changes to `"PAID"` we know that the payment was done and we can mint the proofs using Cashu-TS' `mintProofs` method. This returns some Cashu proofs that we will serialize into a Cashu Token and save to our disk using the `saveTokenLocally` function.
> [!NOTE]
> In this example we use `setInterval` to poll for a payment update. In the real world you would use a proper request queue for this to make sure we do not spam the mint with too many requests at the same time
> Also saving the token to disk is not ideal. You could instead send yourself a nostr DM or post it to a webhook
## Adding the handler
Because our LNURL endpoint and our callback endpoint are the same, we only need a single route handler. This route handler will take care of any GET request coming in at `/.well-known/lnurlp/USERNAME`. Wether it is a callback or not can be determined by checking the `amount` query parameter.
```ts
import { NextFunction, Request, Response } from "express";
import { createLnurlResponse, isValidAmount } from "./utils";
import { createInvoiceAndHandlePayment } from "./wallet";
export const lud16Controller = async (
req: Request<unknown, unknown, unknown, { amount: string }>,
res: Response,
next: NextFunction,
) => {
try {
if (!req.query.amount) {
res.json(createLnurlResponse());
return;
}
const parsedAmount = parseInt(req.query.amount);
const mintAmount = parsedAmount / 1000;
const isValid = isValidAmount(mintAmount);
if (!isValid) {
throw new Error("Invalid Amount");
}
const invoice = await createInvoiceAndHandlePayment(mintAmount);
res.json({
pr: invoice,
routes: [],
});
} catch (e) {
next(e);
}
};
```
Let's take this handler function apart and see hat is happening here.
First we check whether the `amount` query parameter is present. If it is not, we now that we are currently in step 3 of our LNURL flow. In this case all we need to do is create the expected metadata object using our `createLnurlResponse` utility and return it to the caller.
If the parameter is present we are in step 5 of our flow and the real work begins. As mentioned above we need to first convert the amount, which is in mSats as per LUD16 into sats to be compatible with our mint running the `sat` unit. Because query parameters are always `string`, we use the built-in `parseInt` to parse the string into a `number`. We then check whether the amount is valid using our `isValidAmount` utility. If it is not, we throw an error which will get caught and passed to express' built in error middleware.
> [!NOTE]
> The error returned by the express middleware is a basic error page without proper error codes. Usually you would define error classed and a custom middleware to take care of this.
Once we made sure that the amount is valid the Cashu logic takes place. We pass the amount to `createInvoiceAndHandlePayment` to create an invoice and start the state polling behind the scenes. At the end of the function we simply return the mint's invoice in a JSON reponse as per LUD16.
## Adding the route
The last step of the process is to add our route handler to the right path of our web server. This path is defined in LUD16: `<domain>/.well-known/lnurlp/<username>`. We create our web server and add the route handler in `/src/index.ts`.
```ts
import express from "express";
import { USERNAME } from "./config";
import { lud16Controller } from "./controller";
const app = express();
app.get("/.well-known/lnurlp/" + USERNAME, lud16Controller);
app.listen(8080, () => {
console.log("Server running on port 8080");
});
```
This snippet is very straight forward. We create an express app, add the route handler to handle GET requests at our desired path and then tell the server to listen on port 8080.
## Conclusion
With just a few lines of code and without using our own Lightning backend we have built a working LNURL Lightning Address server. This is one of the features I love so much about Cashu: It enables new Lightning and Bitcoin use cases. I hope you enjoyed this first part of the new series. Please make sure to leave your feedback 💜🥜
-

@ 04c915da:3dfbecc9
2025-03-12 15:30:46
Recently we have seen a wave of high profile X accounts hacked. These attacks have exposed the fragility of the status quo security model used by modern social media platforms like X. Many users have asked if nostr fixes this, so lets dive in. How do these types of attacks translate into the world of nostr apps? For clarity, I will use X’s security model as representative of most big tech social platforms and compare it to nostr.
**The Status Quo**
On X, you never have full control of your account. Ultimately to use it requires permission from the company. They can suspend your account or limit your distribution. Theoretically they can even post from your account at will. An X account is tied to an email and password. Users can also opt into two factor authentication, which adds an extra layer of protection, a login code generated by an app. In theory, this setup works well, but it places a heavy burden on users. You need to create a strong, unique password and safeguard it. You also need to ensure your email account and phone number remain secure, as attackers can exploit these to reset your credentials and take over your account. Even if you do everything responsibly, there is another weak link in X infrastructure itself. The platform’s infrastructure allows accounts to be reset through its backend. This could happen maliciously by an employee or through an external attacker who compromises X’s backend. When an account is compromised, the legitimate user often gets locked out, unable to post or regain control without contacting X’s support team. That process can be slow, frustrating, and sometimes fruitless if support denies the request or cannot verify your identity. Often times support will require users to provide identification info in order to regain access, which represents a privacy risk. The centralized nature of X means you are ultimately at the mercy of the company’s systems and staff.
**Nostr Requires Responsibility**
Nostr flips this model radically. Users do not need permission from a company to access their account, they can generate as many accounts as they want, and cannot be easily censored. The key tradeoff here is that users have to take complete responsibility for their security. Instead of relying on a username, password, and corporate servers, nostr uses a private key as the sole credential for your account. Users generate this key and it is their responsibility to keep it safe. As long as you have your key, you can post. If someone else gets it, they can post too. It is that simple. This design has strong implications. Unlike X, there is no backend reset option. If your key is compromised or lost, there is no customer support to call. In a compromise scenario, both you and the attacker can post from the account simultaneously. Neither can lock the other out, since nostr relays simply accept whatever is signed with a valid key.
The benefit? No reliance on proprietary corporate infrastructure.. The negative? Security rests entirely on how well you protect your key.
**Future Nostr Security Improvements**
For many users, nostr’s standard security model, storing a private key on a phone with an encrypted cloud backup, will likely be sufficient. It is simple and reasonably secure. That said, nostr’s strength lies in its flexibility as an open protocol. Users will be able to choose between a range of security models, balancing convenience and protection based on need.
One promising option is a web of trust model for key rotation. Imagine pre-selecting a group of trusted friends. If your account is compromised, these people could collectively sign an event announcing the compromise to the network and designate a new key as your legitimate one. Apps could handle this process seamlessly in the background, notifying followers of the switch without much user interaction. This could become a popular choice for average users, but it is not without tradeoffs. It requires trust in your chosen web of trust, which might not suit power users or large organizations. It also has the issue that some apps may not recognize the key rotation properly and followers might get confused about which account is “real.”
For those needing higher security, there is the option of multisig using FROST (Flexible Round-Optimized Schnorr Threshold). In this setup, multiple keys must sign off on every action, including posting and updating a profile. A hacker with just one key could not do anything. This is likely overkill for most users due to complexity and inconvenience, but it could be a game changer for large organizations, companies, and governments. Imagine the White House nostr account requiring signatures from multiple people before a post goes live, that would be much more secure than the status quo big tech model.
Another option are hardware signers, similar to bitcoin hardware wallets. Private keys are kept on secure, offline devices, separate from the internet connected phone or computer you use to broadcast events. This drastically reduces the risk of remote hacks, as private keys never touches the internet. It can be used in combination with multisig setups for extra protection. This setup is much less convenient and probably overkill for most but could be ideal for governments, companies, or other high profile accounts.
---
Nostr’s security model is not perfect but is robust and versatile. Ultimately users are in control and security is their responsibility. Apps will give users multiple options to choose from and users will choose what best fits their need.