v1.2.0 Node.js 18+ MIT / OGDL OpenClaw Skill

๐ŸšŒ Taipei Bus
Fixed-Point Vehicle OD Skill

Integrates Taipei City fixed-point vehicle OD real-time API with PTX open data. Provides real-time bus position tracking, stop-sequence-based ETA estimation, route search, stop sequence lookup, anomaly detection, hotspot analysis, and map visualization.

Introduction

๐Ÿ“– What Can This Skill Do?

taipei-bus is a bus dynamic query skill built for OpenClaw Agents. It integrates three core data sources, focused on solving the most common question: "When will the bus arrive?"

๐Ÿ“ก
Real-time Vehicle Data
Taipei City TCG Bus TstBusEvent API
Each record: plate, route, stop, direction
๐Ÿ—บ๏ธ
Stop Coordinates
PTX Bus Stop API
28,741 stops, locally cached
๐ŸšŒ
Route & Stop Sequences
PTX StopOfRoute API
415 routes, outbound & return sequences
โฑ๏ธ
3-Tier ETA Accuracy
Sequence โ˜…โ˜…โ˜…โ˜…โ˜… / Haversine โ˜…โ˜…โ˜… / Fallback โ˜…
โš ๏ธImportant: Two Independent ID Systems

TstBusEvent RouteID (e.g., 158701) and PTX RouteID (e.g., 10132, corresponding to Route 234) are completely independent systems. They do not share IDs. Use stop name or route name search to establish cross-references.

Installation

๐Ÿ“ฆ How to Install

Two installation methods are supported. Method 1 (SkillHub) is recommended and handles all dependencies automatically.

Method 1: SkillHub Install (Recommended)

Terminal
skillhub_install install_skill taipei-bus

Method 2: Manual Copy

Manually copy the lib/ and data/ directories to your project:

Terminal
# Copy lib/ (bus-api.js + route-api.js + stop-coords.js)
cp -r ~/.qclaw/skills/taipei-bus/lib ./taipei-bus-lib

# Copy cached data (28,741 stop coords + 415 route sequences)
cp -r ~/.qclaw/skills/taipei-bus/data ./taipei-bus-data

# Or copy the entire skill
cp -r ~/.qclaw/skills/taipei-bus /your/project/
๐Ÿ’ก Node.js 18+ required: This skill uses the native Web Standard fetch API, which requires Node.js 18 or higher. For older versions, install node-fetch as a polyfill.
Quick Start

โšก Your First Bus Query

Prerequisites

  • Node.js 18.0+ (with native fetch)
  • Skill installed with lib/ and data/ directories copied
  • Network access (to PTX API + TCG Bus API)

Basic Query: When will Route 234 arrive?

quickstart.js
const { searchRouteByName, etaBySequence, getRouteStopSequence } = require('./lib/bus-api.js');

async function main() {
  // Step 1: Search by route name (numbers, trunk names, colors all work)
  const route = searchRouteByName('234');
  if (!route) {
    console.log('Route not found');
    return;
  }
  console.log(`Found: ${route.routeName} (RouteID: ${route.routeId})`);

  // Step 2: Get outbound stop sequence
  const stops = getRouteStopSequence(route.routeId, 0); // 0=outbound, 1=return
  console.log(`Total stops: ${stops.length}`);

  // Step 3: Find target stop (by partial name match)
  const target = stops.find(s => s.stopName.includes('Huwanzayuan'));
  if (!target) {
    console.log('Target stop not found in sequence');
    return;
  }

  // Step 4: Estimate arrival by stop sequence (most accurate)
  const eta = etaBySequence(route.routeId, target.stopId, 0);
  console.log(`\n๐ŸšŒ ${route.routeName} โ†’ ${target.stopName}`);
  console.log(`โฑ๏ธ  ETA: ~${eta.minutes} minutes`);
  console.log(`๐Ÿ“Œ ${eta.note}`);
}

main().catch(console.error);

Run It

Terminal
node quickstart.js
# Output:
# Found: 234 (RouteID: 10132)
# Total stops: 40
# ๐ŸšŒ 234 โ†’ Huwanzayuan
# โฑ๏ธ  ETA: ~8 minutes
# ๐Ÿ“Œ By sequence (stop 12/40), 5 stops to target (outbound)
Triggers

