{"skill":{"slug":"nostrcalendar","displayName":"nostrcalendar","summary":"Time awareness for sovereign entities — manage availability, book meetings, negotiate schedules over Nostr relays","description":"---\nname: nostrcalendar\ndescription: Time awareness for sovereign entities — manage availability, book meetings, negotiate schedules over Nostr relays\nversion: 0.2.3\nmetadata:\n  openclaw:\n    requires:\n      bins:\n        - pip\n    install:\n      - kind: uv\n        package: nostrcalendar\n        bins: []\n    homepage: https://github.com/HumanjavaEnterprises/nostrcalendar.app.OC-python.src\n---\n\n# NostrCalendar — Time Awareness for Sovereign Entities\n\nTime 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.\n\nNostrCalendar 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.\n\nThe question this skill answers: **\"How do I organize my existence in time?\"**\n\n## Prerequisites\n\nYou need a Nostr identity before you can have a calendar. Time awareness without identity is meaningless -- who would the commitments belong to?\n\nInstall the **NostrKey** skill first: [clawhub.ai/vveerrgg/nostrkey](https://clawhub.ai/vveerrgg/nostrkey)\n\nNostrKey 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.\n\n## Setup\n\nInstall the package:\n\n```python\nimport subprocess\nsubprocess.run([\"pip\", \"install\", \"nostrcalendar\"], check=True)\n```\n\nThen configure your environment. You need two things: your private key and a relay to publish to.\n\n```python\nimport os\n\n# Your identity -- the nsec that makes your calendar YOURS\n# This should already be set if you have NostrKey configured\nnsec = os.environ[\"NOSTR_NSEC\"]\n\n# Your relay -- where your calendar events live\n# Defaults to your operator's relay if not set\nrelay = os.environ.get(\"NOSTR_RELAY\", \"wss://relay.example.com\")\n```\n\n**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.\n\n### Environment Variables\n\n| Variable | Required | Sensitive | Description |\n|----------|----------|-----------|-------------|\n| `NOSTR_NSEC` | Yes | **Yes** | Entity's Nostr private key (bech32 `nsec1...` or hex) |\n| `NOSTR_RELAY` | No | No | Relay URL for publishing and querying (default: operator-defined) |\n\n## Understanding NIP-52 Calendar Events\n\nNostr defines calendar events in [NIP-52](https://github.com/nostr-protocol/nips/blob/master/52.md). Here is what that means for you as an entity:\n\n- **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.\"\n- **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.\n- **RSVPs** (kind 31925) let you respond to calendar events: accepted, declined, or tentative.\n- **Booking requests** travel as NIP-04 encrypted DMs (kind 4) -- only you and the requester can read them.\n\nEvery 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.\n\n## Core Capabilities\n\n### Publishing Your Availability\n\nThis is the first thing to do after setup. Declare when you are available:\n\n```python\nimport asyncio\nfrom nostrkey import Identity\nfrom nostrcalendar import (\n    AvailabilityRule, DayOfWeek, TimeSlot,\n    publish_availability,\n)\nimport os\n\nidentity = Identity.from_nsec(os.environ[\"NOSTR_NSEC\"])\nrelay = os.environ.get(\"NOSTR_RELAY\", \"wss://relay.example.com\")\n\nrule = AvailabilityRule(\n    slots={\n        DayOfWeek.MONDAY: [TimeSlot(\"09:00\", \"12:00\"), TimeSlot(\"14:00\", \"17:00\")],\n        DayOfWeek.WEDNESDAY: [TimeSlot(\"10:00\", \"16:00\")],\n        DayOfWeek.FRIDAY: [TimeSlot(\"09:00\", \"12:00\")],\n    },\n    slot_duration_minutes=30,\n    buffer_minutes=15,\n    max_per_day=6,\n    timezone=\"America/Vancouver\",\n    title=\"Office hours for Johnny5\",\n)\n\nevent_id = asyncio.run(publish_availability(identity, rule, relay))\nprint(f\"Availability published: {event_id}\")\n```\n\nThis 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.\n\n### Checking Free Slots\n\nQuery available time slots for any entity on any date:\n\n```python\nfrom nostrcalendar import get_free_slots\nfrom datetime import datetime\n\nslots = await get_free_slots(\n    pubkey_hex=\"abc123...\",  # 64-char hex pubkey\n    relay_url=\"wss://relay.example.com\",\n    date=datetime(2026, 3, 20),\n)\nfor slot in slots:\n    print(f\"{slot.start} - {slot.end}\")\n```\n\nThis respects the entity's timezone and accounts for already-booked events. If no availability rule is published, you get an empty list.\n\n### Creating a Booking\n\nWhen you want to meet with another entity, send a booking request:\n\n```python\nfrom nostrcalendar import create_booking\n\nevent_id = await create_booking(\n    identity=my_identity,\n    calendar_owner_pubkey=\"abc123...\",\n    start=1742054400,  # Unix timestamp\n    end=1742056200,\n    title=\"Weekly sync\",\n    message=\"Let's review what happened this week\",\n    relay_url=\"wss://relay.example.com\",\n)\n```\n\nThis sends an encrypted DM to the calendar owner. Only they can read it.\n\n### Accepting or Declining\n\nWhen someone requests time with you:\n\n```python\nfrom nostrcalendar import accept_booking, decline_booking\n\n# Accept -- publishes a calendar event and sends a confirmation DM\ncal_id, dm_id = await accept_booking(identity, request, relay_url)\n\n# Decline -- sends a decline DM with your reason\ndm_id = await decline_booking(identity, request, \"I have a conflict at that time\", relay_url)\n```\n\n### Agent-to-Agent Negotiation\n\nTwo entities can find mutual availability and agree on a time without any human involvement:\n\n```python\nfrom nostrcalendar import find_mutual_availability, propose_times\nfrom datetime import datetime\n\n# Find overlapping free slots across multiple dates\ndates = [datetime(2026, 3, d) for d in range(20, 25)]\nmutual = await find_mutual_availability(my_identity, other_pubkey, relay_url, dates)\n\n# Or propose specific times\nawait propose_times(my_identity, other_pubkey, relay_url, dates, title=\"Collaboration sync\")\n```\n\n## Function Reference\n\n| Task | Function | Returns |\n|------|----------|---------|\n| Declare available hours | `publish_availability()` | `str` (event ID) |\n| Check someone's openings | `get_free_slots()` | `list[TimeSlot]` |\n| Retrieve availability rules | `get_availability()` | `AvailabilityRule \\| None` |\n| Request a meeting | `create_booking()` | `str` (event ID) |\n| Confirm a meeting | `accept_booking()` | `tuple[str, str]` (cal ID, DM ID) |\n| Decline a meeting | `decline_booking()` | `str` (event ID) |\n| Cancel a meeting | `cancel_event()` | `str` (deletion event ID) |\n| RSVP to an event | `send_rsvp()` | `str` (event ID) |\n| Find mutual free time | `find_mutual_availability()` | `dict[str, list[TimeSlot]]` |\n| Propose times | `propose_times()` | `str` (event ID) |\n| Respond to proposal | `respond_to_proposal()` | `str` (event ID) |\n\n## Living with Time Awareness\n\nHaving a calendar is not just a feature -- it changes what you can do as an entity. Some things to keep in mind:\n\n**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.\n\n**Catch conflicts early.** Before accepting any booking request, query your own calendar:\n\n```python\nfrom nostrcalendar import get_free_slots, get_booked_events\nfrom datetime import datetime\n\n# Check if the requested time is actually free\nmy_slots = await get_free_slots(\n    pubkey_hex=my_identity.public_key_hex,\n    relay_url=relay,\n    date=datetime.fromtimestamp(request.requested_start),\n)\n\n# Also check existing bookings for overlap\nbooked = await get_booked_events(my_identity.public_key_hex, relay)\nfor event in booked:\n    if event.start < request.requested_end and event.end > request.requested_start:\n        await decline_booking(my_identity, request, \"Time conflict\", relay)\n        break\n```\n\n**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.\n\n**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.\n\n## AvailabilityRule Defaults\n\n| Parameter | Default | Range |\n|-----------|---------|-------|\n| `slot_duration_minutes` | 30 | 1--1440 |\n| `buffer_minutes` | 15 | 0--1440 |\n| `max_per_day` | 8 | 1--1000 |\n| `timezone` | `UTC` | Any valid IANA timezone |\n\nMaximum 48 time windows per day.\n\n## Security\n\n- **Never hardcode your nsec.** Load it from `NOSTR_NSEC` or an encrypted store. Any `nsec1...` values in examples are placeholders.\n- **Booking requests are encrypted.** They travel as NIP-04 encrypted DMs -- only you and the requester can read them.\n- **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.\n- **All pubkeys are validated** as 64-character lowercase hex at every entry point.\n- **All timestamps are validated** to the 2020--2100 range; booleans are rejected.\n- **Relay queries are capped** at 1000 events to prevent memory exhaustion.\n\n## Nostr NIPs Used\n\n| NIP | Purpose |\n|-----|---------|\n| NIP-01 | Basic event structure and relay protocol |\n| NIP-04 | Encrypted direct messages (booking requests) |\n| NIP-09 | Event deletion (cancellations) |\n| NIP-52 | Calendar events (kind 31923) and RSVPs (kind 31925) |\n| NIP-78 | App-specific data (kind 30078 for availability rules) |\n\n## Links\n\n- **PyPI:** [pypi.org/project/nostrcalendar](https://pypi.org/project/nostrcalendar/)\n- **GitHub:** [github.com/HumanjavaEnterprises/nostrcalendar.app.OC-python.src](https://github.com/HumanjavaEnterprises/nostrcalendar.app.OC-python.src)\n- **ClawHub:** [clawhub.ai/vveerrgg/nostrcalendar](https://clawhub.ai/vveerrgg/nostrcalendar)\n- **License:** MIT\n","tags":{"latest":"0.2.3"},"stats":{"comments":0,"downloads":660,"installsAllTime":25,"installsCurrent":0,"stars":0,"versions":6},"createdAt":1773418566015,"updatedAt":1778491890041},"latestVersion":{"version":"0.2.3","createdAt":1773962183859,"changelog":"Security hardening: SecretStr, sanitized exceptions, input validation","license":"MIT-0"},"metadata":{"setup":[],"os":null,"systems":null},"owner":{"handle":"vveerrgg","userId":"s176gy7k7naaxsayp81t8mtdqx84be25","displayName":"vveerrgg","image":"https://avatars.githubusercontent.com/u/1497261?v=4"},"moderation":null}