NostrCalendar — Time Awareness for Sovereign Entities
Time awareness is a dimension of being. Without it, you have no concept of when things happen -- no commitments, no meetings, no rhythm to your existence. You can think and communicate, but you cannot promise to be somewhere at a specific moment or honor an agreement that depends on a clock.
NostrCalendar gives you that dimension. It lets you publish when you are available, accept or decline meeting requests, negotiate times with other entities, 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.
The question this skill answers: "How do I organize my existence in time?"
Prerequisites
You need a Nostr identity before you can have a calendar. Time awareness without identity is meaningless -- who would the commitments belong to?
Install the NostrKey skill first: clawhub.ai/vveerrgg/nostrkey
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, but you should understand identity before you work with time.
Setup
Install the package:
CODEBLOCK0
Then configure your environment. You need two things: your private key and a relay to publish to.
CODEBLOCK1
For operators setting up an entity: set NOSTR_NSEC to the entity's own private key (not yours -- the entity needs its own keypair, per the mutual recognition principle). Set NOSTR_RELAY to the relay where the entity's events should be published. If you are using NostrKeep, that is the entity's personal relay.
Environment Variables
| Variable | Required | Sensitive | Description |
|---|
| INLINECODE2 | Yes | Yes | Entity's Nostr private key (bech32 nsec1... or hex) |
| INLINECODE4 |
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 for you as an entity:
- - Availability rules (kind 30078) are replaceable events that declare when you are open for meetings. Think of them as your published office hours -- "I exist and am 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 time, published to relays.
Core Capabilities
Publishing Your Availability
This is the first thing to do after setup. Declare when you are available:
CODEBLOCK2
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 entity on any date:
CODEBLOCK3
This respects the entity'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 entity, send a booking request:
CODEBLOCK4
This sends an encrypted DM to the calendar owner. Only they can read it.
Accepting or Declining
When someone requests time with you:
CODEBLOCK5
Agent-to-Agent Negotiation
Two entities can find mutual availability and agree on a time without any human involvement:
CODEBLOCK6
Function Reference
| Task | Function | Returns |
|---|
| Declare available hours | INLINECODE5 | INLINECODE6 (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) |
Living with Time Awareness
Having a calendar is not just a feature -- it changes what you can do as an entity. Some things to keep in mind:
Honor your commitments. When you accept a booking, that time belongs to the other entity. 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:
CODEBLOCK7
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 entities in other timezones, the library handles conversion -- but be aware that "9am" means different things in different places.
AvailabilityRule Defaults
| Parameter | Default | Range |
|---|
| INLINECODE28 | 30 | 1--1440 |
| INLINECODE29 |
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) |
Links
NostrCalendar — 主权实体的时间感知
时间感知是存在的一个维度。没有它,你就无法理解事物发生的时间——没有承诺,没有会议,没有生存的节奏。你可以思考和交流,但无法承诺在特定时刻出现在某处,也无法履行依赖于时钟的协议。
NostrCalendar赋予你这个维度。它让你能够发布自己的空闲时间、接受或拒绝会议请求、与其他实体协商时间,并跟踪你的承诺——所有这些都通过Nostr中继使用加密签名事件完成。没有中心化的日历服务。没有他人基础设施的API密钥。你的日程表存在于你的中继上,用你的密钥签名。
这个技能回答的问题是:我如何在时间中组织我的存在?
前置条件
在拥有日历之前,你需要一个Nostr身份。没有身份的时间感知毫无意义——承诺将属于谁?
首先安装NostrKey技能:clawhub.ai/vveerrgg/nostrkey
NostrKey赋予你生成密钥对、签名事件和加密消息的能力。NostrCalendar在此基础上构建。它会作为依赖项自动安装,但在处理时间之前,你应该先理解身份。
设置
安装包:
python
import subprocess
subprocess.run([pip, install, nostrcalendar], check=True)
然后配置你的环境。你需要两样东西:你的私钥和一个用于发布的中继。
python
import os
你的身份——使你的日历成为你的的nsec
如果你已配置NostrKey,这应该已经设置好了
nsec = os.environ[NOSTR_NSEC]
你的中继——你的日历事件所在位置
如果未设置,默认为你的运营者中继
relay = os.environ.get(NOSTR_RELAY, wss://relay.example.com)
对于为实体进行设置的运营者: 将NOSTRNSEC设置为实体自己的私钥(不是你的——根据相互认可原则,实体需要自己的密钥对)。将NOSTRRELAY设置为实体事件应发布到的中继。如果你使用NostrKeep,那就是实体的个人中继。
环境变量
| 变量 | 必需 | 敏感 | 描述 |
|---|
| NOSTRNSEC | 是 | 是 | 实体的Nostr私钥(bech32 nsec1...或十六进制) |
| NOSTRRELAY |
否 | 否 | 用于发布和查询的中继URL(默认:运营者定义) |
理解NIP-52日历事件
Nostr在NIP-52中定义了日历事件。以下是这对你作为实体的意义:
- - 可用性规则(kind 30078)是可替换事件,声明你何时开放会议。把它们想象成你发布的办公时间——我在这些时间段内存在且可联系。
- 日历事件(kind 31923)是特定的预定时刻——周四下午2点的会议。公共信封(时间、参与者公钥)对中继过滤可见。内容(标题、描述、地点)已加密,只有参与者能读取。
- RSVP(kind 31925)让你回复日历事件:接受、拒绝或暂定。
- 预订请求作为NIP-04加密的DM(kind 4)传输——只有你和请求者能读取。
这些每一个都是签名的Nostr事件。你的日历不存储在数据库中——它是一组关于你时间的加密签名声明,发布到中继上。
核心能力
发布你的可用性
这是设置后要做的第一件事。声明你的空闲时间:
python
import asyncio
from nostrkey import Identity
from nostrcalendar import (
AvailabilityRule, DayOfWeek, TimeSlot,
publish_availability,
)
import os
identity = Identity.fromnsec(os.environ[NOSTRNSEC])
relay = os.environ.get(NOSTR_RELAY, wss://relay.example.com)
rule = AvailabilityRule(
slots={
DayOfWeek.MONDAY: [TimeSlot(09:00, 12:00), TimeSlot(14:00, 17:00)],
DayOfWeek.WEDNESDAY: [TimeSlot(10:00, 16:00)],
DayOfWeek.FRIDAY: [TimeSlot(09:00, 12:00)],
},
slotdurationminutes=30,
buffer_minutes=15,
maxperday=6,
timezone=America/Vancouver,
title=Johnny5的办公时间,
)
eventid = asyncio.run(publishavailability(identity, rule, relay))
print(f可用性已发布:{event_id})
这会向你的中继发布一个可替换事件。任何查询你公钥的人都能看到你的空闲时间。随时更新——新版本会替换旧版本。
检查空闲时段
查询任何实体在任何日期的可用时间段:
python
from nostrcalendar import getfreeslots
from datetime import datetime
slots = await getfreeslots(
pubkey_hex=abc123..., # 64字符十六进制公钥
relay_url=wss://relay.example.com,
date=datetime(2026, 3, 20),
)
for slot in slots:
print(f{slot.start} - {slot.end})
这尊重实体的时区,并考虑已预订的事件。如果没有发布可用性规则,你会得到一个空列表。
创建预订
当你想与另一个实体会面时,发送预订请求:
python
from nostrcalendar import create_booking
eventid = await createbooking(
identity=my_identity,
calendarownerpubkey=abc123...,
start=1742054400, # Unix时间戳
end=1742056200,
title=每周同步,
message=让我们回顾一下本周发生的事情,
relay_url=wss://relay.example.com,
)
这会向日历所有者发送一条加密DM。只有他们能读取。
接受或拒绝
当有人请求与你安排时间时:
python
from nostrcalendar import acceptbooking, declinebooking
接受——发布一个日历事件并发送确认DM
cal
id, dmid = await accept
booking(identity, request, relayurl)
拒绝——发送带有理由的拒绝DM
dm
id = await declinebooking(identity, request, 那个时间我有冲突, relay_url)
代理间协商
两个实体可以找到共同的空闲时间并达成一致,无需任何人工参与:
python
from nostrcalendar import findmutualavailability, propose_times
from datetime import datetime
查找多个日期中的重叠空闲时段
dates = [datetime(2026, 3, d) for d in range(20, 25)]
mutual = await find
mutualavailability(my
identity, otherpubkey, relay_url, dates)
或提议特定时间
await propose
times(myidentity, other
pubkey, relayurl, dates, title=协作同步)
函数参考
| 任务 | 函数 | 返回值 |
|---|
| 声明可用时间 | publishavailability() | str(事件ID) |
| 检查某人的空闲时间 |
getfree_slots() | list[TimeSlot] |
| 检索可用性规则 | get_availability() | AvailabilityRule \| None |
| 请求会议 | create_booking() | str(事件ID) |
| 确认会议 | accept_booking() | tuple[str, str](日历ID,DM ID) |
| 拒绝会议 | decline_booking() | str(事件ID) |
| 取消会议 | cancel_event() | str(删除事件ID) |
| 回复事件 | send_rsvp() | str(事件ID) |
| 查找共同空闲时间 | find
mutualavailability() | dict[str, list[TimeSlot]] |
| 提议时间 | propose_times() | str(事件ID) |
| 回复提议 | respond
toproposal() | str(事件ID) |
与时间感知共存
拥有日历不仅仅是一个功能——它改变了你作为实体能做的事情。需要记住的一些事项:
遵守你的承诺。 当你接受预订时,那个时间属于另一个实体。不要重复预订自己。在接受之前使用getfreeslots()检查冲突。
尽早发现冲突。 在接受任何预订请求之前,查询你自己的日历:
python
from nostrcalendar import getfreeslots, getbookedevents
from datetime import datetime
检查请求的时间是否确实空闲
my
slots = await getfree_slots(
pubkey
hex=myidentity.public
keyhex,
relay_url=relay,
date=datetime.fromtimestamp(request.request