๐Ÿ”” AI Agent Trigger Keywords

Mentioning these keywords in an OpenClaw Agent chat activates this skill

Category Keywords Description
Dynamic ๅ…ฌ่ปŠๅ‹•ๆ…‹/bus dynamic, ๅ…ฌ่ปŠๅคšไน…/bus ETA, ็ญ‰ๅ…ฌ่ปŠ, ๅ…ฌ่ปŠๅˆฐ็ซ™ Query bus arrival time
Tracking ๅ…ฌ่ปŠ่ฟฝ่นค, ๅ…ฌ่ปŠไพ†ไบ†, ๅ…ฌ่ปŠ่ฟฝ่นค Track a specific bus
Map ๅ…ฌ่ปŠๅœฐๅœ–, ๅ…ฌ่ปŠ็ซ™็‰Œ, ๅ…ฌ่ปŠ็†ฑ้ปž, bus map Map visualization or stop lookup
Route ๅ…ฌ่ปŠ่ทฏ็ทš, ๅ…ฌ่ปŠ้ฆ–็ญ, ๅ…ฌ่ปŠๆœซ็ญ, bus route Route info or search
Anomaly ๅ…ฌ่ปŠ่„ซ็ญ, ๅ…ฌ่ปŠ็•ฐๅธธ, bus anomaly Anomaly or skip detection
Mixed Taipei bus, ๅฐๅŒ—ๅ…ฌ่ปŠ, ่‡บๅŒ—ๅธ‚ๅ…ฌ่ปŠ Mixed-language triggers
Data Sources

๐Ÿ“ก Four API Data Sources

Source URL / Endpoint Contents Cache Strategy
Live
TstBusEvent
tcgbusfs.blob.core.windows.net/blobbus/TstBusEvent.json Plate, RouteID, StopID, direction, DataTime, DutyStatus, BusStatus None (live fetch each time)
PTX Stop
Stop coords
ptx.transportdata.tw/MOTC/v2/Bus/Stop/City/Taipei 28,741 stops โ€” name, lat/lon, StationID data/stop-coords.json (~2 MB, quarterly rebuild)
PTX Route
Route list
ptx.transportdata.tw/MOTC/v2/Bus/Route/City/Taipei 415 routes โ€” name, departure/terminal, first/last bus, operator data/route-full-cache.json (~11 MB, quarterly rebuild)
PTX StopOfRoute
Stop sequences
ptx.transportdata.tw/MOTC/v2/Bus/StopOfRoute/City/Taipei Outbound/return stop sequences per route with StopIDs and coordinates data/route-full-cache.json (~11 MB, quarterly rebuild)
๐Ÿ”ง Cache rebuild: Caches are generated by scripts/build-cache.py. To update PTX data, run: python3 ~/.qclaw/skills/taipei-bus/scripts/build-cache.py
API Reference

โš™๏ธ Complete Function Reference

All functions are imported via require('./lib/bus-api.js')

๐Ÿ“ก Real-time Vehicle Functions (bus-api.js)

fetchAll()
Fetch all real-time bus data (TstBusEvent OD)
bus-api async โ–ผ
Returns๏ผšPromise<BusRecord[]> โ€” sorted by DataTime descending
javascript
const { fetchAll } = require('./lib/bus-api.js');

const buses = await fetchAll();
console.log(`Total buses online: ${buses.length}`);
buses.slice(0, 3).forEach(b => console.log(b.BusID, b.RouteID, b.StopID));
findByRoute(routeId)
Query all online buses by TstBusEvent RouteID
bus-api async โ–ผ
Returns๏ผšPromise<BusRecord[]>
ParamTypeDescription
routeIdstringTstBusEvent RouteID (e.g., '158701') โš ๏ธ Not a PTX RouteID
โš ๏ธThis routeId uses the TstBusEvent system (e.g., 158701), NOT the PTX RouteID (e.g., 10132). Use searchRouteByName to confirm first.
javascript
const { findByRoute } = require('./lib/bus-api.js');

