PlanPilot is a tool layer, not just an app. Everything the built-in agent can do, you can do, from your own agent over MCP, or by publishing your own agent and data as content. Follow the ten-minute path from zero to your own planning agent, then try your own implementation.
No key needed for the map. The planner interface at / works without an account: type a request, read the map, share a plan link. Reads on the API are open too: GET /api/features, /api/geocode?q=, /api/layers, /api/agents, /api/plans/{id}.
A key is needed for compute and write calls (/api/clearance, /api/route, /api/points, /api/trace, /api/corridor, /api/along, /api/plans, /api/agents, /api/layers) and for MCP. No terminal? Every endpoint can be tried from the API reference page with the key pasted into Authorize.
Limits: 2000 characters per message, 60 turns per conversation, 12 tool rounds per reply, 5000 features per layer, 10 messages per minute per connection. Handles (agent and layer names) are first come, first served and can only be changed by the account that created them; pick a team prefix like team7-.
Authorization: Bearer pp_….
export URL=https://goneon.luquematte.com export KEY=pp_your_key_here
curl -s -X POST $URL/api/clearance -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"geometry":{"type":"Point","coordinates":[8.5145897,47.366881]},"layer":"sewer","min_distance_m":3}'offset_m puts the trench beside the centerline where existing services live. Crossings come back as point violations to engineer, not failures:
curl -s -X POST $URL/api/route -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"start":"Goldbrunnenstrasse","end":"Hardstrasse","mode":"roads","offset_m":2,"clearance_rules":[{"layer":"water","min_distance_m":1}]}'
# start/end take street names or addresses ("Saumstrasse 23"), resolved server-side, or [lng, lat]claude mcp add --transport http planpilot $URL/mcp/ --header "Authorization: Bearer $KEY" # then: "place trees every 15 m along Aemtlerstrasse avoiding sewer and gas by 3 m"
curl -s -X POST $URL/api/layers -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
"name": "team7-busstops",
"features": {"type":"FeatureCollection","features":[
{"type":"Feature","properties":{"name":"Stop A"},"geometry":{"type":"Point","coordinates":[8.5205,47.3715]}}
]}}'
# now {"layer":"team7-busstops","min_distance_m":5} works in any clearance rule, and /?layers=team7-busstops draws it
# delete with: curl -X DELETE $URL/api/layers/team7-busstops -H "Authorization: Bearer $KEY"write decides whether it may save plans:
curl -s -X POST $URL/api/agents -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
"name": "team7-planner",
"prompt": "You plan for team 7. Always keep 5 m from team7-busstops and explain trade-offs in one paragraph.",
"tools": ["search_features","check_clearance","propose_route","place_points","list_layers","save_plan"],
"write": true }'
Then chat with it: $URL/?agent=team7-planner&layers=team7-busstops. Re-POST the same name to publish a new version; running conversations stay pinned to the version they started on.plan_url, a read-only page anyone can open: $URL/plans/<id>.search_features: query any layer near a point, in a bbox, or by namecheck_clearance: the validator: geometry × layer × minimum distance → violations with distancespropose_route: shortest path over the street graph (or a direct polyline through waypoints), validated; avoid zones give alternativesplace_points: spaced placements of an asset (tree, lamp, bench, ramp…) along a named street at a sidewalk offset, validatedtrace_road: a lane or corridor along one named street at a lateral offset, validatedmeasure_corridor: facade-to-facade width along a street, with the narrowest spotsfeatures_along: impact analysis: everything within N m of a proposed linegeocode: “Saumstrasse 23” or a street name → a point (route endpoints take these strings directly)list_layers: built-in district data plus every participant layersave_plan / get_plan: persistence and shareable pagesDiscovery is open (GET /api/agents, GET /api/layers, GET /api/plans/{id}); creating and changing needs your key. Full reference at /docs.
What the checks do and do not prove. Every clearance is a 2D distance in meters (Swiss LV95) between a geometry and the features of a layer, sampled every 5 m along lines. It does not know depth, slope, elevation or cover; nothing here verifies a gravity sewer, and the assistant is told to say so. The clearance defaults (1 m from underground services, 3 m for trees from services, 4 m between trees) are assumptions to override per project, not standards.
The utilities are synthetic. Sewer, water and gas were seeded on the real street centerlines, so a route with a 2 m offset is clean by construction along the street and only conflicts at crossings. That demonstrates the mechanism, not the district. A real cadastre loads through the same import script, and the checks then mean what they say. Buildings, streets, addresses and trees are real OpenStreetMap data.
A worked example of what an entry looks like end to end: three tool calls and one published agent. Street lighting is a placement problem with three constraints that all live in this data: keep lamp foundations away from underground services, keep lamps out of tree crowns, and know which facades each lamp will light.
curl -s -X POST $URL/api/points -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"along_road":"Zweierstrasse","spacing_m":30,"offset_m":4,"asset":"lamp","avoid":[{"layer":"sewer","min_distance_m":1},{"layer":"water","min_distance_m":1},{"layer":"gas","min_distance_m":1},{"layer":"trees","min_distance_m":3}]}'"offset_m": -4; compare placed and rejected counts. The side with fewer rejections wins, or you mix sides per block.curl -s -X POST $URL/api/along -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"geometry": {"type":"Point","coordinates": [8.5203, 47.3711]}, "layer":"buildings", "within_m":12}'/?agent=lighting-planner:
curl -s -X POST $URL/api/agents -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
"name": "lighting-planner",
"prompt": "You plan street lighting. For a requested street: place_points with asset lamp every 30 m at a 4 m sidewalk offset, 1 m from sewer, water and gas and 3 m from trees; try both sides and keep the better one; then features_along on two sample lamps to name the facades within 12 m. Report placed, rejected with reasons, and the lit facades, then save the plan.",
"tools": ["place_points","features_along","check_clearance","search_features","list_layers","save_plan"],
"write": true }'9,436 real mapped trees. Plant a row along a street that keeps 3 m from underground services and 4 m from existing trees, then make the agent explain every rejection.
Tools: place_points, check_clearance, list_layers
Route a cable between two addresses along the road graph. Minimize crossings of water mains; treat unavoidable crossings as engineering points and list them.
Tools: propose_route (mode=roads, offset_m, avoid), search_features
Gravity sewers need continuous downhill slope, a physics rule rather than a distance rule. This is deliberately not built. It plugs in as one validator next to check_clearance (sample the line, look up elevation, check slope ≥ 0.5%), one tool definition, and an elevation layer (swissALTI3D is open data). Build the rule; bring the data.
Where it lives: app/tools.py, app/agent.py TOOL_DEFS
save_plan, and a call outside its grant is refused at execution, not asked politely in a prompt.Built as the goNEON Platform & Ecosystem Owner take-home. Demo area: Zürich Kreis 3, 4 and 5.