Developers
Build on GeoAgents
Put AI agents at real places in your own app. GeoAgents handles the hard parts: location triggers that cope with noisy GPS, an agent runtime that knows where the visitor is, visitor memory, request caps, voice, and a lazy-walk mode for exploring from a laptop. The GeoAgents app itself is built only on these public pieces.
Concepts
- Anchor: where an agent lives. A
circle(lat, lng, radius), apolygon, aroutecorridor, or an indoorcode(QR / beacon). Any anchor can addelevationandfloor. - Agent: a persona, goal, rules, knowledge, scripted lines and voice, pinned to an anchor.
- Experience: a set of agents.
order: "any"for collections,"sequence"for tours and hunts where each stop unlocks the next. - Session: one visitor's walk, in
gps(on site) orlazy(remote) mode. Positions go in;enter,dwellandexitevents come out, with greetings, farewells and progress.
Quickstart
No build step needed. Import the SDK as an ES module (26 KB):
<script type="module">
import { GeoAgents, lazyWalk, Speaker } from "https://geoagents.chinmayasahu.com/sdk/v1.js";
const ga = new GeoAgents({ publishableKey: "ga_pub_…" }); // key optional
const voice = new Speaker();
// Explore Cubbon Park, Bengaluru from the keyboard (W A S D, Q/E to turn)
const walker = lazyWalk({ lat: 12.9769, lng: 77.5936 });
walker.bindKeyboard();
const session = await ga.startSession({ location: walker });
session.on("enter", async (e) => {
console.log(`${e.agent.name} woke up`);
if (e.greeting) await voice.speak(e.greeting, { lang: e.agent.language });
const reply = await session.chat(e.agentId, "What's the story of this place?", {
onDelta: (text) => console.log(text), // streams in
});
voice.speak(reply.text, { lang: e.agent.language });
});
session.on("exit", (e) => e.farewell && voice.speak(e.farewell));
</script>Call voice.unlock() (and start the session) from a click or tap: browsers only allow speech and microphone access after a user gesture.
Location: GPS & lazy walk
A session takes any location source:
import { gps, lazyWalk, simulated, requestCompass } from "https://geoagents.chinmayasahu.com/sdk/v1.js";
// On site: real GPS + compass (iOS needs requestCompass() from a tap)
await requestCompass();
const session = await ga.startSession({ location: gps(), experienceId: "old-pete" });
// Remote: lazy walk
const walker = lazyWalk({ lat: 12.9629, lng: 77.5758, heading: 0 });
walker.setSpeed("cycle"); // walk | jog | cycle | scooter, or m/s
walker.setInput({ forward: 1, turn: 0 }); // joystick / gamepad
await walker.walkTo({ lat: 12.9594, lng: 77.5736 }); // follows streets
walker.teleport({ lat: 12.9716, lng: 77.5946 });
walker.onMove((s) => drawMe(s.lat, s.lng, s.heading));
// Tests and demos: replay a recorded or simulated track
const session2 = await ga.startSession({ location: simulated(positions, 10 /* × speed */) });The session runs the trigger engine on the device, so it contacts the server the moment you cross an edge, and batches positions otherwise. The server re-runs the same engine and is the source of truth. GPS triggers need two readings to enter when accuracy is poor, and a margin plus ~8 seconds to leave, so agents don't flicker when you stand at the edge. Lazy-walk positions are exact and fire instantly. Lazy walkers never see agents marked remote: "onsite_only".
Talking & voice
// Text in, streaming text out
const reply = await session.chat(agentId, "Who built this?", {
language: "hi-IN", // optional: answer in Hindi
onStart: ({ fallback, notice }) => {}, // fallback = scripted reply (AI unavailable / capped)
onDelta: (text) => {},
});
// Free on-device speech, sentence by sentence while the reply streams
const voice = new Speaker();
const stream = voice.stream({ lang: "en-IN", rate: 1, pitch: 1 });
await session.chat(agentId, question, { onDelta: (t) => stream.push(t) });
stream.end();
await stream.done;
// Microphone → text (Whisper). Stops automatically when the visitor goes quiet.
const rec = await new Recorder().record({ onLevel: (l) => meter(l) });
if (rec) {
const { text, language } = await ga.transcribe(rec.blob, { prompt: "Veera, Bangalore Fort" });
await session.chat(agentId, text);
}Agents know whether the visitor is on site or remote, how far away and in which direction they are (relative to the way they're facing), the local time and weather, the tour progress, the nearest other agents, and a short memory of past conversations with this visitor.
Events
| Event | Payload |
|---|---|
enter | Agent woke up. greeting (null if it greeted this visitor recently), locked + hint in sequences, asleep outside active hours, visited, unlocked, completed. |
dwell | Visitor lingered for the agent's dwellSeconds. |
exit | Visitor walked away. farewell. |
nearby | Agents within 2 km, nearest first (refreshed every ~300 m). |
awake · progress · status · position · error | Awake agent ids · tour progress · GPS status (waiting, ok, poor, denied) · every position · sync errors. |
Keys & visitors
- Visitors are anonymous.
POST /api/v1/visitorsreturns a token; send it asX-GeoAgents-Visitor. The SDK keeps it inlocalStorage, so agents remember people across visits. - Publishable keys (
ga_pub_…) go in browser apps asX-GeoAgents-Keyand attribute visitors to your app. - Secret keys (
ga_sk_…) manage your experiences from a server:Authorization: Bearer ga_sk_…. Never ship them to browsers.
Create keys in the studio. The visitor API is open to any origin (CORS).
REST API
| Endpoint | Auth | What it does |
|---|---|---|
| POST /api/v1/visitors | — | Create an anonymous visitor → { token } |
| GET /api/v1/nearby?lat&lng&radius&mode | — | Agents near a point (public experiences + ?experience=) |
| GET /api/v1/featured | — | Public experiences |
| GET /api/v1/experiences/:id | — | Experience + agents (owner also gets full definitions) |
| POST /api/v1/sessions | visitor | Start a walk { mode: gps|lazy, experienceId? } |
| POST /api/v1/sessions/:id/location | visitor | { positions: [...], nearby? } → { events, inside, progress, nearby? } |
| POST /api/v1/sessions/:id/chat | visitor | { agentId, text, language?, position? } → NDJSON stream |
| POST /api/v1/transcribe | visitor | multipart audio (+ language, prompt) → { text, language } |
| GET /api/v1/status | — | AI availability and today's usage against caps |
| GET · POST /api/v1/experiences | secret | List / create your experiences |
| PATCH · DELETE /api/v1/experiences/:id | secret | Update (incl. visibility) / delete |
| POST /api/v1/experiences/:id/agents | secret | Add an agent (Agent JSON) |
| PUT /api/v1/experiences/:id/order | secret | { agentIds } stop order |
| PATCH · DELETE /api/v1/agents/:id | secret | Partial update / delete an agent |
| POST /api/v1/agents/:id/test-chat | secret | Talk to an agent without a session (nothing stored) |
| GET /api/v1/experiences/:id/export | secret | Download as a geoagents/v1 document |
| POST /api/v1/import | secret | Create an experience from a document |
Chat streams newline-delimited JSON: {"type":"start","fallback":false}, then {"type":"delta","text":"…"}…, then {"type":"done","text":"…","fallback":false}. Errors are { "error": "…", "code": "…" } with a readable message.
Open format
Everything in GeoAgents is a plain JSON document you can export, version and import. The JSON Schema lives at /spec/v1.json.
{
"spec": "geoagents/v1",
"experience": {
"title": "Old Pete: where Bengaluru began",
"kind": "tour",
"order": "sequence",
"agents": [
{
"name": "Veera",
"role": "Fort sentinel",
"place": "Delhi Gate, Bangalore Fort",
"avatar": { "emoji": "🛡️", "color": "#e76f51" },
"anchor": { "type": "circle", "lat": 12.96294, "lng": 77.57576, "radius": 35 },
"persona": "The proud, theatrical spirit of a sentinel…",
"goal": "Make the visitor feel the 1791 siege…",
"knowledge": "# Overview\nBangalore Fort began in 1537…",
"greeting": "Halt! Who approaches the Delhi Gate? {time_of_day}!",
"farewell": "Go well, traveller.",
"backupLines": ["This gate has seen…"],
"language": "en-IN",
"remote": "allowed",
"triggers": { "dwellSeconds": 20, "cooldownMinutes": 45 }
}
]
}
}Creating content from code
curl -X POST https://geoagents.chinmayasahu.com/api/v1/experiences \
-H "Authorization: Bearer ga_sk_…" -H "Content-Type: application/json" \
-d '{"title":"Campus helpers","order":"any"}'
curl -X POST https://geoagents.chinmayasahu.com/api/v1/experiences/EXP_ID/agents \
-H "Authorization: Bearer ga_sk_…" -H "Content-Type: application/json" \
-d '{"name":"Hall C helper","anchor":{"type":"circle","lat":12.97,"lng":77.59,"radius":40},
"persona":"Calm and efficient.","knowledge":"# Schedule\n10:00 Keynote in Hall A…"}'
curl -X PATCH https://geoagents.chinmayasahu.com/api/v1/experiences/EXP_ID \
-H "Authorization: Bearer ga_sk_…" -H "Content-Type: application/json" -d '{"visibility":"unlisted"}'Embed
The full walk experience (map, lazy walk, GPS, voice) in an iframe:
<iframe src="https://geoagents.chinmayasahu.com/walk?exp=EXP_ID&embed=1"
allow="geolocation; microphone; screen-wake-lock"
style="width:100%;height:640px;border:0;border-radius:16px"></iframe>URL options: exp, agent (start next to an agent), at=lat,lng[,heading], mode=lazy|gps, embed=1.
Limits
AI requests are capped so the platform stays free. When a cap is reached, agents answer with their scripted backup lines or the best-matching line of their knowledge, and the reply is marked fallback: true. Current caps: 1,500 AI requests per day platform-wide, 80 per visitor per day on site (40 when lazy-walking), 12 per minute, and 400 per agent per day. Check GET /api/v1/status.
Coming next: webhooks, analytics, native iOS/Android SDKs, beacons, 360° scenes and hide-and-seek games.