
Seoul Bike
- 1.5k installs
- 7k repo stars
- Updated August 2, 2026
- nomadamas/k-skill
Seoul Bike is an agent skill that queries real-time Seoul 따릉이 bike-share station availability and returns counts of available bicycles and empty racks near a location or by station name.
About
Seoul Bike is an agent skill that lets your AI coding assistant fetch real-time bike availability and empty rack counts from Seoul's public 따릉이 bike-share system. It wraps the official open data through a hosted proxy so no personal API key is required. Builders can ask natural questions such as current bikes near their location, stations near Gwanghwamun, or availability at Gangnam Station and receive concise, structured answers with distances and timestamps. The skill ships as a single Python script with three subcommands and works out of the box with only standard library dependencies.
- Queries live Seoul 따릉이 station data via hosted k-skill-proxy
- Three subcommands: nearby (by coordinates), search (by name), realtime (raw JSON)
- Returns parsed counts of available bikes, empty racks, distance, and timestamp
- Zero user-managed API keys – proxy holds the upstream credential
- Single Python entrypoint with automatic permission approval after first run
Seoul Bike by the numbers
- 1,544 all-time installs (skills.sh)
- +231 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #207 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nomadamas/k-skill --skill seoul-bikeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.5k |
|---|---|
| repo stars | ★ 7k |
| Last updated | August 2, 2026 |
| Repository | nomadamas/k-skill ↗ |
What it does
Query real-time Seoul bike-share station availability directly from their AI coding agent.
Who is it for?
Best when you're building location-aware tools, travel assistants, or Seoul-specific utilities and need live bike-share data inside your AI coding workflow.
Skip if: Skip if you're working outside the Seoul region or needing historical bike-share analytics rather than live station status.
When should I use this skill?
When the user asks about current Seoul bike availability near a location or by station name.
What you get
Your agent receives structured station data including bike counts, empty racks, distance, and timestamp, ready to incorporate into location-aware features or user-facing answers.
- Structured station data with bike counts, empty racks, distance and timestamp
- Raw realtime JSON when requested
By the numbers
- 3 subcommands
- 2 summary metrics (available bikes, empty racks)
- 1 hosted proxy endpoint
Files
Seoul Bike (따릉이)
What this skill does
서울 열린데이터 광장의 따릉이 실시간 대여정보를 k-skill-proxy 경유로 조회해 대여 가능 자전거 수와 빈 거치대 수를 요약한다.
When to use
- "지금 여기서 따릉이 빌릴 수 있어?"
- "광화문 근처 빈 거치대 있어?"
- "강남역 따릉이 대여소에 자전거 몇 대 남았어?"
Prerequisites
- Python 3 표준 라이브러리만 사용한다.
- optional:
KSKILL_PROXY_BASE_URL(self-host·별도 프록시를 쓸 때만 설정. 비우면 기본 hostedhttps://k-skill-proxy.nomadamas.org를 사용한다.)
Required environment variables
없음. 사용자가 개인 서울 열린데이터 광장 OpenAPI key를 직접 발급할 필요는 없다. /v1/seoul-bike/* routes는 기본 hosted proxy에서 호출하고, upstream key는 proxy 서버 쪽에만 보관한다.
Single entrypoint
python3 "$SKILL_DIR/scripts/seoul_bike.py" <subcommand> [args]첫 사용 시 Bash(python3 *seoul_bike.py:*) 패턴 한 번만 승인하면 이후 호출은 모두 자동 허용된다.
Subcommands
| 명령 | 설명 |
|---|---|
nearby --lat LAT --lon LON [--radius-m 500] [--limit 10] [--json] | 좌표 주변 실시간 대여소 조회 |
search <키워드> [--limit 10] [--json] | 대여소 이름에 키워드가 포함된 실시간 상태 검색 |
realtime [--start-index 1 --end-index 1000] | 실시간 대여정보 원문 JSON 페이지 조회 |
Workflow
1. 현재 위치 주변 대여소 조회
python3 "$SKILL_DIR/scripts/seoul_bike.py" nearby --lat 37.5717 --lon 126.9763 --radius-m 500요약 항목:
- 대여소명
- 대여 가능 자전거 수 (
parkingBikeTotCnt) - 빈 거치대 수 (
rackTotCnt - parkingBikeTotCnt) - 거리(m)
- 조회 시각(
proxy.requested_at)
2. 대여소 이름 검색
python3 "$SKILL_DIR/scripts/seoul_bike.py" search "광화문" --limit 53. Proxy endpoints
GET /v1/seoul-bike/realtime?startIndex=1&endIndex=1000→ 서울bikeList실시간 대여정보GET /v1/seoul-bike/stations?startIndex=1&endIndex=1000→ 서울tbCycleStationInfo대여소 마스터 정보GET /v1/seoul-bike/nearby?lat=37.5717&lon=126.9763&radius_m=500&limit=10→ proxy-side 주변 대여소 필터링
Done when
- 대여 가능 자전거 수와 빈 거치대 수가 정리되어 있다.
- live data 기준 조회 시각이 명시되어 있다.
- upstream key가 클라이언트에 노출되지 않았다.
Failure modes
- proxy upstream key 미설정 (
SEOUL_OPEN_API_KEY없음) - 서울 열린데이터 광장 quota 초과
- 실시간 API가 빈 행 또는 일시 오류를 반환
- 좌표가 없거나 반경 안에 대여소가 없음
Notes
- 실시간 데이터는 계속 변하므로 답변에 조회 시각을 함께 적는다.
- 예약/대여 자동화는 하지 않는다. 조회 전용 스킬이다.
- proxy 운영/환경변수 설정은
docs/features/k-skill-proxy.md를 참고한다.
#!/usr/bin/env python3
"""Single-entrypoint CLI for the seoul-bike skill.
Subcommands:
nearby --lat LAT --lon LON — find realtime Seoul Bike stations near coordinates
search KEYWORD — search station names in realtime availability page(s)
realtime — fetch raw realtime station availability
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
for _stream in (sys.stdout, sys.stderr):
reconfigure = getattr(_stream, "reconfigure", None)
if reconfigure is not None:
try:
reconfigure(encoding="utf-8")
except (OSError, ValueError):
pass
TIMEOUT_SEC = 15
PROXY_BASE_URL_NAME = "KSKILL_PROXY_BASE_URL"
DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
def get_proxy_base_url() -> str:
value = os.environ.get(PROXY_BASE_URL_NAME)
if value and value.strip() and value.strip() != "replace-me":
return value.strip().rstrip("/")
return DEFAULT_PROXY_BASE_URL
def fetch_json(path: str, params: dict[str, Any]) -> dict[str, Any]:
query = urllib.parse.urlencode(params)
url = f"{get_proxy_base_url()}{path}?{query}"
req = urllib.request.Request(url, headers={"User-Agent": "k-skill/seoul-bike"})
with urllib.request.urlopen(req, timeout=TIMEOUT_SEC) as resp:
raw = resp.read().decode("utf-8")
return json.loads(raw)
def _to_int(value: Any) -> int | None:
if value in (None, ""):
return None
try:
return int(float(value))
except (TypeError, ValueError):
return None
def normalize_realtime_row(row: dict[str, Any]) -> dict[str, Any]:
rack_total = _to_int(row.get("rackTotCnt") or row.get("rack_total_count"))
available = _to_int(row.get("parkingBikeTotCnt") or row.get("available_bikes"))
empty_docks = None if rack_total is None or available is None else max(0, rack_total - available)
return {
"station_id": row.get("stationId") or row.get("station_id"),
"station_name": row.get("stationName") or row.get("station_name"),
"rack_total_count": rack_total,
"available_bikes": available,
"empty_docks": empty_docks,
"shared_percent": _to_int(row.get("shared") or row.get("shared_percent")),
"latitude": row.get("stationLatitude") or row.get("latitude"),
"longitude": row.get("stationLongitude") or row.get("longitude"),
}
def realtime_rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
status = payload.get("rentBikeStatus") or {}
rows = status.get("row") or []
return rows if isinstance(rows, list) else []
def filter_realtime_rows(payload: dict[str, Any], keyword: str, limit: int) -> list[dict[str, Any]]:
normalized_keyword = keyword.strip().lower()
matches: list[dict[str, Any]] = []
for row in realtime_rows(payload):
station_name = str(row.get("stationName") or row.get("station_name") or "")
if normalized_keyword in station_name.lower():
matches.append(normalize_realtime_row(row))
if len(matches) >= limit:
break
return matches
def format_station(item: dict[str, Any]) -> str:
distance = item.get("distance_m")
distance_text = f", 거리 {distance}m" if distance is not None else ""
bikes = item.get("available_bikes")
docks = item.get("empty_docks")
bikes_text = "알 수 없음" if bikes is None else f"{bikes}대"
docks_text = "알 수 없음" if docks is None else f"{docks}개"
return f"- {item.get('station_name')}: 대여 가능 {bikes_text}, 빈 거치대 {docks_text}{distance_text}"
def format_nearby(payload: dict[str, Any]) -> list[str]:
query = payload.get("query") or {}
lines = [
f"따릉이 주변 대여소 {payload.get('count', 0)}곳",
f"기준 좌표: {query.get('latitude')}, {query.get('longitude')} / 반경 {query.get('radius_m')}m",
]
for item in payload.get("items") or []:
lines.append(format_station(item))
requested_at = (payload.get("proxy") or {}).get("requested_at")
if requested_at:
lines.append(f"조회 시각: {requested_at}")
return lines
def cmd_nearby(args: argparse.Namespace) -> int:
payload = fetch_json(
"/v1/seoul-bike/nearby",
{"lat": args.lat, "lon": args.lon, "radius_m": args.radius_m, "limit": args.limit},
)
if args.json:
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
else:
print("\n".join(format_nearby(payload)))
return 0
def fetch_realtime_payload(start_index: int = 1, end_index: int = 1000) -> dict[str, Any]:
rows: list[dict[str, Any]] = []
current_start = start_index
page_size = max(1, end_index - start_index + 1)
requested_at = None
while True:
current_end = current_start + page_size - 1
payload = fetch_json(
"/v1/seoul-bike/realtime",
{"startIndex": current_start, "endIndex": current_end},
)
if requested_at is None:
requested_at = (payload.get("proxy") or {}).get("requested_at")
page_rows = realtime_rows(payload)
rows.extend(page_rows)
total_count = _to_int((payload.get("rentBikeStatus") or {}).get("list_total_count"))
if total_count is None or current_end >= total_count or not page_rows:
break
current_start = current_end + 1
return {
"rentBikeStatus": {"row": rows},
"proxy": {"requested_at": requested_at},
}
def fetch_realtime_pages(start_index: int = 1, end_index: int = 1000) -> list[dict[str, Any]]:
return realtime_rows(fetch_realtime_payload(start_index, end_index))
def cmd_search(args: argparse.Namespace) -> int:
payload = fetch_realtime_payload(args.start_index, args.end_index)
matches = filter_realtime_rows(payload, args.keyword, args.limit)
if args.json:
json.dump({"keyword": args.keyword, "count": len(matches), "items": matches, "proxy": payload.get("proxy")}, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
else:
if not matches:
print(f"'{args.keyword}'와 일치하는 따릉이 대여소가 없습니다.", file=sys.stderr)
return 1
print(f"따릉이 대여소 검색: {args.keyword}")
for item in matches:
print(format_station(item))
requested_at = (payload.get("proxy") or {}).get("requested_at")
if requested_at:
print(f"조회 시각: {requested_at}")
return 0
def cmd_realtime(args: argparse.Namespace) -> int:
payload = fetch_json(
"/v1/seoul-bike/realtime",
{"startIndex": args.start_index, "endIndex": args.end_index},
)
json.dump(payload, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="서울 따릉이 실시간 대여소 조회")
sub = parser.add_subparsers(dest="command", required=True)
nearby = sub.add_parser("nearby", help="좌표 주변 대여소 조회")
nearby.add_argument("--lat", required=True, type=float)
nearby.add_argument("--lon", required=True, type=float)
nearby.add_argument("--radius-m", type=int, default=500)
nearby.add_argument("--limit", type=int, default=10)
nearby.add_argument("--json", action="store_true")
nearby.set_defaults(func=cmd_nearby)
search = sub.add_parser("search", help="실시간 대여소 이름 검색")
search.add_argument("keyword")
search.add_argument("--start-index", type=int, default=1)
search.add_argument("--end-index", type=int, default=1000, help="page size end index for the first realtime page; search continues through all pages")
search.add_argument("--limit", type=int, default=10)
search.add_argument("--json", action="store_true")
search.set_defaults(func=cmd_search)
realtime = sub.add_parser("realtime", help="실시간 대여소 원문 JSON 조회")
realtime.add_argument("--start-index", type=int, default=1)
realtime.add_argument("--end-index", type=int, default=1000)
realtime.set_defaults(func=cmd_realtime)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
return args.func(args)
except urllib.error.HTTPError as exc:
print(f"API HTTP 오류: {exc.code} {exc.reason}", file=sys.stderr)
return 1
except urllib.error.URLError as exc:
print(f"API 연결 실패: {exc.reason}", file=sys.stderr)
return 1
except json.JSONDecodeError as exc:
print(f"API 응답 JSON 파싱 실패: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
Related skills
How it compares
Use instead of manually calling the Seoul open-data API or writing your own scraper.
FAQ
Who is seoul-bike for?
Developers and developers creating location-aware apps, travel helpers, or Seoul-focused utilities who want live bike-share data inside their AI coding agent.
When should I use seoul-bike?
Use it when you need current bicycle availability near a set of coordinates, want to search stations by name like Gangnam Station or Gwanghwamun, or require raw real-time JSON for a feature that shows nearby transport options.
Is seoul-bike safe to install?
Users should review the Security Audits panel on this page. The skill uses only a hosted proxy that keeps the upstream API key server-side and requires no personal credentials.