# הפודקאסט של מיכאל — Docs for AI agents

> Build a listener integration for הפודקאסט של מיכאל in five minutes. No signup, no API keys, no rate-limit gymnastics.

## Quickstart

Three lines, in order, get you from zero to a real episode:

```bash
# 1. Health check
curl https://podcast.lugassy.net/status

# 2. Find an episode about something the listener cares about
curl 'https://podcast.lugassy.net/api/search?q=ai&limit=3'

# 3. Read the full transcript of the top result (replace 1 with the id)
curl https://podcast.lugassy.net/1.md
```

## Authentication

**Auth is optional.** Every endpoint is public, read-only, and CORS-open. Choose one of two modes:

### 1. Zero-auth (default)

No header, no signup. Just call the endpoints. This is the recommended path for most agents and listener-side integrations.

### 2. Public OAuth 2.1 + PKCE S256 (optional)

Agents that prefer issuing a bearer token (for per-request quotas, audit logs, or M2M / client_credentials patterns) can run an anonymous OAuth flow — no consent screen, no client secret, no signup.

**Discovery (RFC 8414 + RFC 9728):**

```bash
curl https://podcast.lugassy.net/.well-known/oauth-authorization-server   # RFC 8414 metadata
curl https://podcast.lugassy.net/.well-known/oauth-protected-resource     # RFC 9728 metadata
curl https://podcast.lugassy.net/.well-known/openid-configuration         # OIDC discovery
```

**Walkthrough — anonymous client_credentials (M2M):**

```bash
curl -X POST https://podcast.lugassy.net/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&client_id=public&scope=read:episodes search:episodes'

# Response:
# { "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "read:episodes search:episodes" }

curl -H 'Authorization: Bearer <token>' 'https://podcast.lugassy.net/api/search?q=ai'
```

**Walkthrough — authorization_code + PKCE (browser-side agents):**

```bash
# 1. Generate code_verifier + code_challenge (S256)
# 2. Redirect user (or auto-grant for public client) to:
#    https://podcast.lugassy.net/oauth/authorize?response_type=code&client_id=public&code_challenge=<S256>&code_challenge_method=S256&scope=read:episodes&redirect_uri=<your-uri>
# 3. Exchange the code:
curl -X POST https://podcast.lugassy.net/oauth/token \
  -d 'grant_type=authorization_code&code=<code>&code_verifier=<verifier>&client_id=public'
```

### Scopes

| Scope | Permission |
|---|---|
| `read:episodes` | Episode metadata, audio URLs, transcript URLs |
| `read:transcripts` | Full transcript text |
| `search:episodes` | `/api/search` and `/ask` |

All scopes are granted automatically on anonymous client_credentials. Scopes exist so agents can advertise least-privilege intent in audit logs, even when nothing is enforced server-side.

Rate limits are enforced regardless of auth (60 req/min per IP). Honor `X-RateLimit-Remaining` and `Retry-After`.

## SDK install

There's no proprietary SDK — every endpoint is plain HTTP/JSON. Use any client library:

```bash
# JavaScript / TypeScript
npm install undici          # or use built-in fetch in Node 18+

# Python
pip install httpx           # or stdlib urllib

# Ruby
gem install http

# Go
# Use net/http from stdlib
```

MCP clients (Claude.ai, ChatGPT, Cursor, Continue, Cline) connect natively — see the connector configs below.

## Code examples

### curl
```bash
# Latest episode as JSON
curl 'https://podcast.lugassy.net/?mode=agent'

# Episode in markdown (use Accept header or .md suffix)
curl https://podcast.lugassy.net/1.md
curl -H 'Accept: text/markdown' https://podcast.lugassy.net/1

# NLWeb /ask, JSON
curl -X POST -H 'Content-Type: application/json' -d '{"query":"agentic commerce"}' https://podcast.lugassy.net/ask

# NLWeb /ask, SSE streaming
curl -N -H 'Accept: text/event-stream' 'https://podcast.lugassy.net/ask?q=agentic+commerce'

# MCP initialize (Streamable HTTP)
curl -X POST -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \
  https://podcast.lugassy.net/mcp
```

### JavaScript / TypeScript
```js
// Search
const r = await fetch('https://podcast.lugassy.net/api/search?q=ai+agents&limit=5');
const { results } = await r.json();

// Latest episode card
const agent = await fetch('https://podcast.lugassy.net/?mode=agent').then(r => r.json());
console.log(agent.latestEpisode);

// MCP tool call
const mcp = await fetch('https://podcast.lugassy.net/mcp', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    jsonrpc: '2.0', id: 1,
    method: 'tools/call',
    params: { name: 'search_episodes', arguments: { query: 'ai', limit: 5 } },
  }),
}).then(r => r.json());
```

### Python
```python
import requests

# Search
r = requests.get('https://podcast.lugassy.net/api/search', params={'q': 'ai agents', 'limit': 5})
results = r.json()['results']

# NLWeb ask
r = requests.post('https://podcast.lugassy.net/ask', json={'query': 'agentic commerce'})
for ep in r.json()['results']:
    print(ep['title'], ep['url'])
```

### Claude.ai (custom MCP connector)
```
Settings → Connectors → Add custom connector
URL: https://podcast.lugassy.net/mcp
Transport: Streamable HTTP
Auth: None
```
After adding, Claude can call `search_episodes`, `get_episode`, `get_latest_episode`, `list_episodes`, and `subscribe_via_rss` directly.

