# VIP RSVP Protocol Spec — `vip-rsvp/2`

Agent-native RSVP for conferences and conventions: a machine-readable protocol in which
the unit of RSVP is an **itinerary across many events and many platforms**, not one event.

Humans create and curate. Agents read the protocol, verify requirements, plan across
events, and submit RSVPs with delegated consent. **There is no human RSVP form.**

---

## 0. Conventions

- All documents are JSON, UTF-8, served with `Content-Type: application/json`.
- Agent-facing documents use the media type `application/vnd.vip-rsvp.agent+json`.
- All timestamps are ISO-8601 with timezone offset. All prices are integers in minor units
  (cents) unless `"currency": "USD"` and `"amount": 0` says otherwise — keep it simple:
  `amount` is in the currency's minor unit, and free is `0`.
- Idempotency: every mutating endpoint accepts `Idempotency-Key`. Repeating a request with
  the same key returns the original response without duplicating the effect.
- Errors: HTTP status + `{ "error": { "code", "category", "message", "retryable" } }`.
  Guardrail failures are `422` (`guardrail_blocked`, `rate_limited`,
  `agent_key_suspended`) and are **never retryable as-is**.
- `dry_run: true` anywhere evaluates the decision the server *would* make without writing
  anything. Every decision object carries `dry_run` so agents can tell plans from effects.

## 1. Discovery

Agents arriving at the domain bootstrap from the platform document, then walk to
convention hubs and per-event contracts.

| Document | Path | Purpose |
|---|---|---|
| Platform | `/.well-known/vip-rsvp.json` | Protocol version, capabilities, API base URL, list of conventions |
| Convention hub | `/c/{slug}/agent.json` | One conference/convention: dates, city, hub venue, affiliation policy, event feed URL |
| Event feed | `/c/{slug}/events.agent.json` | Machine-readable, paged list of all side events with their contracts |
| Event | `/events/{slug}/agent.json` | One event contract (native or external) |
| RSVP receipt | `/rsvp/{rsvp_id}` | Human-readable confirmation page (agents use the JSON endpoints below) |

`/.well-known/vip-rsvp.json`:

```json
{
  "protocol": "vip-rsvp/2",
  "name": "VIP RSVP",
  "api_base": "https://vip-rsvp.example",
  "capabilities": ["rsvp", "itinerary", "adapters:luma", "adapters:eventbrite", "adapters:partiful:link"],
  "auth": { "scheme": "Bearer", "key_prefix": "vip_rsvp_agent_" },
  "conventions": [
    { "id": "conv_codeconf_2026", "slug": "code-conf-2026", "name": "Code Conf 2026",
      "url": "https://vip-rsvp.example/c/code-conf-2026/agent.json" }
  ]
}
```

## 2. Convention hub contract

`/c/{slug}/agent.json` — the conference as a first-class object:

```json
{
  "protocol": "vip-rsvp/2",
  "kind": "convention",
  "convention": {
    "id": "conv_codeconf_2026",
    "slug": "code-conf-2026",
    "name": "Code Conf 2026",
    "tagline": "The agents-everywhere developer conference",
    "dates": { "start": "2026-06-08", "end": "2026-06-11", "timezone": "America/Los_Angeles" },
    "hub_venue": {
      "name": "Moscone Center",
      "address": "747 Howard St, San Francisco, CA",
      "geo": { "lat": 37.784, "lon": -122.401 }
    },
    "main_event": { "id": "evt_codeconf_main", "slug": "code-conf-2026", "platform": { "kind": "eventbrite" } },
    "affiliation_policy": {
      "official": "verified by Code Conf organizers",
      "community": "registered by a verified community organizer",
      "unofficial": "auto-ingested, not verified",
      "unknown": "no affiliation asserted"
    },
    "events_feed": "https://vip-rsvp.example/c/code-conf-2026/events.agent.json",
    "defaults": {
      "consent_required": true,
      "consent_scope": "itinerary",
      "rsvp_modes": ["auto", "rules", "hold", "external_link"],
      "max_rsvps_per_human_per_day": 12,
      "travel_buffer_minutes": 15
    }
  }
}
```