const buses = await findByRoute('158701');
console.log(`Route: ${buses.length} buses online`);
buses.forEach(b => console.log(`${b.BusID} at ${b.StopID}`));
findByBus(busId)
Query a single bus by plate number
bus-api async โ–ผ
Returns๏ผšPromise<BusRecord | null>
javascript
const { findByBus, formatBus } = require('./lib/bus-api.js');

const bus = await findByBus('KKC-1100');
if (bus) {
  console.log(formatBus(bus, true));
} else {
  console.log('Bus not found or offline');
}
findAtStation(stopId)
Find buses currently at a specific stop (CarOnStop === '1')
bus-api async โ–ผ
Returns๏ผšPromise<BusRecord[]>
javascript
const { findAtStation } = require('./lib/bus-api.js');

const buses = await findAtStation('179647');
console.log(`${buses.length} buses at stop`);
buses.forEach(b => console.log(b.BusID, b.RouteID));
detectAnomalies()
Detect buses with DutyStatus !== '1' or BusStatus !== '0'
bus-api async โ–ผ
Returns๏ผšPromise<BusRecord[]> โ€” empty = all normal
javascript
const { detectAnomalies, formatBus } = require('./lib/bus-api.js');

const anomalies = await detectAnomalies();
if (anomalies.length === 0) {
  console.log('โœ… All systems normal');
} else {
  console.log(`โš ๏ธ ${anomalies.length} anomalies`);
  anomalies.forEach(b => console.log(formatBus(b)));
}
routeSummary()
System-wide route overview (bus count per route)
bus-api async โ–ผ
Returns๏ผšPromise<{[routeId]: {count, buses[]}}>
javascript
const { routeSummary } = require('./lib/bus-api.js');

const summary = await routeSummary();
const top5 = Object.entries(summary)
  .sort(([,a],[,b]) => b.count - a.count)
  .slice(0, 5);

top5.forEach(([routeId, {count}]) => {
  console.log(`Route ${routeId}: ${count} buses`);
});

๐Ÿ—บ๏ธ PTX Coordinate Functions (bus-api.js + stop-coords.js)

fetchAllWithCoords()
Fetch all buses with stop coordinates attached (lat/lon/name)
bus-api async โ–ผ
Returns๏ผšPromise<EnrichedBusRecord[]> โ€” each record has _stopCoords field
javascript
const { fetchAllWithCoords } = require('./lib/bus-api.js');

const buses = await fetchAllWithCoords();
const first = buses[0];
console.log(first.BusID, first.RouteID);
console.log('๐Ÿ“', first._stopCoords?.name,
  `(${first._stopCoords?.lat?.toFixed(5)}, ${first._stopCoords?.lon?.toFixed(5)})`);
getHotspots(topN)
Get top-N busiest stops (highest bus density)
bus-api async โ–ผ
Returns๏ผšPromise<Array<{stopId, stopName, lat, lon, count}>>
javascript
const { getHotspots } = require('./lib/bus-api.js');

const hotspots = await getHotspots(5);
hotspots.forEach((h, i) => {
  console.log(`${i+1}. ${h.stopName} โ€” ${h.count} buses`);
  console.log(`   ๐Ÿ“ (${h.lat.toFixed(5)}, ${h.lon.toFixed(5)})`);
});
getStopCoords(stopId)
Get stop coordinates by PTX StopID (sync, locally cached)
stop-coords sync โ–ผ
Returns๏ผš{name, lat, lon, stationId} | null
javascript
const { getStopCoords } = require('./lib/bus-api.js');

const coords = getStopCoords('33210');
if (coords) {
  console.log(coords.name); // "Gongguan"
  console.log(coords.lat, coords.lon);
}
searchStops(keyword, limit)
Fuzzy search stops by name (sync, locally cached)
stop-coords sync โ–ผ
Returns๏ผšArray<{stopId, name, lat, lon...}>
javascript
const { searchStops } = require('./lib/bus-api.js');

