Title: Angor Nostr Communication Protocol
Type: Protocol
Status: Draft
Authors: Dan Gershony, David Gershony
Created: 2025-06-09
This document specifies the Nostr event kinds, tags, and message formats used by the Angor P2P Funding Protocol (P2P Funding Protocol) for decentralized communication between founders and investors. The protocol uses Nostr for project discovery, metadata publication, and the interactive investment handshake. It defines a custom event kind 3030 for project creation, leverages NIP-01 profiles and NIP-78 application-specific data for project metadata, and uses NIP-44 encrypted direct messages for the investment signature exchange.
The Angor protocol requires a decentralized communication layer for founders and investors to coordinate without relying on centralized servers. Nostr provides a censorship-resistant relay network suitable for publishing project metadata and exchanging investment messages. This document formalizes the Nostr event structures to ensure interoperability between different Angor client implementations.
- founder : creator of the funding project.
- investor : participant who locks bitcoin into a project's time-locked stages.
- seeder (lead investor) : an early investor who commits a secret, enabling penalty-free recovery for regular investors.
- projectIdentifier : the unique on-chain identifier for a project (e.g.,
angor1q...).
The protocol uses the following Nostr event kinds:
| Kind | Standard | Usage |
|---|---|---|
| 0 | NIP-01 Metadata | Project profile identity (name, description, avatar) |
| 4 | NIP-04/NIP-44 Encrypted DM | Investment messaging (offers, approvals, notifications) |
| 3030 | Custom (this spec) | Project creation event |
| 10002 | NIP-65 Relay List Metadata | Relay list for the project identity |
| 30078 | NIP-78 Application-Specific Data | Project content, FAQ, members, media |
The founder publishes a kind 3030 event to announce a new project on the Nostr network. The event ID is then committed on-chain in the founder's initialization transaction via OP_RETURN, binding the Nostr metadata to the blockchain.
This is an addressable (replaceable) event, keyed by the d tag.
Tags:
[
["d", "<projectIdentifier>"]
]Content:
The content is a JSON-serialized ProjectInfo object. Property names use camelCase. Dates are serialized as Unix timestamps (seconds). Null values are omitted.
{
"founderKey": "<string, Bitcoin compressed public key of the founder (hex)>",
"founderRecoveryKey": "<string, Bitcoin compressed public key for recovery multisig (hex)>",
"projectIdentifier": "<string, unique project identifier (bech32, angor1...)>",
"nostrPubKey": "<string, Nostr public key for the project (hex)>",
"startDate": "<int, Unix timestamp of campaign start>",
"penaltyDays": "<int, number of days funds are locked during penalty recovery>",
"expiryDate": "<int, Unix timestamp when remaining funds can be reclaimed>",
"targetAmount": "<int, target funding amount in satoshis>",
"stages": [
{
"amountToRelease": "<decimal, percentage of total to release at this stage>",
"releaseDate": "<int, Unix timestamp of stage release>"
}
],
"projectSeeders": {
"threshold": "<int, number of seeder secrets required for penalty-free recovery>",
"secretHashes": ["<string, hex-encoded hash of seeder secret>"]
}
}Version 2 adds support for multiple project types, dynamic stages, and penalty thresholds. All version 1 fields remain, with the following additions:
{
"version": 2,
"projectType": "<int, 0=Invest, 1=Fund, 2=Subscribe>",
"founderKey": "<string, Bitcoin compressed public key of the founder (hex)>",
"founderRecoveryKey": "<string, Bitcoin compressed public key for recovery multisig (hex)>",
"projectIdentifier": "<string, unique project identifier (bech32, angor1...)>",
"nostrPubKey": "<string, Nostr public key for the project (hex)>",
"networkName": "<string, blockchain network name (e.g. 'Bitcoin', 'BitcoinTestnet', 'angornet')>",
"startDate": "<int, Unix timestamp of campaign start (required for Invest, optional otherwise)>",
"endDate": "<int, Unix timestamp of campaign end (required for Invest, unused otherwise)>",
"penaltyDays": "<int, penalty lock days (Invest/Fund) or 0 (Subscribe)>",
"expiryDate": "<int, Unix timestamp when remaining funds can be reclaimed>",
"targetAmount": "<int, target amount in satoshis (required for Invest, optional otherwise)>",
"penaltyThreshold": "<int|null, optional threshold in satoshis (Fund only)>",
"stages": [
{
"amountToRelease": "<decimal, percentage>",
"releaseDate": "<int, Unix timestamp>"
}
],
"projectSeeders": {
"threshold": "<int>",
"secretHashes": ["<string>"]
},
"dynamicStagePatterns": [
{
"patternId": "<int, 0-255, unique pattern identifier>",
"name": "<string, display name>",
"description": "<string, description>",
"frequency": "<int, 0=Weekly, 1=Biweekly, 2=Monthly, 3=BiMonthly, 4=Quarterly>",
"stageCount": "<int, number of stages>",
"payoutDayType": "<int, 0=FromStartDate, 1=SpecificDayOfMonth, 2=SpecificDayOfWeek>",
"payoutDay": "<int, day value (context depends on payoutDayType)>",
"amount": "<int|null, fixed amount in satoshis (mandatory for Subscribe, optional for Fund)>"
}
]
}Field requirements by project type:
| Field | Invest | Fund | Subscribe |
|---|---|---|---|
version |
Required (2) | Required (2) | Required (2) |
projectType |
0 | 1 | 2 |
startDate |
Required | Optional | Optional |
endDate |
Required | Omitted | Omitted |
targetAmount |
Required (> 0) | Optional (0 if unused) | Optional (0 if unused) |
penaltyDays |
Required (10-365) | Required (10-365) | 0 |
penaltyThreshold |
Omitted | Optional | Omitted |
stages |
Required (fixed list) | Empty list | Empty list |
dynamicStagePatterns |
Empty list | Required (at least one) | Required (at least one, each with amount set) |
Version detection:
- If
versionis absent or< 2, the event is treated as version 1 and the project type is alwaysInvest. - If
version >= 2, theprojectTypefield determines the project type.
{
"version": 2,
"projectType": 0,
"founderKey": "03309b10a078ca8e718693d241b3a57ff31f5aabcd7ec53089bd143a57036332ea",
"founderRecoveryKey": "03cc053e8fd5bd6cea509df6c58d0f6fe16d9f4bed20a7b15a9447dbd9d6a52d9a",
"projectIdentifier": "angor1q9j9jvmqwll00gnzf8thu9lrar65ccpu4z5np6j",
"nostrPubKey": "5a05cc7a38e3875ee3242e5f068304a36c9609c4c15f5baaf7d75e8fcdfe36c5",
"networkName": "Bitcoin",
"startDate": 1738886400,
"endDate": 1749254400,
"penaltyDays": 90,
"expiryDate": 1780790400,
"targetAmount": 5000000000,
"stages": [
{"amountToRelease": 25, "releaseDate": 1738886400},
{"amountToRelease": 25, "releaseDate": 1741564800},
{"amountToRelease": 25, "releaseDate": 1744156800},
{"amountToRelease": 25, "releaseDate": 1746835200}
],
"projectSeeders": {"threshold": 0, "secretHashes": []}
}{
"version": 2,
"projectType": 1,
"founderKey": "03309b10a078ca8e718693d241b3a57ff31f5aabcd7ec53089bd143a57036332ea",
"founderRecoveryKey": "03cc053e8fd5bd6cea509df6c58d0f6fe16d9f4bed20a7b15a9447dbd9d6a52d9a",
"projectIdentifier": "angor1q9j9jvmqwll00gnzf8thu9lrar65ccpu4z5np6j",
"nostrPubKey": "5a05cc7a38e3875ee3242e5f068304a36c9609c4c15f5baaf7d75e8fcdfe36c5",
"networkName": "Bitcoin",
"penaltyDays": 60,
"expiryDate": 1798761600,
"targetAmount": 0,
"penaltyThreshold": 1000000,
"stages": [],
"projectSeeders": {"threshold": 3, "secretHashes": ["a1b2c3...","d4e5f6...","789abc..."]},
"dynamicStagePatterns": [
{
"patternId": 0,
"name": "6-Month Monthly",
"description": "6 monthly payments on the 1st of each month",
"frequency": 2,
"stageCount": 6,
"payoutDayType": 1,
"payoutDay": 1
},
{
"patternId": 1,
"name": "12-Month Monthly",
"description": "12 monthly payments on the 1st of each month",
"frequency": 2,
"stageCount": 12,
"payoutDayType": 1,
"payoutDay": 1
}
]
}{
"version": 2,
"projectType": 2,
"founderKey": "03309b10a078ca8e718693d241b3a57ff31f5aabcd7ec53089bd143a57036332ea",
"founderRecoveryKey": "03cc053e8fd5bd6cea509df6c58d0f6fe16d9f4bed20a7b15a9447dbd9d6a52d9a",
"projectIdentifier": "angor1q9j9jvmqwll00gnzf8thu9lrar65ccpu4z5np6j",
"nostrPubKey": "5a05cc7a38e3875ee3242e5f068304a36c9609c4c15f5baaf7d75e8fcdfe36c5",
"networkName": "Bitcoin",
"penaltyDays": 0,
"expiryDate": 1798761600,
"targetAmount": 0,
"stages": [],
"projectSeeders": {"threshold": 0, "secretHashes": []},
"dynamicStagePatterns": [
{
"patternId": 0,
"name": "6-Month Monthly",
"description": "6 monthly payments on the 1st of each month",
"frequency": 2,
"stageCount": 6,
"payoutDayType": 1,
"payoutDay": 1,
"amount": 500000
}
]
}Project metadata beyond the core ProjectInfo is stored in NIP-78 application-specific data events (kind: 30078). Each metadata category is a separate replaceable event, keyed by the d tag. These events are published under the project's Nostr identity (derived from the founder key via P2P Funding Protocol key derivation).
| d-tag | Content format | Description |
|---|---|---|
angor:project |
Plain text string | Project description/about content |
angor:faq |
JSON array | FAQ items |
angor:members |
JSON object | Team member pubkeys |
angor:media |
JSON array | Media gallery items |
Tags (all metadata events):
[
["d", "<d-tag value>"]
]Free-text project description. May contain HTML or markdown.
This is a detailed description of the project goals, roadmap, and team.
[
{"question": "What is this project?", "answer": "A decentralized..."},
{"question": "How do I invest?", "answer": "Use the Angor app..."}
]{
"pubkeys": [
"npub1abc...",
"npub1def..."
]
}[
{"url": "https://example.com/image1.png", "type": "image"},
{"url": "https://example.com/video1.mp4", "type": "video"}
]The project's Nostr identity publishes a standard NIP-01 kind: 0 profile metadata event containing:
{
"name": "<string, project name>",
"display_name": "<string, display name>",
"about": "<string, short project description>",
"website": "<string, project URL>",
"picture": "<string, avatar URL>",
"banner": "<string, banner image URL>",
"nip05": "<string, NIP-05 identifier>",
"lud16": "<string, Lightning address>"
}The project identity publishes a kind: 10002 event listing preferred relays, following NIP-65:
{
"tags": [
["r", "wss://relay1.example.com"],
["r", "wss://relay2.example.com"]
]
}Campaign browser software should be entirely client-side, either as a stand-alone app or as a purely frontend webpage.
To discover projects, the browser should:
- Query relays for
kind: 3030events to find published projects. - Use the
nostrPubKeyfield in the event content to query forkind: 0(NIP-01 profile) andkind: 30078(NIP-78 metadata) events authored by that pubkey. - Query a blockchain indexer to find the founder's initialization transaction that commits to the Nostr event ID via
OP_RETURN. - Verify the on-chain commitment matches the Nostr event to ensure metadata integrity.
These event kinds can be fetched in a single relay subscription:
{
"kinds": [3030, 30078, 0],
"authors": ["<nostrPubKey>"]
}All investment communication between founders and investors uses Nostr encrypted direct messages. The subject tag identifies the message type.
Encryption:
- Messages SHOULD be encrypted with NIP-44 (ECDH + HKDF + ChaCha20-Poly1305).
- Implementations SHOULD support decrypting NIP-04 (AES-CBC) messages for backward compatibility. NIP-04 ciphertext is detected by the presence of
?iv=in the encrypted payload. - Cancellation notifications MAY be sent unencrypted as they contain only public identifiers.
Tags:
All DM events include:
[
["p", "<recipient pubkey (hex)>"],
["subject", "<message type>"]
]Reply messages (founder to investor) additionally include an e tag referencing the original request:
[
["p", "<recipient pubkey (hex)>"],
["e", "<original request event ID>"],
["subject", "<message type>"]
]The investor sends the unsigned investment transaction to the founder, requesting recovery signatures.
Content (NIP-44 encrypted):
{
"projectIdentifier": "<string, project identifier>",
"investmentTransactionHex": "<string, hex-encoded unsigned investment transaction>"
}The founder returns recovery signatures for the investment transaction.
Tags include e tag referencing the original Investment offer event.
Content (NIP-44 encrypted):
{
"projectIdentifier": "<string, project identifier>",
"signatures": [
{"stageIndex": 0, "signature": "<string, hex-encoded Schnorr signature>"},
{"stageIndex": 1, "signature": "<string, hex-encoded Schnorr signature>"}
],
"timeOfSignatureRequest": "<int, Unix timestamp>",
"signatureRequestEventId": "<string, event ID of the original request>",
"signatureType": 1
}Where signatureType: 0 = Release, 1 = Recovery.
Notification that the investment transaction has been broadcast on-chain.
Content (NIP-44 encrypted):
{
"projectIdentifier": "<string, project identifier>",
"transactionId": "<string, hex-encoded Bitcoin transaction ID>"
}Notification that the investor has canceled the investment request.
Content (unencrypted or NIP-44 encrypted):
{
"projectIdentifier": "<string, project identifier>",
"requestEventId": "<string, event ID of the original request>"
}The founder sends release signatures for unfunded coin release (when an Invest project does not meet its target).
Tags include e tag referencing the original investment request event.
Content (NIP-44 encrypted):
{
"projectIdentifier": "<string, project identifier>",
"signatures": [
{"stageIndex": 0, "signature": "<string, hex-encoded Schnorr signature>"},
{"stageIndex": 1, "signature": "<string, hex-encoded Schnorr signature>"}
],
"timeOfSignatureRequest": "<int, Unix timestamp>",
"signatureRequestEventId": "<string, event ID of the original request>",
"signatureType": 0
}| Subject tag | Direction | Content | Encrypted |
|---|---|---|---|
"Investment offer" |
Investor --> Founder | Investment transaction hex | NIP-44 |
"Re:Investment offer" |
Founder --> Investor | Recovery signatures | NIP-44 |
"Investment completed" |
Investor --> Founder | Transaction ID | NIP-44 |
"Investment canceled" |
Investor --> Founder | Request event ID | Optional |
"Release transaction signatures" |
Founder --> Investor | Release signatures | NIP-44 |
To retrieve investment messages for a founder, clients use these filters:
Incoming messages (to founder):
{
"kinds": [4],
"#p": ["<founder nostr pubkey>"],
"since": "<timestamp>"
}Outgoing messages (from founder):
{
"kinds": [4],
"authors": ["<founder nostr pubkey>"],
"since": "<timestamp>"
}Specific signature response (investor waiting for founder approval):
{
"kinds": [4],
"authors": ["<project nostr pubkey>"],
"#p": ["<investor nostr pubkey>"],
"#e": ["<request event ID>"],
"since": "<request sent timestamp>",
"limit": 1
}Message type dispatch is done client-side by inspecting the subject tag value.
For Fund projects with a penalty threshold, when an investment amount is at or below the threshold, the investor does not need recovery signatures. In this case the investor skips the Investment offer / Re:Investment offer exchange and directly broadcasts the transaction, then sends only an Investment completed notification.
See P2P Funding Protocol for the full penalty threshold specification.