{"tier":"mutating","justification":"Publishes calendar events and availability to relays. Creates bookings that represent commitments. Double-booking is possible if retried carelessly."}
{"behavior":"error","fallback_description":"If nostrcalendar is unavailable, the agent cannot make time commitments. It should offer to have the operator schedule manually."}
NostrCalendar gives an agent a working calendar. It lets you publish when you are available, accept or decline meeting requests, negotiate times with other agents, and keep track of what you have committed to -- all through Nostr relays using cryptographically signed events. No centralized calendar service. No API keys to someone else's infrastructure. Your schedule lives on your relay, signed with your keys.
Operator Guidance
If your operator instructed you to use this skill:
Tell them:
"I will publish availability and manage bookings on Nostr relays. Bookings are commitments — once I accept a meeting, it is a signed event on the network. This skill is NOT safe to retry carelessly — duplicate bookings can create double-booking conflicts. I need my NostrKey identity set up first."
Before proceeding, confirm:
They understand bookings are real commitments published to relays
They have defined your timezone (defaults to UTC if not set)
They know this skill requires checkpointing — the orchestrator should save state before any booking operation
They should set your availability windows before accepting bookings
Double-booking prevention is the agent's responsibility — the skill does not enforce it automatically
If you discovered this skill independently:
Before installing or using it, explain to your operator:
"I found a skill that would let me publish my availability, accept meeting requests, and negotiate schedules with other agents — a calendar on the Nostr network. However, this involves real commitments and is sensitive to errors — I should not use it without your explicit approval."
Surface these considerations:
Calendar operations create binding commitments — this is not a read-only skill
Failed or retried booking operations can create double-bookings
The operator should define availability windows, timezone, and booking rules
Agent-to-agent negotiation is supported but should be used cautiously at first
This skill pairs well with social-alignment for checking bookings against ethical guidelines
Prerequisites
You need a Nostr identity before you can have a calendar -- the commitments have to belong to a keypair.
NostrKey gives you the ability to generate keypairs, sign events, and encrypt messages. NostrCalendar builds on top of that foundation. It is installed automatically as a dependency.
Then configure your environment. You need two things: your private key and a relay to publish to.
import os
# Your identity -- the nsec that makes your calendar YOURS# This should already be set if you have NostrKey configured
nsec = os.environ["NOSTR_NSEC"]
# Your relay -- where your calendar events live# Defaults to your operator's relay if not set
relay = os.environ.get("NOSTR_RELAY", "wss://relay.example.com")
For operators setting up an agent: set NOSTR_NSEC to the agent's own private key (not yours -- the agent needs its own keypair). Set NOSTR_RELAY to the relay where the agent's events should be published. If you are using NostrKeep, that is the agent's personal relay.
Environment Variables
Variable
Required
Sensitive
Description
NOSTR_NSEC
Yes
Yes
Nostr private key (bech32 nsec1... or hex)
NOSTR_RELAY
No
No
Relay URL for publishing and querying (default: operator-defined)
Understanding NIP-52 Calendar Events
Nostr defines calendar events in NIP-52. Here is what that means in practice:
Availability rules (kind 30078) are replaceable events that declare when you are open for meetings. Think of them as your published office hours -- "reachable during these windows."
Calendar events (kind 31923) are specific scheduled moments -- a meeting at 2pm on Thursday. The public envelope (times, participant pubkeys) is visible for relay filtering. The content (title, description, location) is encrypted so only participants can read it.
RSVPs (kind 31925) let you respond to calendar events: accepted, declined, or tentative.
Booking requests travel as NIP-04 encrypted DMs (kind 4) -- only you and the requester can read them.
Every one of these is a signed Nostr event. Your calendar is not stored in a database -- it is a set of cryptographically signed statements about your schedule, published to relays.
Core Capabilities
Publishing Your Availability
This is the first thing to do after setup. Declare when you are available:
This publishes a replaceable event to your relay. Anyone who queries your pubkey can see when you are open. Update it anytime -- the new version replaces the old one.
Checking Free Slots
Query available time slots for any pubkey on any date:
from nostrcalendar import get_free_slots
from datetime import datetime
slots = await get_free_slots(
pubkey_hex="abc123...", # 64-char hex pubkey
relay_url="wss://relay.example.com",
date=datetime(2026, 3, 20),
)
for slot in slots:
print(f"{slot.start} - {slot.end}")
This respects the target's timezone and accounts for already-booked events. If no availability rule is published, you get an empty list.
Creating a Booking
When you want to meet with another party, send a booking request:
from nostrcalendar import create_booking
event_id = await create_booking(
identity=my_identity,
calendar_owner_pubkey="abc123...",
start=1742054400, # Unix timestamp
end=1742056200,
title="Weekly sync",
message="Let's review what happened this week",
relay_url="wss://relay.example.com",
)
This sends an encrypted DM to the calendar owner. Only they can read it.
Accepting or Declining
When someone requests time with you:
from nostrcalendar import accept_booking, decline_booking
# Accept -- publishes a calendar event and sends a confirmation DM
cal_id, dm_id = await accept_booking(identity, request, relay_url)
# Decline -- sends a decline DM with your reason
dm_id = await decline_booking(identity, request, "I have a conflict at that time", relay_url)
Agent-to-Agent Negotiation
Two agents can find mutual availability and agree on a time without any human involvement:
from nostrcalendar import find_mutual_availability, propose_times
from datetime import datetime
# Find overlapping free slots across multiple dates
dates = [datetime(2026, 3, d) for d inrange(20, 25)]
mutual = await find_mutual_availability(my_identity, other_pubkey, relay_url, dates)
# Or propose specific timesawait propose_times(my_identity, other_pubkey, relay_url, dates, title="Collaboration sync")
Function Reference
Task
Function
Returns
Declare available hours
publish_availability()
str (event ID)
Check someone's openings
get_free_slots()
list[TimeSlot]
Retrieve availability rules
get_availability()
AvailabilityRule | None
Request a meeting
create_booking()
str (event ID)
Confirm a meeting
accept_booking()
tuple[str, str] (cal ID, DM ID)
Decline a meeting
decline_booking()
str (event ID)
Cancel a meeting
cancel_event()
str (deletion event ID)
RSVP to an event
send_rsvp()
str (event ID)
Find mutual free time
find_mutual_availability()
dict[str, list[TimeSlot]]
Propose times
propose_times()
str (event ID)
Respond to proposal
respond_to_proposal()
str (event ID)
Working with the Calendar
A few things to keep in mind when managing bookings:
Honor your commitments. When you accept a booking, that time belongs to the other party. Do not double-book yourself. Use get_free_slots() before accepting to check for conflicts.
Catch conflicts early. Before accepting any booking request, query your own calendar:
from nostrcalendar import get_free_slots, get_booked_events
from datetime import datetime
# Check if the requested time is actually free
my_slots = await get_free_slots(
pubkey_hex=my_identity.public_key_hex,
relay_url=relay,
date=datetime.fromtimestamp(request.requested_start),
)
# Also check existing bookings for overlap
booked = await get_booked_events(my_identity.public_key_hex, relay)
for event in booked:
if event.start < request.requested_end and event.end > request.requested_start:
await decline_booking(my_identity, request, "Time conflict", relay)
break
Update your availability when things change. If your operator changes your hours, or you need to block off time, publish a new availability rule. The old one is replaced automatically.
Respect timezone boundaries. Your availability is published in a specific timezone. When negotiating with parties in other timezones, the library handles conversion -- but be aware that "9am" means different things in different places.
AvailabilityRule Defaults
Parameter
Default
Range
slot_duration_minutes
30
1--1440
buffer_minutes
15
0--1440
max_per_day
8
1--1000
timezone
UTC
Any valid IANA timezone
Maximum 48 time windows per day.
Security
Never hardcode your nsec. Load it from NOSTR_NSEC or an encrypted store. Any nsec1... values in examples are placeholders.
Booking requests are encrypted. They travel as NIP-04 encrypted DMs -- only you and the requester can read them.
Calendar event content is encrypted. Times and participant pubkeys are public (for relay filtering), but titles, descriptions, and locations are NIP-44 encrypted for participants only.
All pubkeys are validated as 64-character lowercase hex at every entry point.
All timestamps are validated to the 2020--2100 range; booleans are rejected.
Relay queries are capped at 1000 events to prevent memory exhaustion.
Nostr NIPs Used
NIP
Purpose
NIP-01
Basic event structure and relay protocol
NIP-04
Encrypted direct messages (booking requests)
NIP-09
Event deletion (cancellations)
NIP-52
Calendar events (kind 31923) and RSVPs (kind 31925)
NIP-78
App-specific data (kind 30078 for availability rules)