const hits = searchStops('MRT', 5);
hits.forEach(s => {
  console.log(`${s.stopId}: ${s.name}`);
  console.log(`  ๐Ÿ“ (${s.lat?.toFixed(5)}, ${s.lon?.toFixed(5)})`);
});

๐ŸšŒ PTX Route Functions (bus-api.js + route-api.js)

searchRouteByName(name)
Search bus routes by name (exact + fuzzy match)
route-api sync โ–ผ
Returns๏ผšRouteInfo | null โ€” includes routeId, routeName, departure, terminal
javascript
const { searchRouteByName } = require('./lib/bus-api.js');

const route = searchRouteByName('Blue10');
// also works: '่—10', '10', etc.
if (route) {
  console.log(`${route.routeName} (${route.routeId})`);
  console.log(`${route.departure} โ†’ ${route.terminal}`);
}
getRouteInfo(routeId)
Get full route info by PTX RouteID (first/last bus, operator)
route-api sync โ–ผ
Returns๏ผšRouteInfo | null
javascript
const { getRouteInfo, routeApi } = require('./lib/bus-api.js');

const route = getRouteInfo('10132'); // Route 234
console.log(routeApi.formatRoute(route));
searchRoutes(keyword, limit)
Fuzzy search all matching routes (returns array)
route-api sync โ–ผ
Returns๏ผšRouteInfo[]
javascript
const { searchRoutes } = require('./lib/bus-api.js');

const routes = searchRoutes('Blue', 5);
routes.forEach(r => console.log(`${r.routeName}: ${r.departure} โ†’ ${r.terminal}`));
getRouteStopSequence(routeId, direction)
Get all stops for a route (outbound or return)
route-api sync โ–ผ
Returns๏ผšStopEntry[] โ€” includes stopId, stopName, lat, lon
ParamTypeDefaultDescription
routeIdstringโ€”PTX RouteID
directionnumber00=outbound, 1=return
javascript
const { getRouteStopSequence } = require('./lib/bus-api.js');

const stops = getRouteStopSequence('10132', 0); // Route 234 outbound
stops.forEach((s, i) => {
  console.log(`${i+1}. ${s.stopName} (${s.stopId})`);
});
etaBySequence(routeId, targetStopId, direction, avgSecPerStop)
Estimate arrival by stop sequence (most accurate method)
route-api sync โ–ผ
Returns๏ผš{minutes, remaining, total, stop, note}
ParamTypeDefaultDescription
routeIdstringโ€”PTX RouteID
targetStopIdstringโ€”Target PTX StopID
directionnumber00=outbound, 1=return
avgSecPerStopnumber90Avg seconds per stop (default: 1.5min urban)
javascript
const { etaBySequence } = require('./lib/bus-api.js');

const eta = etaBySequence('10132', '33210', 0);
console.log(`ETA: ${eta.minutes} minutes (${eta.note})`);
console.log(`${eta.remaining}/${eta.total} stops remaining`);
getRoutesByStop(stopId)
Find all routes that pass through a specific stop
route-api sync โ–ผ
Returns๏ผš{routeId, routeName, direction, index}[]
javascript
const { getRoutesByStop } = require('./lib/bus-api.js');

const routes = getRoutesByStop('33210');
routes.forEach(r => {
  const dir = r.direction === 0 ? 'Outbound' : 'Return';
  console.log(`${r.routeName} (${dir}) stop #${r.index}`);
});

โฑ๏ธ Three-Tier ETA Accuracy System

Tier Method Accuracy When to Use
โ˜…โ˜…โ˜…โ˜…โ˜…
Sequence
remaining stops ร— 90s ยฑ1-2 stops Known bus position + complete sequence (etaBySequence)
โ˜…โ˜…โ˜…
Haversine
straight-line distance รท 25 km/h ยฑ2-5 min Has bus coords + stop coords (etaEstimate)
โ˜…
Fallback
500m รท 25 km/h = 1.2 min Coarse estimate No coordinates available (fixed stop distance)
Examples

๐Ÿ’ก Five Real-World Scenarios

1
Bus Arrival Time (ETA)
Estimate arrival time for a specific route to a specific stop
eta-example.js
const { searchRouteByName, etaBySequence, getRouteStopSequence } = require('./lib/bus-api.js');

