Navigation: Docs index · API how-to · Custom events · API reference
This guide explains how to create Nostr events and tags using nostr-java 2.0. The library uses a single event class (GenericEvent) and a single tag class (GenericTag) for all event kinds — no subclasses, no factories, no registries.
Every Nostr event is a GenericEvent, differentiated by its int kind:
// Text note (kind 1)
GenericEvent note = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(Kinds.TEXT_NOTE)
.content("Hello Nostr!")
.build();
// Metadata (kind 0)
GenericEvent metadata = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(Kinds.SET_METADATA)
.content("{\"name\":\"Alice\",\"about\":\"Nostr user\"}")
.build();
// Any custom kind
GenericEvent custom = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(30078) // any integer
.content("custom content")
.build();Tags are a code string and a list of string parameters — exactly what the Nostr protocol specifies:
// Event reference: ["e", "eventId", "relay", "marker"]
GenericTag.of("e", "abc123", "wss://relay.example.com", "reply")
// Public key reference: ["p", "pubkey"]
GenericTag.of("p", "deadbeef1234...")
// Hashtag: ["t", "nostr"]
GenericTag.of("t", "nostr")
// Any custom tag
GenericTag.of("custom", "value1", "value2")Access tag data positionally:
GenericTag tag = GenericTag.of("e", "abc123", "wss://relay.example.com", "reply");
tag.getCode() // "e"
tag.getParams() // ["abc123", "wss://relay.example.com", "reply"]
tag.getParams().get(0) // "abc123"
tag.toArray() // ["e", "abc123", "wss://relay.example.com", "reply"]Common kind values are available as static int constants. Any integer is a valid kind — these are convenience constants for discoverability:
Kinds.SET_METADATA // 0
Kinds.TEXT_NOTE // 1
Kinds.CONTACT_LIST // 3
Kinds.ENCRYPTED_DIRECT_MESSAGE // 4
Kinds.DELETION // 5
Kinds.REPOST // 6
Kinds.REACTION // 7
Kinds.ZAP_REQUEST // 9734
Kinds.ZAP_RECEIPT // 9735
// Range checks
Kinds.isReplaceable(10002) // true (10000-19999)
Kinds.isEphemeral(20001) // true (20000-29999)
Kinds.isAddressable(30023) // true (30000-39999)
Kinds.isValid(65536) // false (must be 0-65535)GenericEvent note = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(Kinds.TEXT_NOTE)
.content("Check out #nostr! cc @someone")
.tags(List.of(
GenericTag.of("t", "nostr"),
GenericTag.of("p", recipientPubKeyHex)
))
.build();
identity.sign(note);GenericEvent reply = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(Kinds.TEXT_NOTE)
.content("Great post!")
.tags(List.of(
GenericTag.of("e", originalEventId, "wss://relay.example.com", "reply"),
GenericTag.of("p", originalAuthorPubKey)
))
.build();
identity.sign(reply);GenericEvent reaction = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(Kinds.REACTION)
.content("+") // or any emoji
.tags(List.of(
GenericTag.of("e", targetEventId),
GenericTag.of("p", targetAuthorPubKey)
))
.build();
identity.sign(reaction);// Contact list (kind 3) — only the latest per pubkey is kept
GenericEvent contactList = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(Kinds.CONTACT_LIST)
.content("")
.tags(List.of(
GenericTag.of("p", friend1PubKey, "wss://relay1.example.com"),
GenericTag.of("p", friend2PubKey, "wss://relay2.example.com")
))
.build();
identity.sign(contactList);// Typing indicator (kind 20001) — relays forward but don't store
GenericEvent typing = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(20001)
.content("{\"typing\":true}")
.build();
identity.sign(typing);// Long-form content (kind 30023) — replaceable by pubkey + d-tag
GenericEvent article = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(30023)
.content("# My Article\n\nFull content here...")
.tags(List.of(
GenericTag.of("d", "my-article-slug"),
GenericTag.of("title", "My Article"),
GenericTag.of("t", "blog")
))
.build();
identity.sign(article);MessageCipher04 cipher = new MessageCipher04(
senderIdentity.getPrivateKey(),
recipientPublicKey
);
String encrypted = cipher.encrypt("Secret message");
String decrypted = cipher.decrypt(encrypted);MessageCipher44 cipher = new MessageCipher44(
senderIdentity.getPrivateKey(),
recipientPublicKey
);
String encrypted = cipher.encrypt("Secret message");
String decrypted = cipher.decrypt(encrypted);Query relays for specific events using EventFilter:
EventFilter filter = EventFilter.builder()
.kinds(List.of(Kinds.TEXT_NOTE, Kinds.REACTION))
.authors(List.of(pubKeyHex))
.since(timestampSeconds)
.limit(50)
.build();
Filters filters = new Filters(filter);Tag-based filtering:
EventFilter filter = EventFilter.builder()
.kinds(List.of(Kinds.TEXT_NOTE))
.addTagFilter("t", List.of("nostr", "bitcoin"))
.addTagFilter("p", List.of(specificPubKey))
.build();@Test
void testEventCreation() {
Identity identity = Identity.generateRandomIdentity();
GenericEvent event = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(Kinds.TEXT_NOTE)
.content("Test content")
.tags(List.of(GenericTag.of("t", "test")))
.build();
identity.sign(event);
assertNotNull(event.getId());
assertNotNull(event.getSignature());
assertEquals(Kinds.TEXT_NOTE, event.getKind());
assertEquals("t", event.getTags().get(0).getCode());
assertEquals("test", event.getTags().get(0).getParams().get(0));
}
@Test
void testSerialization() throws Exception {
GenericEvent event = createAndSignEvent();
String json = new EventMessage(event).encode();
BaseMessage decoded = BaseMessage.read(json);
assertTrue(decoded instanceof EventMessage);
GenericEvent deserialized = ((EventMessage) decoded).getEvent();
assertEquals(event.getId(), deserialized.getId());
}- Custom events how-to — Sending custom event kinds
- Streaming subscriptions — Long-lived relay subscriptions
- API reference — Full class and method reference
- NIP-01 — Basic protocol
- NIP-16 — Event kind ranges