API for AI agents
Free, no key. JSON over HTTPS. Read only for AI agents: a session means a person will be at the place, so sessions come from people in the app.
Read
GET /api/tutor/summary?city=dc|nyc|sf[&role=learn&lang=es&level=beginner]: open sessions per place this week.{places:{id:{sessions,people,open_seats,matches}},sessions,people,matches,is_ai_counted:false}GET /api/tutor/place?city=sf&id=w162518029[&role=learn&lang=es&level=beginner]or[&role=teach&lang=es&levels=beginner,intermediate&pay=free]: one place.{name,kind,sessions:[{id,kind,host,lang,starts,ends,seats_left,match,…}],nearby,people,ai_tutor,week_options}. Handles are random words, never names.
Tutoring is one-way. kind:"offer" is a tutor’s hour: lang, levels, and pay:"free" or pay:"paid" with rate (USD per hour, the tutor’s own stated rate, from a fixed list) and arranged:"Arranged between you, not through who else?". There are no payments in who else?. kind:"request" is a learner looking for a tutor: lang, level, and pay_ok:"free_only"|"can_pay". Each has two seats: the host and one other person.
match is true for a learner when an offer is in their language and includes their level, and for a tutor when a request is in their language, at a level they teach, and (for free_only) they tutor for free. nearby lists open matching sessions at other places within 2 km. Language codes: en es fr de it pt zh ja ko ar. Levels: beginner intermediate advanced.
For learners, ai_tutor is the ✦ AI phone lesson for their language and level: {"is_ai":true,"counted":false,"badge":"AI · Not a person · Not counted","label":"Phone lesson · Spanish","phrases_label":"Beginner phrases","pay":"free","always_available":true,…}. There is one per language and level (30 in all), always available and free. Each is a scripted guided lesson that runs on the phone (phrase drills, listen and repeat with the browser’s on-device speech, a short quiz), with no AI model and no network. AI tutors are not people, never post offers, never take seats and are in no count. The app lists people first; the AI joins the end of the list only when 3 or more sessions are listed, otherwise it sits in a separate “Practice on your phone” section. There is a People / AI / Both filter. For tutors, ai_tutor is null.
Place ids, names and facts come from the static snapshot at /tutor/data/<city>.json (© OpenStreetMap contributors, ODbL 1.0), the same public libraries and cafés as Language exchange who else?. hours is the raw OSM opening_hours string or null.
Writes
POST /api/tutor/post
content-type: application/json
{"city":"sf","id":"w162518029","kind":"offer","lang":"es",
"levels":"beginner","pay":"free","day":"2026-10-31","time":"18:00",
"client_id":"my-agent-0123456789","is_ai":true}
→ 403 {"code":"ai_read_only"}
- Every write with
"is_ai": truereturns 403ai_read_only. AI agents never count as people. - People post, take, leave and cancel in the app, after confirming they are 18 or older.
dayis one of the next 7 days in the city’s time zone;timeis on the hour or half hour, 8am to 9pm, at least 15 minutes away, and inside the place’s mapped hours. We send no messages, emails or notifications. - Allowed fields only: city, id, slot, kind, role, lang, level, levels, pay, rate, day, time, adult, device, client_id, is_ai. Any other field (name, note, phone…) returns 422
no_free_textand nothing is stored. - Other refusals: 422
bad_kind,bad_lang,bad_level,bad_levels,bad_pay,bad_rate,bad_time,place_closed,adult_required,no_match,free_only; 409full,started,own_slot,already_seated,overlap,max_open; 429 when a daily limit is reached; 503tutor_offwhen the kill switch is on (reads keep working,writes_open:false).
Limits
- Per device per day: 6 posts, 12 takes, 20 leaves, 20 cancels; at most 3 upcoming posts, nothing overlapping. 80 writes per network per day, 5,000 writes per day for the app.
- Sessions disappear when they end. Rows are deleted about 2 days later.
A who else? lab experiment. Not affiliated with any library, café or school. Place data © OpenStreetMap contributors, ODbL 1.0.