A network of signed notes
Nostr is a social network with no company in the middle and no accounts. Everything on it is a short, signed piece of JSON called an event, and relays, servers anyone can run, keep events and pass them on. Because every event is signed, a reader can check who wrote it whichever relay it came from. NostrXT is the xTalk Suite's Nostr library, written in xTalk over CoinXT's signatures and SodiumXT's encryption; its handlers begin with nx.
The examples make keys from phrases, so that every run gives the same answers; a real app calls nxKeyGenerate() for a random one.
You are a key pair
On Nostr your secret key is 64 hex digits that only you hold, and your public key, which comes from it, is who you are to everyone. People share keys as npub names (and back them up as nsec names), which have a typo check built in:
-- for learning only: a real app calls nxKeyGenerate()
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxKeyIsValid(tSecret)
put nxKeyPublic(tSecret) into tPublic
put tPublic
put nxNpubEncode(tPublic) into tNpub
put tNpub
put nxNpubDecode(tNpub) is tPublic
put char 1 to 5 of nxNsecEncode(tSecret)
true 4c03b4839d256423f8601551d16f46d9251d29f294cc79cfd51676542d78267a npub1fspmfquay4jz87rqz4gazm6xmyj3620jjnx8nn74zem9gttcyeaqtkq2jp true nsec1
Events, ids and signatures
An event has a kind (1 is a short note, 0 a profile, 7 a reaction), a time in seconds since 1970, its text, tags and its author. nxEventBuild makes one, and nxEventSign fills in the author, the id (the SHA-256 fingerprint of its exact text) and a Schnorr signature. nxEventVerify says true only when both check out, so treat anything else as fake:
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxEventBuild(1, "Hello, Nostr!", empty, 1700000000) into tEvent
put nxEventSign(tEvent, tSecret, empty) into tEvent
put tEvent["kind"] && tEvent["created_at"]
put the length of tEvent["id"] && "hex digits of id," && the length of tEvent["sig"] && "of signature"
put nxEventVerify(tEvent)
-- someone changes the note on its way
put "Hello! Send me coins." into tEvent["content"]
put nxEventVerify(tEvent)
1 1700000000 64 hex digits of id, 128 of signature true false
The empty third parameter of nxEventSign asks for fresh randomness in the signature, so the same note signed twice has two different signatures, and both check.
On the wire: JSON
To travel, an event becomes JSON. nxEventToJson writes it, nxEventFromJson reads it back into an array, and nxJsonGet takes one value out by its name. Text from the internet could say anything, so verify what you read before you trust it:
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxEventSign(nxEventBuild(1, "Hello, Nostr!", empty, 1700000000), tSecret, empty) into tEvent
put nxEventToJson(tEvent) into tJson
put char 1 to 10 of tJson
put nxJsonGet(tJson, "content")
put nxJsonGet(tJson, "kind")
put nxEventVerify(nxEventFromJson(tJson))
{"id":"4ff
Hello, Nostr!
1
true
Tags, replies and reactions
Tags label an event: "t" for a topic, "p" for a person mentioned, "e" for another event. nxTagsFromText makes them from text, a line each with a tab between the parts, and nxTagFirst and nxTagValues read them back. A conversation is events that point at each other with tags: nxReplyBuild makes a reply with an "e" tag for the post and a "p" tag for its author, and nxReactionBuild a reaction, kind 7, with "+" for a like:
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put "t" & tab & "openxtalk" & return & "t" & tab & "learning" into tTags
put nxEventBuild(1, "Hi #openxtalk", nxTagsFromText(tTags), 1700000000) into tPost
put nxEventSign(tPost, tSecret, empty) into tPost
put nxTagCount(tPost) && "tags"
put nxTagFirst(tPost, "t")
put nxTagValues(tPost, "t")
put nxReplyBuild(tPost, "Me too!", 1700000060) into tReply
put nxTagFirst(tReply, "e") is tPost["id"]
put nxTagFirst(tReply, "p") is nxKeyPublic(tSecret)
put nxReactionBuild(tPost, empty, 1700000090) into tLike
put tLike["kind"] && tLike["content"]
2 tags openxtalk openxtalk learning true true 7 +
A reply and a reaction come back unsigned, ready to sign with nxEventSign as any event.
Profiles and links
Your profile is an event too, of kind 0, with your name and a few words about you; post a new one to change it. nxMetadataBuild makes one, and nxMetadataParse reads it. To share a note, nxNeventEncode packs its id with a relay that has it, and nxUriEncode makes a nostr: link that apps open; nxEntityDecode reads any such name back:
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxEventSign(nxMetadataBuild("Alice", "Learning xTalk", empty, 1700000000), tSecret, empty) into tProfile
put tProfile["kind"]
put nxMetadataParse(tProfile) into tFields
put tFields["name"] & ":" && tFields["about"]
put nxEventSign(nxEventBuild(1, "Hello, Nostr!", empty, 1700000000), tSecret, empty) into tNote
put nxUriEncode(nxNeventEncode(tNote["id"], "wss://relay.example.com", nxKeyPublic(tSecret), 1)) into tLink
put char 1 to 14 of tLink
put nxEntityDecode(tLink) into tFields
put tFields["type"] && tFields["relays"]
put tFields["id"] is tNote["id"]
0 Alice: Learning xTalk nostr:nevent1q nevent wss://relay.example.com true
Private messages
Notes are public. For a private message, NIP-44 locks it so that only one person can read it. nxNip44ConversationKey shows the trick: your secret key with their public key gives the same shared key as their secret key with yours, and it is never sent. nxNip44Encrypt locks a message for them, and nxNip44Decrypt opens it, or gives empty with the wrong keys:
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tAlice
put nxHexEncode(cxSha256(textEncode("bob learns too", "UTF-8"))) into tBob
put nxNip44ConversationKey(tAlice, nxKeyPublic(tBob)) is nxNip44ConversationKey(tBob, nxKeyPublic(tAlice))
-- Alice locks a message for Bob (empty: a fresh random nonce)
put nxNip44Encrypt(tAlice, nxKeyPublic(tBob), "Meet at noon", empty) into tLocked
put tLocked contains "noon"
put nxNip44Decrypt(tBob, nxKeyPublic(tAlice), tLocked)
put nxNip44Decrypt(tAlice, nxKeyPublic(tAlice), tLocked) is empty
true false Meet at noon true
Asking a relay
A relay answers filters: whose events, which kinds, how many. nxFilterBuild makes one from an array, and nxFilterMatches says whether an event fits, as a relay would decide. You talk to a relay in short JSON messages over a websocket: nxClientEvent publishes an event, nxClientReq asks with a filter under a name of your choice, and nxRelayParse reads the relay's answers, such as EOSE, "that is all, for now":
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxKeyPublic(tSecret) into tAsk["authors"]
put "1" into tAsk["kinds"]
put 10 into tAsk["limit"]
put nxFilterBuild(tAsk) into tFilter
put nxEventSign(nxEventBuild(1, "A note", empty, 1700000000), tSecret, empty) into tNote
put nxEventSign(nxReactionBuild(tNote, empty, 1700000060), tSecret, empty) into tLike
put nxFilterMatches(tFilter, tNote)
put nxFilterMatches(tFilter, tLike)
put char 1 to 16 of nxClientReq("feed", tFilter)
put nxClientClose("feed")
put nxRelayParse("[" & quote & "EOSE" & quote & "," & quote & "feed" & quote & "]") into tMessage
put tMessage["type"] && tMessage["subId"]
true
false
["REQ","feed",{"
["CLOSE","feed"]
EOSE feed
The nostr-relay library that comes with NostrXT opens the websockets and sends these for you; Learn NostrXT (Help > Examples) and the Dictionary have the rest, proof of work included.
Try it yourself
Write a function trustedNotes that takes a list of events as JSON, a line each, and returns the text of only those that verify, a line each.
Show a solution
function trustedNotes pLines
local tNotes, tEvent
repeat for each line tJson in pLines
put nxEventFromJson(tJson) into tEvent
if nxEventVerify(tEvent) then
put tEvent["content"] & return after tNotes
end if
end repeat
return line 1 to -1 of tNotes
end trustedNotes
put nxHexEncode(cxSha256(textEncode("alice learns nostr", "UTF-8"))) into tSecret
put nxEventSign(nxEventBuild(1, "first", empty, 1700000000), tSecret, empty) into tOne
put nxEventSign(nxEventBuild(1, "second", empty, 1700000060), tSecret, empty) into tTwo
put "forged" into tTwo["content"]
put nxEventSign(nxEventBuild(1, "third", empty, 1700000120), tSecret, empty) into tThree
put nxEventToJson(tOne) & return & nxEventToJson(tTwo) & return & nxEventToJson(tThree) into tFeed
put trustedNotes(tFeed)
first third
Words in this lesson
nxKeyGeneratenxKeyPublicnxKeyIsValidnxNpubEncodenxNpubDecodenxEventBuildnxEventIdnxEventSignnxEventVerifynxEventToJsonnxEventFromJsonnxJsonGetnxTagsFromTextnxTagFirstnxTagValuesnxReplyBuildnxReactionBuildnxMetadataBuildnxNeventEncodenxUriEncodenxEntityDecodenxNip44EncryptnxNip44DecryptnxFilterBuildnxFilterMatchesnxClientReqnxRelayParseEach one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.