OwnTracks Location Receiver
Give your AI agent GPS awareness. Zero dependencies, one Node.js file.
Architecture
OwnTracks app → (HTTPS via Cloudflare Tunnel) → server.mjs → data/
├── current-location.json
└── location-log.jsonl
Setup
1. Start the receiver
AUTH_USER=yi AUTH_SECRET=$(openssl rand -base64 24) node scripts/server.mjs
# Default port 8073. Override with PORT=9090
Without AUTH_SECRET, the server runs open (fine for localhost, not for public URLs).
2. Expose via tunnel (if behind NAT)
Pick one:
| ngrok | Cloudflare Tunnel | |
|---|---|---|
| Setup | One command | Needs domain + DNS config |
| URL | Random subdomain (changes on restart) | Fixed custom domain |
| Best for | Quick testing, no domain | Production, permanent setup |
| China | Works but unstable | Use --protocol http2 (QUIC blocked) |
Option A: ngrok (quick start)
ngrok http 8073
# Copy the https://xxx.ngrok-free.app URL to OwnTracks
Option B: Cloudflare Tunnel (permanent)
# One-time setup
brew install cloudflared
cloudflared tunnel login
cloudflared tunnel create owntracks
cloudflared tunnel route dns owntracks gps.yourdomain.com
# Create ~/.cloudflared/config.yml:
# tunnel: <tunnel-id>
# credentials-file: ~/.cloudflared/<tunnel-id>.json
# ingress:
# - hostname: gps.yourdomain.com
# service: http://localhost:8073
# - service: http_status:404
# Run (use --protocol http2 in China where QUIC is blocked)
cloudflared tunnel run owntracks
Tip: set up as a launchd service for auto-start on boot.
3. Configure OwnTracks app
- Mode: HTTP
- URL: Your tunnel URL (ngrok URL or
https://gps.yourdomain.com/) - Authentication: Username + password (matching AUTH_USER / AUTH_SECRET)
- Monitoring: Significant (battery-friendly) or Move (precise tracking)
⚠️ OwnTracks may add trailing spaces when pasting passwords. The server trims automatically.
Named Places
Copy and edit the example:
cp scripts/places.example.json places.json
# Edit places.json with your locations
Format:
{
"places": [
{ "name": "home", "label": "Home", "lat": 37.775, "lon": -122.419, "radius": 200 },
{ "name": "office", "label": "Office", "lat": 37.790, "lon": -122.389, "radius": 300 }
]
}
With places configured, queries return semantic locations ("At 家") instead of raw coordinates.
Querying Location
# Current location + nearest place
node scripts/query.mjs
# JSON output (for programmatic use)
node scripts/query.mjs --json
# Last N locations
node scripts/query.mjs --history 5
# Locations from a specific date
node scripts/query.mjs --at 2026-03-10
# List configured places
node scripts/query.mjs --places
Direct file access
cat data/current-location.json # latest location
tail -5 data/location-log.jsonl # recent history
Location fields
lat, lon, alt, acc (meters), vel (km/h), batt (%), conn (w=wifi, m=mobile), tid, tst (epoch), timestamp (ISO), receivedAt (ISO).
Use Cases for Agents
- Context awareness: "You're still at the office at 11pm — time to head home?"
- Proactive help: Detect arrival at airport → check flight status
- Smart home: Detect "at home" → trigger automations
- Daily journaling: Log places visited with timestamps
- Safety: Alert if no location update for extended period
Nearby Search (AMap/高德)
Requires AMAP_API_KEY env var. Get a free key at https://lbs.amap.com
Coordinate System (China users)
AMap requires GCJ-02 coordinates. iOS CoreLocation returns WGS-84 (the GPS standard), and the nearby.mjs script includes a built-in WGS-84 → GCJ-02 converter (default: COORD_SYSTEM=wgs84). If your device already provides GCJ-02, set COORD_SYSTEM=gcj02 to skip conversion:
# iOS / standard GPS devices (default, auto-converts WGS→GCJ)
node scripts/nearby.mjs "咖啡"
# If your device already reports GCJ-02
COORD_SYSTEM=gcj02 node scripts/nearby.mjs "咖啡"
# Auto-uses current GPS location
node scripts/nearby.mjs "咖啡"
# Custom radius and explicit coordinates
node scripts/nearby.mjs "餐厅" --radius 1000 --lat 37.775 --lng -122.419
# JSON output for programmatic use
node scripts/nearby.mjs "药店" --json
# Limit results
node scripts/nearby.mjs "便利店" --limit 5
Combines with GPS: if OwnTracks is running, nearby search auto-detects your location. No need to pass coordinates.
Use cases: restaurants, cafes, pharmacies, hospitals, transit, gyms — anything in AMap's POI database (excellent coverage in China).
Files
scripts/server.mjs— HTTP receiver (zero deps)scripts/query.mjs— CLI query tool (zero deps)scripts/nearby.mjs— AMap nearby POI search (zero deps, needs AMAP_API_KEY)scripts/gps-inject.sh— Injects GPS status into HEARTBEAT.md (for agent context)scripts/places.example.json— Example places configplaces.json— Your places config (create from example, gitignored)data/— Location data (created automatically, gitignored)
Heartbeat Integration (Agent GPS Awareness)
The key challenge: an agent's heartbeat reads HEARTBEAT.md, but may skip instructions inside it. The solution is code-level injection — a shell script writes GPS status directly into HEARTBEAT.md so the data is already in the agent's system prompt.
How it works
launchd (every 2 min) → gps-inject.sh → reads GPS data → writes to HEARTBEAT.md header
↓
Agent heartbeat reads HEARTBEAT.md
GPS data is already in system prompt
Agent can't "forget" to check GPS
Setup
-
Copy and edit
scripts/gps-inject.sh:- Set
GPS_FILEto yourdata/current-location.jsonpath - Set
HEARTBEATto your agent'sHEARTBEAT.mdpath - Create a
known-places.jsonwith your named locations (see format below)
- Set
-
Create a launchd plist (macOS) or cron entry:
<!-- ~/Library/LaunchAgents/com.agent.gps-inject.plist -->
<plist version="1.0">
<dict>
<key>Label</key><string>com.agent.gps-inject</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/path/to/scripts/gps-inject.sh</string>
</array>
<key>StartInterval</key><integer>120</integer>
<key>RunAtLoad</key><true/>
</dict>
</plist>
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.agent.gps-inject.plist
- Your HEARTBEAT.md will automatically show:
<!-- GPS_START -->
> 📍 User at Home | Updated 09:53 (1 min ago) | 🔋85% WiFi | Accuracy 7m
<!-- GPS_END -->
Known places format
[
{"name": "Home", "lat": 40.033, "lon": 116.417, "radius": 0.003},
{"name": "Office", "lat": 40.052, "lon": 116.294, "radius": 0.003}
]
Use coordinates from your actual GPS data (same coordinate system). For unknown locations, the script falls back to OSM Nominatim reverse geocoding (WGS-84, free, no API key).
Why this pattern matters
Text-level instructions ("check GPS every heartbeat") are unreliable — agents read them but may not execute. Code-level injection bypasses the agent's decision-making entirely: the data is in the prompt whether the agent "remembers" to check or not. This is a general pattern: if an agent must see something, inject it into context; don't ask the agent to go look.