async function getBusEta(routeName, targetStopName) {
  const route = searchRouteByName(routeName);
  if (!route) return `Route "${routeName}" not found`;

  const stops = getRouteStopSequence(route.routeId, 0);
  const target = stops.find(s => s.stopName.includes(targetStopName));
  if (!target) return `Stop "${targetStopName}" not in sequence`;

  const eta = etaBySequence(route.routeId, target.stopId, 0);
  return `๐ŸšŒ ${route.routeName} โ†’ ${target.stopName}: ~${eta.minutes} min\n${eta.note}`;
}

await getBusEta('234', 'Huwanzayuan');
2
Skip / Anomaly Detection
Scan the entire system for duty or status anomalies
anomaly-example.js
const { detectAnomalies, formatBus } = require('./lib/bus-api.js');

async function checkSystemHealth() {
  const anomalies = await detectAnomalies();
  if (anomalies.length === 0) return 'โœ… All buses operating normally';

  const lines = [`โš ๏ธ  ${anomalies.length} anomalies found:\n`];
  anomalies.forEach(b => {
    const reasons = [];
    if (b.DutyStatus !== '1') reasons.push(`Duty(${b.DutyStatus})`);
    if (b.BusStatus !== '0') reasons.push(`Status(${b.BusStatus})`);
    lines.push(`๐Ÿšจ ${b.BusID} Route:${b.RouteID} โ†’ ${reasons.join(', ')}`);
  });
  return lines.join('\n');
}

await checkSystemHealth();
3
Stop Sequence Lookup
Get the full outbound/return stop list for a route
route-sequence.js
const { searchRouteByName, getRouteStopSequence } = require('./lib/bus-api.js');

function showRouteStops(routeName, direction = 0) {
  const route = searchRouteByName(routeName);
  if (!route) return;
  const dirLabel = direction === 0 ? 'Outbound' : 'Return';
  const stops = getRouteStopSequence(route.routeId, direction);
  console.log(`๐ŸšŒ ${route.routeName} ${dirLabel} (${stops.length} stops)\n`);
  stops.forEach((s, i) => console.log(`  ${String(i+1).padStart(2,'0')}. ${s.stopName}`));
}

showRouteStops('Blue10', 0);
4
Stop Search + Routes at Stop
Search by stop name, then find all routes passing through
stop-search.js
const { searchStops, getRoutesByStop } = require('./lib/bus-api.js');

async function findBusRoutesAtStop(stopKeyword) {
  const hits = searchStops(stopKeyword, 3);
  if (!hits.length) return `No stops matching "${stopKeyword}"`;

  const results = [];
  for (const stop of hits) {
    const routes = getRoutesByStop(stop.stopId);
    results.push({
      stop: stop.name,
      routes: routes.map(r => `${r.routeName}(${r.direction===0?'Out':'Ret'}#${r.index})`)
    });
  }
  return results;
}

await findBusRoutesAtStop('MRT Gongguan');
5
Cron Job Scheduled Monitoring
Set up automatic anomaly checks every N minutes with notifications
Using cron tool
// Use qclaw-cron-skill to check every 5 minutes
cron(action='add', job={
  name: 'Taipei Bus Anomaly Detection',
  schedule: { kind: 'every', everyMs: 5 * 60 * 1000 },
  sessionTarget: 'isolated',
  payload: {
    kind: 'agentTurn',
    message: `Run detectAnomalies() and report. If anomalies found,
              output "โš ๏ธ N anomalies: plate/route/reason"; otherwise "โœ… All normal".`,
    timeoutSeconds: 60
  },
  delivery: { mode: 'announce' }
})

See examples/example-cron.js for more templates: every-5-min anomaly detection, daily morning route summary, and one-shot ETA reminder.

Use Cases

๐Ÿ’ฌ Conversational Use Case Reference

User query โ†’ Agent function โ†’ Response format