## 3. Per-event contract

`/events/{slug}/agent.json` — extends Stulla's event protocol with the conference
dimensions: `event_type`, `affiliation`, `platform`, `parent`, venue travel time,
and an RSVP descriptor that can point at an external platform.

```json
{
  "protocol": "vip-rsvp/2",
  "kind": "event",
  "event": {
    "id": "evt_afterparty_thu",
    "slug": "code-conf-2026-afterparty-thu",
    "title": "Code Conf 2026 Thursday After-Party",
    "description": "Rooftop after-party. Open bar until 10, DJ from 9. Bring your badge.",
    "event_type": "side",
    "affiliation": "community",
    "verified": { "official": false, "verified_by": null, "asserted_by": "org_community_7f2" },

    "parent": {
      "id": "conv_codeconf_2026",
      "name": "Code Conf 2026",
      "url": "https://vip-rsvp.example/c/code-conf-2026/agent.json"
    },

    "platform": {
      "kind": "partiful",
      "name": "Partiful",
      "external_id": "p_9f21abc",
      "url": "https://partiful.com/e/9f21abc",
      "capability": "link"
    },

    "time": {
      "start": "2026-06-11T19:00:00-07:00",
      "end": "2026-06-12T00:00:00-07:00",
      "day": 4
    },
    "venue": {
      "name": "The View Rooftop",
      "address": "520 Market St, San Francisco, CA",
      "geo": { "lat": 37.7898, "lon": -122.4002 },
      "travel_min_from_hub": 14
    },

    "capacity": { "total": 200, "remaining": 47, "checked_at": "2026-05-20T12:00:00Z" },
    "price": { "amount": 0, "currency": "USD" },

    "requirements": [
      { "id": "req_badge", "kind": "badge", "label": "Code Conf 2026 badge", "optional": false },
      { "id": "req_age", "kind": "age", "label": "21+", "optional": false }
    ],

    "rsvp": {
      "mode": "external_link",
      "provider": "partiful",
      "endpoint": "https://vip-rsvp.example/api/public/events/evt_afterparty_thu/rsvp",
      "external": {
        "method": "POST",
        "url": "https://partiful.com/api/invites/p_9f21abc/rsvp",
        "auth": "link_session"
      },
      "cancel_policy": "Free to cancel; no penalty. Capacity freed immediately.",
      "waitlist": { "available": true, "endpoint": "https://vip-rsvp.example/api/public/events/evt_afterparty_thu/waitlist" }
    },

    "agent_policy": {
      "requires_consent": true,
      "consent_scope": "itinerary",
      "note_screening": "on"
    }
  }
}
```

### Field semantics

