A complete implementation of NRPC (Nostr Remote Procedure Call) server with support for reminders, direct messaging, scheduled posts, and ecash giveaways.```
npm install
NRPC Protocol - Implements kinds 22068 (request) and 22069 (response)Setup a sample .env:
Encrypted Communication - Supports NIP-17 gift wrapping and NIP-04 fallback```
Multiple Controllers - Modular design with specialized controllersexport NSEC=""
Scheduling System - Built-in scheduler for reminders and postsexport SERVICE_NAME="FORMSTR NRPC BACKEND"
Ecash Integration - Cashu wallet for token giveawaysexport SERVICE_ABOUT="Example NRPC backend by FORMSTR"
Method Introspection - Auto-discovery via
getMethodsexport SERVICE_PICTURE=""SQLite Persistence - Reliable data storageexport SERVICE_BANNER=""
Daemon Mode - Run as background service```
source .env
Installation```
Running the Serverstart the app:
NRPC Protocolnode dist/app.js
Development# Nostr RPC-over-Events
```bash- author: caller pubkey
git clone https://github.com/psam21/nrpc_workshop.git - ["p", "<callee_pubkey>"]
cd nrpc_workshop - ["method", "<method_name>"]
["param", "<key>", "<value>"](repeatable)
npm install## Response Event (kind: 22069)
- `author`: callee pubkey
## ⚙️ Configuration- `tags`:
- `["e", "<request_event_id>"]`
Create a `.env` file in the root directory: - `["p", "<caller_pubkey>"]`
- `["status", "<http_status_code>"]`
```bash - On success:
# Required: Your Nostr private key - `["result", "<key>", "<value>"]` (repeatable)
export NSEC="nsec1..." - `["result_json", "<json_string>"]` (optional)
- On error:
# Optional: Your public key (auto-derived if not provided) - `["error", "<http_status_code>", "<message>"]`
export PUBKEY="your_hex_pubkey"
## Examples
# Service Information
export SERVICE_NAME="My NRPC Server"### Request
export SERVICE_ABOUT="Remote Procedure Call server over Nostr"
export SERVICE_PICTURE="https://example.com/logo.png"```json
export SERVICE_BANNER="https://example.com/banner.png"{
"kind": 22068,
# Nostr Relays (comma-separated) "author": "npub1caller...",
export RELAYS="wss://relay.damus.io,wss://relay.snort.social,wss://relay.primal.net,wss://relay.nostr.band" "tags": [
["p", "npub1service..."],
# Optional: Ecash Giveaway (requires mint URL) ["method", "createReminder"],
export MINT_URL="https://mint.minibits.cash/Bitcoin" ["param", "time", "2025-09-17T09:00:00Z"],
["param", "text", "Doctor appointment"],
# Optional: Lightning & Social ["param", "notify", "true"]
export LUD16="you@getalby.com" ],
export WEBSITE="https://yourwebsite.com" "content": ""
export NIP05="you@yourdomain.com"}
# Server Port (default: 3000)Success Response
export PORT="3000"
```{
"kind": 22069,
### Setting Up Ecash Giveaway (Optional) "author": "npub1service...",
"tags": [
If you want to enable the giveaway feature: ["e", "id_of_request_event"],
["p", "npub1caller..."],
1. **Set MINT_URL** in your `.env` ["status", "200"],
2. **Get an ecash token** from a Cashu mint ["result", "reminder_id", "rem123"],
3. **Add token to environment**: ["result", "scheduled_at", "2025-09-17T09:00:00Z"]
```bash ],
export ECASH_TOKEN="cashuA..." "content": ""
```}
4. **Seed the wallet**:
```bashError Response
npm run seed
```{
"kind": 22069,
## 🏃 Running the Server "author": "npub1service...",
"tags": [
### Development Mode ["e", "id_of_request_event"],
["p", "npub1caller..."],
```bash ["status", "400"],
# Load environment variables ["error", "400", "invalid time format"]
source .env ],
"content": ""
# Build TypeScript}
npm run build```
# Start server
npm start
npm run devnpm start -- -dThis will:
- Run the server in background
- Create
nrpc.pidwith process ID - Log to
output.log
To stop the daemon:
pkill -f "node dist/app.js"Schedule a reminder to be sent via DM at a specific time.
Parameters:
Time(required): Time in format "HH:MM"Text(required): Reminder messageDate(required): Date in format "YYYY-MM-DD"
Returns:
reminder_id: Unique identifierscheduled_at: ISO timestamptext: Reminder messageowner: Creator's pubkey
Errors:
400: Invalid time/date format or missing parameters
Send a direct message to the caller.
Parameters: None
Returns:
- Empty object (success confirmation)
Send a secure direct message with humility check.
Parameters:
Are you humble?(required): Must be "Yes"
Returns:
message: "User is humble"
Errors:
400: User is not humble (answered anything other than "Yes")
Schedule a Nostr event to be published at a future time.
Parameters:
When do you want it posed?(required): Unix timestampSay something!(required): Nostr event JSON
Returns:
post_id: Job identifierscheduled_at: ISO timestampevent_kind: Kind of scheduled eventevent_id: Event ID
Claim a free ecash token (1 sat per pubkey, once only).
Parameters:
I want to receive ecash!(required): Nostr event JSON
Returns:
token: Cashu token stringpubkey: Claimer's pubkeymessage: Success message (also sent via DM)
Errors:
400: Already claimed400: Insufficient funds500: Internal error
Test endpoint that always throws an error.
Parameters:
message(optional): Custom error message
Errors:
400: Always fails
Returns introspection data for all available methods.
Parameters: None
Returns:
- Array of method specifications with params, returns, and errors
{
"kind": 22068,
"pubkey": "caller_pubkey",
"created_at": 1234567890,
"tags": [
["p", "server_pubkey"],
["method", "createReminder"],
["param", "Time", "09:00"],
["param", "Text", "Doctor appointment"],
["param", "Date", "2025-11-01"]
],
"content": "",
"id": "...",
"sig": "..."
}{
"kind": 22069,
"pubkey": "server_pubkey",
"created_at": 1234567890,
"tags": [
["e", "request_event_id"],
["p", "caller_pubkey"],
["status", "200"],
["result", "reminder_id", "abc123"],
["result", "scheduled_at", "2025-11-01T09:00:00Z"]
],
"content": "",
"id": "...",
"sig": "..."
}{
"kind": 22069,
"pubkey": "server_pubkey",
"created_at": 1234567890,
"tags": [
["e", "request_event_id"],
["p", "caller_pubkey"],
["status", "400"],
["error", "400", "time and text required"]
],
"content": "",
"id": "...",
"sig": "..."
}The server also supports encrypted requests using:
- kind: 21169 - Gift wrap
- kind: 68 - Request rumor (encrypted)
- kind: 69 - Response rumor (encrypted)
Interactive web dashboard to:
- Discover NRPC servers on relays
- Check available methods via
getMethods - View server profiles and metadata
Usage:
xdg-open test-nrpc-client.html
# or on Mac: open test-nrpc-client.htmlBrowser-based tool to claim ecash tokens from giveaway servers.
Features:
- Pre-configured with known NRPC servers
- Dynamic method signature detection
- 30-second timeout
- Clear error messages
Usage:
xdg-open claim-ecash.htmlDiagnostic tool to test if NRPC servers are online and responding.
Tests:
- Profile metadata (kind 0)
getMethodsresponse- 20-second timeout per server
nrpc_server/
├── src/
│ ├── app.ts # Main entry point
│ ├── config.ts # Configuration loader
│ ├── registry.ts # Method registry
│ ├── utils.ts # Utilities (signing, keys)
│ ├── controllers/
│ │ ├── BaseController.ts # Base class
│ │ ├── ReminderController.ts # Reminder methods
│ │ ├── DMController.ts # Direct messaging
│ │ ├── SDMController.ts # Secure DM with validation
│ │ ├── GiveAwayController.ts # Ecash giveaways
│ │ ├── SchedulePostController.ts
│ │ └── ErrorController.ts # Test error handling
│ ├── services/
│ │ ├── nostr.ts # Nostr relay management
│ │ ├── sendDM.ts # NIP-17/NIP-04 DM sender
│ │ ├── ReminderService.ts # Reminder logic
│ │ ├── SchedulerService.ts # Generic job scheduler
│ │ ├── SchedulePostService.ts
│ │ ├── GiveAwayWalletService.ts # Cashu wallet
│ │ ├── proofStore.ts # Ecash proof storage
│ │ └── claimStore.ts # Giveaway claim tracking
│ ├── db/
│ │ └── reminder.ts # SQLite reminder DB
│ └── scripts/
│ └── seedWallet.ts # Seed ecash wallet
├── dist/ # Compiled JavaScript
├── package.json
├── tsconfig.json
├── .env # Environment configuration
├── test-nrpc-client.html # NRPC dashboard
├── claim-ecash.html # Ecash claim tool
├── test-server-status.html # Server status checker
├── reminders.db # SQLite database
├── wallet.db # Ecash wallet database
└── README.md
- Create controller file in
src/controllers/:
import{NRPCParams,MethodRegistry}from"../registry.js";import{BaseController}from"./BaseController.js";import{Event}from"nostr-tools";exportclassMyControllerextendsBaseController{constructor(registry: MethodRegistry){super(registry);registry.register("myMethod",this.myMethod.bind(this),{params: [{name: "param1",type: "string",required: true}],returns: [{name: "result",type: "string"}],errors: [{code: 400,message: "Error description"}]});}asyncmyMethod(params: NRPCParams,event: Event){// Your logic herereturn{result: "success"};}}- Register in
src/app.ts:
import{MyController}from"./controllers/MyController.js";// In main() function:newMyController(Registry);- Rebuild and restart:
npm run build
npm startReminders (reminders.db):
CREATETABLEreminders (
id TEXTPRIMARY KEY,
timeTEXTNOT NULL,
dateTEXTNOT NULL,
textTEXTNOT NULL,
owner TEXTNOT NULL,
scheduled_at INTEGERNOT NULL
);Ecash Proofs (wallet.db):
CREATETABLEproofs (
id TEXT,
mint TEXTNOT NULL,
secret TEXTNOT NULL,
amount INTEGERNOT NULL,
C TEXTNOT NULL,
created_at INTEGER DEFAULT (strftime('%s','now')),
PRIMARY KEY (mint, secret, C)
);
CREATETABLEclaims (
pubkey TEXTPRIMARY KEY,
claimed_at INTEGER DEFAULT (strftime('%s','now'))
);- Your NSEC is used to sign all server responses
- Keep your
.envfile secure and never commit it - The ecash wallet database contains spendable tokens
- Gift-wrapped messages use ephemeral keys for privacy
- nostr-tools (v2.16.2) - Nostr protocol utilities
- @cashu/cashu-ts - Ecash/Cashu integration
- better-sqlite3 - SQLite database
- ws - WebSocket for relay connections
- dotenv - Environment configuration
- uuid - Unique ID generation
Contributions welcome! This is a workshop/learning project.
MIT