"When will Route 234 arrive?"
โ†’
searchRouteByName + etaBySequence
โ†’ ~N minutes (with sequence details)
"How many buses at XX stop?"
โ†’
searchStops + findAtStation
โ†’ N buses at stop (plate list)
"Where is KKC-1100?"
โ†’
findByBus + formatBus
โ†’ plate/route/stop/direction/DataTime
"Where are the most buses?"
โ†’
getHotspots
โ†’ TOP N stops (name + count + coords)
"Show Route Blue10 sequence"
โ†’
searchRouteByName + getRouteStopSequence
โ†’ Full stop list (outbound/return)
"Are all buses normal?"
โ†’
detectAnomalies
โ†’ โœ… All normal or โš ๏ธ N anomalies (with details)
"First/last bus for Route 234?"
โ†’
searchRouteByName + getRouteInfo
โ†’ First/last bus times outbound & return
โš ๏ธAddress-to-address routing (partial limitation)

"I'm at XX stop, going to OO address" requires: โ‘  Address geocoding โ†’ โ‘ก Find nearest stop โ†’ โ‘ข Transfer planning. Geocoding and transfer algorithms are not yet implemented. Consider pairing with tencentmap-jsapi-gl-skill (Tencent Maps geocoding API) or Google Maps / TPTP public transit API for full routing.

Limitations

โš ๏ธ Known Limitations โ€” Read Before Using

1
โš ๏ธ Two Independent ID Systems
TstBusEvent RouteID/StopID (e.g., 158701, 179647) and PTX RouteID/StopID (e.g., 10132, 33210) are completely independent. Cross-references must be built via stop/route name search. Use searchRouteByName to get PTX RouteID first.
2
ETA Accuracy Has an Upper Bound
Even the most accurate sequence-based method (etaBySequence) is limited by: โ‘  The TstBusEvent only reports the last reported stop, not real-time GPS tracks; โ‘ก The cache is a static snapshot; actual sequences may vary slightly with road conditions. Always label results as "estimated" rather than "exact".
3
Live Data Covers Only Equipped Vehicles
TstBusEvent only includes buses with installed fixed-point vehicle equipment, not all operating vehicles. Buses without TstBusEvent hardware will not appear in results โ€” that doesn't mean they don't exist or haven't departed.
4
No Transfer Recommendation
A-to-B address routing requires geocoding + transfer API, which is not implemented. You can use tencentmap-jsapi-gl-skill to geocode addresses and find nearby stops, but complex transfers should be delegated to Google Maps / TPTP.
5
No Occupancy / Crowding Data
TstBusEvent API does not include passenger count or crowding information. For ride comfort assessment, additional data sources would be needed.
6
Cache Requires Periodic Rebuild
PTX stop coordinates and route sequences (data/) are generated by scripts/build-cache.py, default quarterly. If PTX data is updated (route changes, stop relocations), run the rebuild script manually.
Deployment

๐ŸŒ Deploy to GitHub Pages

This documentation site is pure static HTML โ€” deployable to GitHub Pages, Netlify, Vercel, or any static host. GitHub Pages example below.

1
Push docs/ to GitHub
Ensure docs/ contains all HTML, CSS, and JS files, then push to your GitHub repository (use a gh-pages branch or main).
2
Settings โ†’ Pages โ†’ Source
Go to Repository Settings โ†’ Pages, set Source to main branch, folder to /docs.
3
Wait 2-3 minutes for deployment
GitHub will build and publish automatically. Access at https://<username>.github.io/<repo>/.
4
(Optional) Custom domain via CNAME
Write your domain (e.g., taipei-bus.openclaw.ai) in docs/CNAME, then add a CNAME DNS record pointing to <username>.github.io.
License

๐Ÿ“„ Licensing & Attribution

๐Ÿ“œ
Skill Core
OGDL (Open Government Data License)
Bus dynamic data collected and integrated by this skill is provided under OGDL. Free for commercial and non-commercial use with attribution to government open data sources.
๐Ÿ—บ๏ธ
PTX Data
MIT License
PTX Open Data Platform data is provided under its own terms. This skill's PTX integration modules are licensed under MIT.
๐Ÿ“Œ Disclaimer: This skill is maintained by the OpenClaw Agent community, not by the Taipei City Government or PTX official channels. For authoritative bus dynamic data, always refer to official sources.