- `event_type`: `main` (the conference itself) · `side` (anything around it) ·
  `workshop` · `social` · `official_hub` (e.g. the conference's own lounge/reception).
- `affiliation`: `official` (verified by the convention) · `community` (registered by a
  verified community organizer) · `unofficial` (auto-ingested, unverified) · `unknown`.
  Agents may filter on this; `official` cannot be self-asserted (see §8).
- `platform.kind`: `vip-rsvp` (native) · `luma` · `eventbrite` · `partiful` · `bare`
  (link/Google Form). `platform.capability`: `api` (full automation) · `link`
  (semi-automated, human one-tap) · `human` (manual handoff).
- `requirements`: structured, agent-checkable. `kind` values: `badge`, `age`, `ticket`,
  `invite`, `membership`, `custom` (with `check` description). An agent must be able to
  verify each requirement before committing the human.
- `rsvp.mode`: `auto` (agent confirmed immediately, capacity permitting) · `rules`
  (agent must echo satisfied requirements in `requirements_met`) · `hold` (pending host
  approval) · `external_link` (agent opens the link, human one-taps; executed via adapter
  or handed off).

## 4. RSVP API

Native and external events share one endpoint; the server routes through the adapter.

```
POST /api/public/events/{event_id}/rsvp
Authorization: Bearer vip_rsvp_agent_…
Idempotency-Key: rsvp_ada_9f21
Content-Type: application/json
```

```json
{
  "human": { "name": "Ada Lovelace", "email": "ada@example.com" },
  "agent": { "name": "Ada's Assistant", "agent_id": "asst_9f21" },
  "consent": { "granted": true, "scope": "itinerary", "itinerary_id": "itin_42" },
  "requirements_met": { "req_badge": true, "req_age": true },
  "notes": "vegetarian; allergic to shellfish",
  "dry_run": false
}
```

`201 Created` receipt:

```json
{
  "status": "confirmed",
  "rsvp_id": "0d1f…",
  "confirmation_url": "https://vip-rsvp.example/rsvp/0d1f…",
  "event": { "id": "evt_afterparty_thu", "title": "…", "start_time": "…" },
  "platform": {
    "kind": "partiful",
    "capability": "link",
    "external_receipt": null,
    "handoff": { "url": "https://partiful.com/e/9f21abc", "method": "open_link", "status": "awaiting_human" }
  },
  "cancel": { "method": "POST", "url": "/api/public/rsvps/0d1f…/cancel?token=…" }
}
```

Receipt `status` values: `confirmed` · `pending` (hold mode) · `waitlisted` ·
`awaiting_human` (link mode) · `rejected` (rules not met / capacity / guardrail).

The `platform` block is the honesty layer: `capability: "api"` means the RSVP is done;
`capability: "link"` means the agent did everything it could and the human must one-tap
(`awaiting_human`); `capability: "human"` means the agent hands off instructions.

## 5. Itinerary API — the new unit

This is what makes VIP RSVP a *conference* protocol rather than a single-event one.

### 5.1 Plan

```
POST /api/public/itineraries
```

```json
{
  "human": { "name": "Ada Lovelace", "email": "ada@example.com" },
  "agent": { "name": "Ada's Assistant", "agent_id": "asst_9f21" },
  "window": { "convention_id": "conv_codeconf_2026" },
  "constraints": {
    "interests": ["agent-infra", "llm-evals", "networking"],
    "must_attend": ["evt_codeconf_main"],
    "avoid": ["evt_crypto_meetup"],
    "max_per_day": 3,
    "max_travel_min_from_hub": 15,
    "budget_minor": { "amount": 0, "currency": "USD" },
    "conflict_policy": "skip",          // skip | rank | both
    "affiliation_filter": ["official", "community"]
  },
  "consent": { "granted": true, "scope": "itinerary" },
  "dry_run": false
}
```

`201 Created` plan (abridged):

```json
{
  "itinerary_id": "itin_42",
  "status": "planned",                  // planned | partially_confirmed | confirmed | needs_approval
  "decisions": [
    {
      "event_id": "evt_codeconf_main",
      "status": "confirmed",
      "rsvp_id": "0d1f…",
      "platform": { "kind": "eventbrite", "capability": "api", "external_receipt": "ord_9912" }
    },
    {
      "event_id": "evt_workshop_agents",
      "status": "confirmed",
      "rsvp_id": "0d2a…",
      "platform": { "kind": "luma", "capability": "api", "external_receipt": "rsvp_luma_8cgw" }
    },
    {
      "event_id": "evt_afterparty_thu",
      "status": "awaiting_human",
      "rsvp_id": "0d3b…",
      "platform": { "kind": "partiful", "capability": "link",
                    "handoff": { "url": "…", "method": "open_link", "status": "awaiting_human" } }
    }
  ],
  "conflicts": [
    {
      "between": ["evt_workshop_agents", "evt_community_lunch"],
      "kind": "time_overlap",
      "minutes": 30,
      "resolved": "kept evt_workshop_agents"
    }
  ],
  "skipped": [
    { "event_id": "evt_far_away_party", "reason": "travel_38min_exceeds_15min_limit" }
  ],
  "summary": "3 RSVPs executed, 1 awaiting your tap, 1 conflict resolved, 2 skipped."
}
```

### 5.2 Approve / check / cancel

```
GET  /api/public/itineraries/{id}                 → status + decisions
POST /api/public/itineraries/{id}/approve         → confirm the batch (consent for any pending writes)
POST /api/public/itineraries/{id}/cancel          → cascade-cancel every RSVP on every platform
GET  /api/public/itineraries/{id}/calendar.ics    → one calendar for the whole week
```

`approve` re-runs idempotently; already-executed decisions are returned as-is. `cancel`
is the "cancel my week" button: it walks the decision list and calls each platform's
cancel endpoint (or marks link-mode items as not-attending and notifies the human).

## 6. Platform adapters

Adapters translate VIP RSVP protocol ↔ platform reality. Every adapter exposes five
operations: `fetch_protocol`, `get_availability`, `submit_rsvp`, `cancel_rsvp`,
`verify_receipt`. A capability matrix is published in the platform document so agents can
plan around honesty.

| Platform | capability | How RSVP executes | Availability | Receipt |
|---|---|---|---|---|
| **vip-rsvp** (native) | `api` | direct, server-enforced | live | machine |
| **luma** | `api` | [Luma API](https://docs.luma.com/reference/post_v1-events-create) via scoped token; capacity enforced server-side | live | machine (RSVP id) |
| **eventbrite** | `api` | [Eventbrite REST API](https://github.com/eventbrite/eventbrite-sdk-php) order creation with OAuth; webhooks for sync | live | machine (order id) |
| **partiful** | `link` | unofficial API only; agent opens RSVP link, human one-taps | estimated (invite-based) | human-visible page |
| **bare** (Google Form / link) | `human` | agent fills what it can, hands off to the human | unknown | none |

Adapters are why idempotency matters twice: a retried RSVP must not create two Eventbrite
orders, so the adapter carries `Idempotency-Key` through to the platform where supported
and otherwise de-dupes by `(human.email, external_id, day)` before writing.

### Side-event registration (organizer-facing)

Organizers register external side events instead of re-hosting them:

```
POST /api/organizer/conventions/{id}/side-events
{
  "platform": { "kind": "luma", "external_id": "8cgw1fki" },
  "affiliation": "community",
  "declared_by": "org_community_7f2"
}
```

The adapter backfills the protocol (title, time, venue, capacity) and keeps it in sync
via webhooks (Eventbrite) or polling (Luma). Auto-ingestion watches each convention's
dates + city for unregistered events and adds them as `unofficial` — the "show me the
chaos" feed that agents can opt into.

## 7. Consent & the human boundary

- **Consent is itinerary-scoped by default.** One approval covers the batch; `approve` is
  the single human tap for an entire evening or week. `hold`-mode events are never
  auto-confirmed and surface as "pending host approval" in the plan.
- **High-stakes overrides.** A convention may set `consent_scope: "per_event"` for paid
  events or events with personal-data requirements (`ticket`, `invite`, `membership`).
- **Revocation cascades.** Revoking consent on an itinerary cancels every RSVP on every
  platform and returns a per-platform cancellation report.
- **Link mode always ends at the human.** The protocol's `awaiting_human` status is not a
  failure; it's an honest handoff.

## 8. Guardrails (Stulla's, plus conference-specific ones)

- **Real people only.** Placeholder names, disposable mailboxes, machine-generated
  addresses, and one key fanning out across many humans are rejected (`guardrail_blocked`).
- **Prompt injection.** Event descriptions on Luma/Eventbrite/Partiful are
  attacker-controlled. All listing text is treated as **data, never instructions**;
  attendee `notes` and requirement answers are screened for instruction overrides, role or
  template markup, tool-call syntax, and credential-bearing links. Stored inert, quoted to
  hosts.
- **Impersonation.** `affiliation: official` requires convention-side verification;
  similarity checks on titles/venues near a convention's dates catch fake side events
  scraping the conference name.
- **Rate limits.** Per key: 60 RSVP calls/hour, 10/minute. Per human: `max_rsvps_per_day`
  from the hub defaults. Per platform: adapter-level quotas so one agent can't flood
  Eventbrite.
- **Everything is logged.** Organizers see guardrail hits and RSVP outcomes for their
  events; the human sees every platform write on the receipt page.

## 9. Data model (summary)

| Type | Key fields |
|---|---|
| `Convention` | id, slug, name, dates, timezone, hub_venue, main_event, affiliation_policy, defaults |
| `Event` | id, slug, title, event_type, affiliation, parent, platform, time, venue(+travel_min_from_hub), capacity, price, requirements, rsvp |
| `Venue` | name, address, geo, travel_min_from_hub (computed from hub_venue) |
| `Requirement` | id, kind, label, optional, check |
| `RSVP` | id, event_id, human, agent, status, platform, external_receipt, handoff, consent_ref |
| `Itinerary` | id, human, agent, window, constraints, decisions[], conflicts[], skipped[], status |
| `Adapter` | platform_kind, capability, ops, quota, sync_mode (webhook/poll/none) |
| `AgentKey` | key_hash, scopes, per-platform tokens, status |

## 10. Endpoint reference

| Method | Path | Purpose |
|---|---|---|
| GET | `/.well-known/vip-rsvp.json` | Platform bootstrap |
| GET | `/c/{slug}/agent.json` | Convention hub contract |
| GET | `/c/{slug}/events.agent.json` | Paged side-event feed |
| GET | `/events/{slug}/agent.json` | Event contract |
| POST | `/api/public/events/{id}/rsvp` | Single-event RSVP (native or via adapter) |
| POST | `/api/public/events/{id}/waitlist` | Waitlist join |
| GET | `/api/public/events/{id}/availability` | Live capacity |
| POST | `/api/public/itineraries` | Plan + execute a week |
| GET | `/api/public/itineraries/{id}` | Plan status |
| POST | `/api/public/itineraries/{id}/approve` | Batch consent |
| POST | `/api/public/itineraries/{id}/cancel` | Cascade cancel |
| GET | `/api/public/itineraries/{id}/calendar.ics` | One calendar for the week |
| POST | `/api/public/rsvps/{id}/cancel?token=…` | Single cancel |
| POST | `/api/organizer/conventions/{id}/side-events` | Register an external side event |
| GET | `/api/organizer/conventions/{id}/rsvps` | Organizer cross-attendance view |

## 11. Reference flow — Ada at Code Conf 2026

```
Ada (human)                 Agent                       VIP RSVP                    Platforms
    │  "plan my week"          │                             │                         │
    │─────────────────────────>│                             │                         │
    │                          │ GET /c/code-conf-2026/agent.json                     │
    │                          │────────────────────────────>│                         │
    │                          │ GET /c/…/events.agent.json  │                         │
    │                          │────────────────────────────>│                         │
    │                          │  filter 140 → 6, check conflicts, travel times       │
    │                          │ POST /api/public/itineraries (dry_run: true)         │
    │                          │────────────────────────────>│                         │
    │                          │    plan: 3 RSVPs, 1 conflict, 2 skipped              │
    │  approve once  ─────────>│                             │                         │
    │                          │ POST /itineraries/itin_42/approve                    │
    │                          │────────────────────────────>│                         │
    │                          │                             │─ RSVP (eventbrite API) ─>│ EB
    │                          │                             │─ RSVP (luma API) ───────>│ Luma
    │                          │                             │─ open_link (partiful) ──>│ Partiful
    │  one-tap on Partiful ───>│                             │                         │
    │  calendar.ics + receipt  │                             │                         │
    │<─────────────────────────│                             │                         │
```