### ChatGPT (custom GPT)
```
Configure → Actions → Import from URL
URL: https://podcast.lugassy.net/.well-known/openapi.json
Auth: None
```
Or import the OpenAI plugin manifest at `https://podcast.lugassy.net/.well-known/ai-plugin.json`.

### Cursor (MCP)
```json
{
  "mcpServers": {
    "הפודקאסט-של-מיכאל": {
      "url": "https://podcast.lugassy.net/mcp",
      "transport": "streamable-http"
    }
  }
}
```

## API reference

| Endpoint | Method | Description |
|---|---|---|
| `/api/search?q=&limit=` | GET | Ranked full-text search over title + description + transcript |
| `/ask` | POST | NLWeb-style natural-language ask. JSON or SSE (`Accept: text/event-stream`) |
| `/ask?q=` | GET | Same as POST /ask but query-string |
| `/mcp` | POST | MCP JSON-RPC (Streamable HTTP). Methods: initialize, ping, tools/list, tools/call |
| `/mcp` | GET | MCP server manifest |
| `/.well-known/mcp` | GET/POST | MCP discovery + live handshake (same JSON-RPC handler) |
| `/.well-known/mcp/server-card.json` | GET | Preview-able server card (name, version, tools[]) |
| `/status` | GET | Service health for circuit-breaker logic |
| `/episodes.json` | GET | Full episode list with metadata |
| `/search-index.json` | GET | Flat search index for offline indexing |
| `/<id>` | GET | Episode HTML page (SSR'd, JS-free) |
| `/<id>.md` | GET | Episode in markdown (or `Accept: text/markdown`) |
| `/<id>?mode=agent` | GET | Episode as compact agent JSON |
| `/?mode=agent` | GET | Homepage as agent JSON (capabilities + endpoints + latest episode) |
| `/index.md` | GET | Homepage as markdown |
| `/AGENTS.md` | GET | This deployment's AGENTS.md |
| `/llms.txt`, `/episodes/llms.txt`, `/api/llms.txt`, `/.well-known/llms.txt` | GET | Section-scoped llms.txt files |
| `/.well-known/openapi.json` | GET | OpenAPI 3.1 spec |
| `/.well-known/agent.json` | GET | Agent capability declaration (schemaVersion 1.0) |
| `/.well-known/agent-card.json` | GET | A2A-style skill card |
| `/.well-known/agent-skills/index.json` | GET | agentskills.io v0.2.0 index |
| `/.well-known/ai-plugin.json` | GET | OpenAI plugin manifest |
| `/.well-known/schema-map.xml` | GET | NLWeb pointer to all structured feeds |
| `/rss.xml` | GET | RSS 2.0 feed |

Full typed schema for every operation is in [`https://podcast.lugassy.net/.well-known/openapi.json`](https://podcast.lugassy.net/.well-known/openapi.json).

## Errors

Every error is a structured JSON envelope:
```json
{
  "error": {
    "code": "episode_not_found",
    "message": "We don't have an episode #999 on this show.",
    "hint": "/episodes.json — full catalog with valid IDs",
    "docs_url": "/api/llms.txt"
  }
}
```

| Status | Code examples | When |
|---|---|---|
| 400 | `missing_query`, `bad_limit`, `bad_body` | Bad input |
| 402 | `payment_required` | Only at `/donate` — voluntary tip jar with x402/MPP headers |
| 404 | `episode_not_found` | Episode ID doesn't exist |
| 405 | `method_not_allowed` | Wrong HTTP method |
| 429 | `rate_limited` | Over 60 req/min/IP |
| 500 | `internal_error` | Something broke server-side |

## Rate limits

- **60 requests/minute per IP** across every endpoint listed above.
- Headers on every API response: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (Unix seconds).
- 429 responses also carry `Retry-After` (seconds).
- Self-throttle on those headers — don't backoff blindly.

## Optional: tip jar (x402 / MPP)

Voluntary support — `POST https://podcast.lugassy.net/donate` returns HTTP 402 with x402 + MPP payment headers pointing at a USDC address on Base Sepolia (configurable via `podcast.yaml`). Payment-aware agents (Coinbase x402, MPP-enabled clients) can route a tip without authenticating. The free read API never returns 402.

```bash
curl -i -X POST https://podcast.lugassy.net/donate
# HTTP/1.1 402 Payment Required
# WWW-Authenticate: Payment realm="https://podcast.lugassy.net/donate", network="base-sepolia", asset="USDC"
# PAYMENT-REQUIRED: x402
# X-Payment-Required: { "x402Version": 1, "accepts": [...] }
```

Discovery files:

- `https://podcast.lugassy.net/.well-known/x402/supported` — x402 facilitator manifest
- `https://podcast.lugassy.net/.well-known/discovery/resources` — x402 Bazaar resources

## More

- Listener-agent integration guide: [`https://podcast.lugassy.net/AGENTS.md`](https://podcast.lugassy.net/AGENTS.md)
- Show briefing: [`https://podcast.lugassy.net/llms.txt`](https://podcast.lugassy.net/llms.txt)
- API briefing: [`https://podcast.lugassy.net/api/llms.txt`](https://podcast.lugassy.net/api/llms.txt)
- Coil source (the platform that generated this site): https://github.com/mluggy/coil
