
Intercity Bus Booking
- 1.8k installs
- 6.5k repo stars
- Updated July 27, 2026
- nomadamas/k-skill
intercity-bus-booking is an agent skill for search and assist korean 시외버스/tmoney bookings using official http/api-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff.
About
The intercity-bus-booking skill is designed for search and assist Korean 시외버스/Tmoney bookings using official HTTP/API-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff. Intercity Bus Booking (Tmoney 시외버스) Overview Use this skill for Korean 시외버스 timetable lookup and reservation assistance via the official Tmoney intercity bus site. Prefer HTTP/API-first flows for route lookup, timetable search, fare/seat-stage entry, and checkout-entry handoff. Invoke when the user asks about intercity bus booking or related SKILL.md workflows.
- The user asks for 시외버스 예매, 시외버스 시간표, 시외버스 예약, or a known 시외버스 route.
- The user names 티머니/Tmoney/시외버스 통합예매.
- 고속버스/KOBUS routes — use express-bus-booking.
- Final card submission or payment unless the user explicitly confirms the exact action.
- Browser automation as the first option when HTTP endpoints are available.
Intercity Bus Booking by the numbers
- 1,770 all-time installs (skills.sh)
- +127 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #195 of 2,742 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
intercity-bus-booking capabilities & compatibility
- Capabilities
- the user asks for 시외버스 예매, 시외버스 시간표, 시외버스 예약, or · the user names 티머니/tmoney/시외버스 통합예매 · 고속버스/kobus routes — use express bus booking · final card submission or payment unless the user
What intercity-bus-booking says it does
Search and assist Korean 시외버스/Tmoney bookings using official HTTP/API-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff.
Search and assist Korean 시외버스/Tmoney bookings using official HTTP/API-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff.
npx skills add https://github.com/nomadamas/k-skill --skill intercity-bus-bookingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 6.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | nomadamas/k-skill ↗ |
How do I search and assist korean 시외버스/tmoney bookings using official http/api-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff?
Search and assist Korean 시외버스/Tmoney bookings using official HTTP/API-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff.
Who is it for?
Developers using intercity bus booking workflows documented in SKILL.md.
Skip if: Skip when the task falls outside intercity-bus-booking scope or needs a different stack.
When should I use this skill?
User asks about intercity bus booking or related SKILL.md workflows.
What you get
Completed intercity-bus-booking workflow with documented commands, files, and expected deliverables.
- Timetable JSON responses
- Fare and seat-availability fields
By the numbers
- Session-proven on 2026-05-08 and re-verified 2026-05-13
- Test route returned 14 schedules on 2026-05-09 and 20 departures on 2026-05-20
Files
Intercity Bus Booking (Tmoney 시외버스)
Overview
Use this skill for Korean 시외버스 timetable lookup and reservation assistance via the official Tmoney intercity bus site. Prefer HTTP/API-first flows for route lookup, timetable search, fare/seat-stage entry, and checkout-entry handoff.
Keep 시외버스 separate from 고속버스. Tmoney terminal codes and POST flows are different from KOBUS 고속버스. Use express-bus-booking for KOBUS routes.
When to Use
- The user asks for
시외버스 예매,시외버스 시간표,시외버스 예약, or a known 시외버스 route. - The user names 티머니/Tmoney/시외버스 통합예매.
- The task is to present timetable candidates, check remaining seats/fares, or assist up to the official card-information page.
Do not use this for:
- 고속버스/KOBUS routes — use
express-bus-booking. - Final card submission or payment unless the user explicitly confirms the exact action.
- Browser automation as the first option when HTTP endpoints are available.
Core Principles
- Use official Tmoney 시외버스 surfaces:
https://intercitybus.tmoney.co.kr. - Use cookies, referers, and a normal browser User-Agent.
- Default scope is lookup and assisted manual checkout: timetable, fare/seat stage, selected-seat temporary hold, and official card-information page handoff.
- Do not submit card fields, final payment, or irreversible reservation steps without explicit confirmation.
- If a test or user-abandoned hold is created, use the official cancellation/back flow when available.
Known HTTP Flow
See references/tmoney-intercity-http-flow.md for session-proven endpoint details and parameter examples.
1. Start a Session
Fetch a landing or booking-entry page with cookies enabled:
GET https://intercitybus.tmoney.co.kr/
GET https://intercitybus.tmoney.co.kr/otck/trmlInfEnty.do2. Query Timetable
POST /otck/readAlcnList.doTypical fields include:
depr_Trml_Cd=0511601
arvl_Trml_Cd=2482701
depr_Trml_Nm=동서울
arvl_Trml_Nm=속초
ig=1
im=0
ic=0
iv=0
depr_Dt=YYYYMMDD
depr_Time=000000
bef_Aft_Dvs=D
req_Rec_Num=10bef_Aft_Dvs and req_Rec_Num are required hidden fields from the browser JavaScript readAlcnListEntry(...). If they are omitted, Tmoney can return a generic error page with no schedules.
Parse schedule buttons/rows. The next-stage parameters are often embedded in readSasFeeInf(...) onclick arguments.
3. Enter Fare / Seat-Count Stage
POST /otck/readSatsFee.doSend selected values from readSasFeeInf(...) plus passenger counts and original search fields. The response should include form#readPcpySats and hidden values such as:
rot_Id
alcn_Sqno
depr_Trml_Cd
arvl_Trml_Cd
depr_Time
igFee
imFee
icFee
total4. Temporary Hold and Card-Information Entry
POST /otck/readPcpySats.doSubmit the readPcpySats hidden fields plus selected seat/count fields, commonly including:
pcpy_Num
sats_No
bus_Tck_Knd_Cd
cty_Bus_Dc_Knd_Cd
dcrt_Dvs_Cd
rtrp_Depr_DtA successful response lands on the official 카드정보 입력 page and includes a temporary seat hold identifier such as sats_Pcpy_Id.
Timetable Helper
For read-only timetable lookup, use the bundled helper before attempting browser automation:
python3 intercity-bus-booking/scripts/intercity_bus_search.py \
--depart-code 0511601 \
--arrive-code 2482701 \
--depart-name 동서울 \
--arrive-name 속초 \
--date 20260520The helper starts a cookie-backed session, posts the browser-required timetable fields, parses readSasFeeInf(...), and prints JSON with departure time, company, class, fares, and remaining/total seats. By default it is read-only. With --hold-seat <seatNo> or --hold-first-seat, it enters readSatsFee.do, posts readPcpySats.do, and saves the official Tmoney card-information HTML page plus cancel/back fields. It still never submits card data or final payment.
Temporary Hold Helper
To create a temporary hold and save the official card-information page:
python3 intercity-bus-booking/scripts/intercity_bus_search.py \
--depart-code 0511601 \
--arrive-code 2482701 \
--depart-name 동서울 \
--arrive-name 속초 \
--date 20260520 \
--select-index 1 \
--hold-first-seat \
--output-dir /tmp/tmoney-holdSuccess requires hold.success=true, a sats_Pcpy_Id, and the saved page containing 카드정보 입력. The saved cancel fields can be posted back to /otck/readSatsFee.do with pcpyCanc=C to abandon the hold. Live probes did not expose an exact countdown on the card-information page; treat the hold as short-lived and have the user complete payment immediately.
Checkout-Entry Link Helper
A helper-served HTML page can auto-submit a POST form directly to:
https://intercitybus.tmoney.co.kr/otck/readPcpySats.doThis creates the hold in the user's browser/session and opens the official Tmoney card-information page. The user should manually finish payment on the official page.
For Discord/mobile, provide helper links as normal text links rather than fenced code blocks so the user can tap them.
Mobile and In-App Browser Notes
Tmoney 시외버스 is more mobile-tolerant than KOBUS in the tested flow: an iPhone Safari-style mobile User-Agent returned the official 카드정보 입력 page with sats_Pcpy_Id present.
However, in-app browser User-Agents can behave differently or fail if the same seat/hold payload is replayed. If a Discord/Android in-app test returns a generic error such as 발행을 실패하였습니다, do not assume route lookup is broken. Recreate a fresh hold payload, retry once in an external browser, and cancel stale holds.
Suggested Output Format
Keep candidate lists concise:
시외버스 동서울 → 속초 / 2026-05-09
1. 06:05 우등 / 금강고속 / 잔여 8석 / 성인 21,300원
2. ...When a checkout-entry helper is created, say that it opens the official Tmoney card-information page and that payment remains manual.
Common Pitfalls
1. Mixing terminal code systems. Tmoney 시외버스 codes are not KOBUS codes. 2. Assuming checkout-entry equals final payment. readPcpySats.do can open the card-information page, but final payment remains a separate manual step. 3. Omitting hidden timetable fields. readAlcnList.do needs bef_Aft_Dvs=D and req_Rec_Num=10; without them it may return a generic errorCont page of about 13 KB instead of schedule rows. 4. Replaying stale hold payloads. A repeated POST for the same route/seat can fail or create confusing results. Generate a fresh seat-stage payload for real use. 5. Skipping cancellation/back flow. Use the official cancellation/back form (pcpyCanc=C via readSatsFee.do when available) for abandoned holds. 6. Overusing browser automation. Use browser only for endpoint discovery or visual verification after HTTP probing.
Verification Checklist
- [ ] Route/terminal codes were resolved from Tmoney 시외버스, not guessed or copied from KOBUS.
- [ ] Timetable POST included
bef_Aft_Dvs=Dandreq_Rec_Num=10. - [ ] Timetable response was parsed for schedule rows/buttons and next-stage parameters.
- [ ] Fare/seat-stage response contains
form#readPcpySatsand expected hidden fields. - [ ] Checkout-entry response contains
카드정보 입력and a hold identifier such assats_Pcpy_Idbefore reporting success. - [ ] Stale/test holds were released through the official cancellation/back flow where possible.
- [ ] Payment/card submission was left to the user unless explicitly confirmed.
- [ ] Mobile links are provided as tappable text links on Discord/mobile platforms.
Tmoney 시외버스 HTTP/API Probe Notes
Session-proven on 2026-05-08 and re-verified on 2026-05-13. Goal: avoid browser automation where possible.
Base
https://intercitybus.tmoney.co.krUse a normal browser User-Agent, cookie jar, and referers.
Tested Flow
Timetable
POST /otck/readAlcnList.doExample tested route/date:
동서울(0511601) -> 속초(2482701), 2026-05-09Observed results:
2026-05-09: 14 reservation buttons/schedules
2026-05-20: 20 readSasFeeInf schedule buttons, first departure 06:05 금강고속 우등, 24/28 seatsTypical POST fields:
depr_Trml_Cd=0511601
arvl_Trml_Cd=2482701
depr_Trml_Nm=동서울
arvl_Trml_Nm=속초
ig=1
im=0
ic=0
iv=0
depr_Dt=YYYYMMDD
depr_Time=000000
bef_Aft_Dvs=D
req_Rec_Num=10bef_Aft_Dvs=D and req_Rec_Num=10 are not optional. They are appended by the site JavaScript (readAlcnListEntry(bef_Aft_Dvs, req_Rec_Num)) before the browser submits #onewayInfo. Omitting them returned a generic error page (errorCont, about 13,770 bytes) with no readSasFeeInf(...) schedules in live probing.
The next-stage values are embedded in readSasFeeInf(...) onclick calls. Example prefix:
readSasFeeInf('RT201603150047276','1','20260509','1','0511601','2482701','동서울','속초','060500','C004','IDP','금강고속','우등','1','0','0','8','28', ...)Fare / Seat-Count Stage
POST /otck/readSatsFee.doSend selected values from readSasFeeInf(...) plus passenger counts and original search fields.
Observed response contained form#readPcpySats and hidden values:
{
"rot_Id": "RT201603150047276",
"alcn_Sqno": "1",
"depr_Trml_Cd": "0511601",
"arvl_Trml_Cd": "2482701",
"depr_Time": "060500",
"igFee": "21300",
"imFee": "17000",
"icFee": "10700",
"total": "21300"
}Temporary Hold / Card-Information Entry
POST /otck/readPcpySats.doSend readPcpySats hidden fields plus selected seat fields:
pcpy_Num
sats_No
bus_Tck_Knd_Cd
cty_Bus_Dc_Knd_Cd
dcrt_Dvs_Cd
rtrp_Depr_DtObserved success markers:
카드정보 입력
sats_Pcpy_IdRe-verified on 2026-05-13 with 동서울 -> 속초, 2026-05-20, 06:05 금강고속 우등, seat 1. readPcpySats.do returned 카드정보 입력 and sats_Pcpy_Id=SP.... Posting the resulting cancel/back fields with pcpyCanc=C to /otck/readSatsFee.do returned to the seat-selection page and subsequent timetable lookup still showed 24/28 seats.
Cancellation / Back Flow
A POST back to /otck/readSatsFee.do with pcpyCanc=C and the hold fields returned to seat selection and appeared to release the temporary hold in testing.
Mobile Notes
- iPhone Safari-style mobile User-Agent returned HTTP 200 with
카드정보 입력andsats_Pcpy_Idpresent. - A replayed Discord/Android in-app User-Agent test returned a generic
발행을 실패하였습니다page. Because the same seat/hold payload had already been used, treat this as a stale/replayed hold caveat rather than proof that mobile is unsupported. - For real use, generate a fresh hold payload, send a helper link, and ask the user to open it in an external browser if an in-app browser fails.
Interpretation
- Login was not required for timetable lookup, fare/seat-stage entry, or card-information page entry in the tested flow.
- CAPTCHA was not observed in the tested flow.
- A generic
errorContresponse usually means the posted form contract is incomplete, not necessarily that the route is unavailable; first verifybef_Aft_Dvsandreq_Rec_Num. - Payment/card-info submission is separate and should not be automated without explicit confirmation.
- The live card-information page did not expose an exact countdown/expiry text in probes. Treat temporary holds as short-lived: hand off immediately, and post the cancel/back fields for abandoned holds.
- Terminal codes are Tmoney-specific and must not be mixed with KOBUS codes.
#!/usr/bin/env python3
"""Search and optionally hold Tmoney intercity-bus seats through official flows.
Default mode is read-only timetable parsing. With --hold-seat, the helper performs
Tmoney's temporary seat-hold POST and saves the official card-information page.
It never submits card fields or final payment.
"""
from __future__ import annotations
import argparse
import html
import http.cookiejar
import json
import re
import ssl
import sys
import tempfile
import urllib.parse
import urllib.request
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Iterable
BASE_URL = "https://intercitybus.tmoney.co.kr"
ENTRY_PATH = "/otck/trmlInfEnty.do"
TIMETABLE_PATH = "/otck/readAlcnList.do"
SEAT_STAGE_PATH = "/otck/readSatsFee.do"
HOLD_PATH = "/otck/readPcpySats.do"
DEFAULT_UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125 Safari/537.36"
)
ROW_RE = re.compile(r"<tr>\s*(.*?)readSasFeeInf\((.*?)\).*?</tr>", re.DOTALL | re.IGNORECASE)
TD_WRAP_RE = re.compile(r'<div class="td_wrap1">(.*?)</div>', re.DOTALL | re.IGNORECASE)
TAG_RE = re.compile(r"<[^>]+>")
ARG_RE = re.compile(r"'((?:\\'|[^'])*)'")
FORM_RE = re.compile(r"<form\b([^>]*)>(.*?)</form>", re.DOTALL | re.IGNORECASE)
INPUT_RE = re.compile(r"<input\b([^>]+)>", re.DOTALL | re.IGNORECASE)
ATTR_RE = re.compile(r"([\w:-]+)=[\"']([^\"']*)[\"']")
SEAT_RE = re.compile(r"<li([^>]*)>\s*<a[^>]*>.*?<span>(\d+)</span>", re.DOTALL | re.IGNORECASE)
@dataclass
class Schedule:
departure_time: str | None
company: str | None
duration: str | None
bus_class: str | None
adult_fare: str | None
child_fare: str | None
student_fare: str | None
remaining_seats: int | None
total_seats: int | None
raw_args: list[str]
@dataclass
class HoldResult:
success: bool
hold_id: str | None
seat: str
card_page_path: str | None
cancel_fields_path: str | None
markers: dict[str, int]
failure_message: str | None = None
def _ssl_context() -> ssl.SSLContext:
# Tmoney has historically required curl -k in probes on some machines.
# Keep this helper resilient while limiting it to the official host.
return ssl._create_unverified_context() # noqa: SLF001
def _strip(value: str) -> str:
value = re.sub(r"<!--.*?-->", "", value, flags=re.DOTALL)
value = TAG_RE.sub("", value)
return html.unescape(value).replace("\xa0", " ").strip()
def _attrs(fragment: str) -> dict[str, str]:
return {k.lower(): html.unescape(v) for k, v in ATTR_RE.findall(fragment)}
def _open(opener: urllib.request.OpenerDirector, request: urllib.request.Request, timeout: int) -> str:
with opener.open(request, timeout=timeout) as response:
charset = response.headers.get_content_charset() or "utf-8"
return response.read().decode(charset, errors="replace")
def build_opener() -> urllib.request.OpenerDirector:
jar = http.cookiejar.CookieJar()
return urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(jar),
urllib.request.HTTPSHandler(context=_ssl_context()),
)
def _request(url: str, data: list[tuple[str, str]] | dict[str, str] | None = None, referer: str | None = None) -> urllib.request.Request:
headers = {"User-Agent": DEFAULT_UA}
if referer:
headers["Referer"] = referer
if data is None:
return urllib.request.Request(url, headers=headers, method="GET")
headers["Content-Type"] = "application/x-www-form-urlencoded"
encoded = urllib.parse.urlencode(data).encode("utf-8")
return urllib.request.Request(url, data=encoded, headers=headers, method="POST")
def search_timetable(
depart_code: str,
arrive_code: str,
depart_name: str,
arrive_name: str,
date: str,
time: str = "000000",
adults: int = 1,
students: int = 0,
children: int = 0,
veterans: int = 0,
timeout: int = 20,
opener: urllib.request.OpenerDirector | None = None,
) -> tuple[urllib.request.OpenerDirector, str, list[Schedule]]:
opener = opener or build_opener()
_open(opener, _request(f"{BASE_URL}{ENTRY_PATH}"), timeout)
fields = {
"depr_Trml_Cd": depart_code,
"arvl_Trml_Cd": arrive_code,
"depr_Trml_Nm": depart_name,
"arvl_Trml_Nm": arrive_name,
"ig": str(adults),
"im": str(students),
"ic": str(children),
"iv": str(veterans),
"depr_Dt": date,
"depr_Time": time,
# Required by the browser JS readAlcnListEntry(). Missing either field
# returns a generic error page with no schedule rows.
"bef_Aft_Dvs": "D",
"req_Rec_Num": "10",
}
body = _open(opener, _request(f"{BASE_URL}{TIMETABLE_PATH}", fields, f"{BASE_URL}{ENTRY_PATH}"), timeout)
return opener, body, parse_schedules(body)
def parse_schedules(body: str) -> list[Schedule]:
schedules: list[Schedule] = []
for row_html, arg_text in ROW_RE.findall(body):
args = [a.replace("\\'", "'") for a in ARG_RE.findall(arg_text)]
cells = [_strip(x) for x in TD_WRAP_RE.findall(row_html)]
departure = cells[0] if len(cells) > 0 else (args[8][:2] + ":" + args[8][2:4] if len(args) > 8 else None)
company_cell = cells[1] if len(cells) > 1 else None
company = args[11] if len(args) > 11 else None
duration = None
if company_cell and company and company_cell.startswith(company):
duration = company_cell[len(company):].strip() or None
elif company_cell:
duration = company_cell
bus_class = args[12] if len(args) > 12 else (cells[2] if len(cells) > 2 else None)
remaining = int(args[16]) if len(args) > 16 and args[16].isdigit() else None
total = int(args[17]) if len(args) > 17 and args[17].isdigit() else None
schedules.append(
Schedule(
departure_time=departure,
company=company,
duration=duration,
bus_class=bus_class,
adult_fare=cells[3] if len(cells) > 3 else None,
child_fare=cells[4] if len(cells) > 4 else None,
student_fare=cells[5] if len(cells) > 5 else None,
remaining_seats=remaining,
total_seats=total,
raw_args=args,
)
)
return schedules
def _seat_stage_fields(schedule: Schedule, search_time: str) -> dict[str, str]:
a = schedule.raw_args
if len(a) < 21:
raise ValueError("schedule raw_args does not contain the expected readSasFeeInf payload")
return {
"atl_Depr_Dt_S1": a[2],
"atl_Depr_Time_S1": search_time,
"rot_Id": a[0],
"rot_Sqno": a[1],
"alcn_Dt": a[2],
"alcn_Sqno": a[3],
"depr_Trml_Cd": a[4],
"arvl_Trml_Cd": a[5],
"depr_Trml_Nm": a[6],
"arvl_Trml_Nm": a[7],
"depr_Time": a[8],
"bus_Cacm_Cd": a[9],
"bus_Cls_Cd": a[10],
"bus_Cacm_Nm": a[11],
"bus_Cls_Nm": a[12],
"ig": a[13],
"im": a[14],
"ic": a[15],
"rmn_Scnt": a[16],
"sats_Num": a[17],
"atl_Depr_Dt": a[18],
"atl_Depr_Time": a[19],
"dc_Psb_Yn": a[20],
}
def _form_fields(body: str, form_id: str) -> list[tuple[str, str]]:
for attrs_text, form_body in FORM_RE.findall(body):
attrs = _attrs(attrs_text)
if attrs.get("id") == form_id or attrs.get("name") == form_id:
fields: list[tuple[str, str]] = []
for input_text in INPUT_RE.findall(form_body):
input_attrs = _attrs(input_text)
name = input_attrs.get("name")
if name:
fields.append((name, input_attrs.get("value", "")))
return fields
return []
def _available_seats(seat_stage_body: str) -> list[str]:
seats: list[str] = []
for li_attrs, seat_no in SEAT_RE.findall(seat_stage_body):
classes = _attrs(li_attrs).get("class", "")
if "disabled" not in classes.split():
seats.append(seat_no)
return seats
def hold_seat(
opener: urllib.request.OpenerDirector,
schedule: Schedule,
search_time: str,
seat: str | None,
output_dir: Path,
timeout: int = 20,
) -> tuple[str, list[str], HoldResult]:
seat_stage_body = _open(
opener,
_request(f"{BASE_URL}{SEAT_STAGE_PATH}", _seat_stage_fields(schedule, search_time), f"{BASE_URL}{TIMETABLE_PATH}"),
timeout,
)
available = _available_seats(seat_stage_body)
selected = seat or (available[0] if available else "")
if not selected:
return seat_stage_body, available, HoldResult(False, None, "", None, None, {}, "No selectable seat was found")
if selected not in available:
return seat_stage_body, available, HoldResult(False, None, selected, None, None, {}, f"Seat {selected} is not selectable")
fields = _form_fields(seat_stage_body, "readPcpySats")
if not fields:
return seat_stage_body, available, HoldResult(False, None, selected, None, None, {}, "No readPcpySats form found")
# Mirror pcpySats() in /js/tckmrs/readSatsInfo.js for a normal adult-only hold.
field_map = dict(fields)
fields.extend(
[
("pcpy_Num", "1"),
("sats_No", selected),
("rtrp_Depr_Dt", ""),
("bus_Tck_Knd_Cd", field_map.get("ig_Knd_Cd", "IG00")),
("cty_Bus_Dc_Knd_Cd", "Z"),
("dcrt_Dvs_Cd", "0"),
]
)
hold_body = _open(opener, _request(f"{BASE_URL}{HOLD_PATH}", fields, f"{BASE_URL}{SEAT_STAGE_PATH}"), timeout)
markers = {k: hold_body.count(k) for k in ["카드정보 입력", "sats_Pcpy_Id", "이미 발매된 좌석", "발행을 실패", "errorCont"]}
hold_ids = re.findall(r'name=["\']sats_Pcpy_Id["\'][^>]*value=["\']([^"\']+)', hold_body)
success = bool(hold_ids and markers["카드정보 입력"] and not markers["errorCont"])
output_dir.mkdir(parents=True, exist_ok=True)
card_path = output_dir / "tmoney-intercity-card-info.html"
card_path.write_text(hold_body)
cancel_fields = _form_fields(hold_body, "alcnInfo") or _form_fields(hold_body, "onwayInfo")
cancel_path = output_dir / "tmoney-intercity-cancel-fields.txt"
if cancel_fields:
cancel_path.write_text("\n".join(f"{k}={v}" for k, v in cancel_fields))
else:
cancel_path = None # type: ignore[assignment]
failure = None if success else _strip(hold_body[hold_body.find("[처리결과]") : hold_body.find("[처리결과]") + 500]) or "Hold did not reach card-information page"
return seat_stage_body, available, HoldResult(
success=success,
hold_id=hold_ids[0] if hold_ids else None,
seat=selected,
card_page_path=str(card_path),
cancel_fields_path=str(cancel_path) if cancel_path else None,
markers=markers,
failure_message=failure,
)
def main(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Search Tmoney intercity-bus timetable and optionally create a temporary seat hold")
parser.add_argument("--depart-code", required=True)
parser.add_argument("--arrive-code", required=True)
parser.add_argument("--depart-name", required=True)
parser.add_argument("--arrive-name", required=True)
parser.add_argument("--date", required=True, help="YYYYMMDD")
parser.add_argument("--time", default="000000", help="HHMMSS, default 000000")
parser.add_argument("--adults", type=int, default=1)
parser.add_argument("--students", type=int, default=0)
parser.add_argument("--children", type=int, default=0)
parser.add_argument("--veterans", type=int, default=0)
parser.add_argument("--timeout", type=int, default=20)
parser.add_argument("--limit", type=int, default=20)
parser.add_argument("--select-index", type=int, default=1, help="1-based schedule index for --hold-seat")
parser.add_argument("--hold-seat", help="Temporarily hold this seat number and save the official card-info page")
parser.add_argument("--hold-first-seat", action="store_true", help="Hold the first selectable seat for the selected schedule")
parser.add_argument("--output-dir", help="Directory for saved hold/card page files; defaults to a temp directory")
args = parser.parse_args(argv)
if not re.fullmatch(r"\d{8}", args.date):
parser.error("--date must be YYYYMMDD")
if not re.fullmatch(r"\d{6}", args.time):
parser.error("--time must be HHMMSS")
if args.students or args.children or args.veterans:
parser.error("seat holding currently supports adult-only payloads; use search mode for mixed passenger counts")
if args.select_index < 1:
parser.error("--select-index must be 1 or greater")
opener, body, schedules = search_timetable(
depart_code=args.depart_code,
arrive_code=args.arrive_code,
depart_name=args.depart_name,
arrive_name=args.arrive_name,
date=args.date,
time=args.time,
adults=args.adults,
students=args.students,
children=args.children,
veterans=args.veterans,
timeout=args.timeout,
)
result: dict[str, object] = {
"route": {
"depart_code": args.depart_code,
"arrive_code": args.arrive_code,
"depart_name": args.depart_name,
"arrive_name": args.arrive_name,
"date": args.date,
"time": args.time,
},
"count": len(schedules),
"items": [asdict(s) for s in schedules[: args.limit]],
"failure_mode": None,
}
if not schedules:
result["failure_mode"] = (
"No readSasFeeInf schedule rows found. Check terminal codes/date, sold-out/no-service state, "
"or whether Tmoney returned its generic error page."
)
result["error_page_marker_count"] = body.count("errorCont")
print(json.dumps(result, ensure_ascii=False, indent=2))
return 2
if args.hold_seat or args.hold_first_seat:
if args.select_index > len(schedules):
parser.error(f"--select-index {args.select_index} exceeds schedule count {len(schedules)}")
output_dir = Path(args.output_dir) if args.output_dir else Path(tempfile.mkdtemp(prefix="tmoney-intercity-hold-"))
_, available, hold = hold_seat(opener, schedules[args.select_index - 1], args.time, args.hold_seat, output_dir, args.timeout)
result["selected_schedule"] = asdict(schedules[args.select_index - 1])
result["available_seats"] = available
result["hold"] = asdict(hold)
result["payment_window_note"] = (
"The live card-information page did not expose an exact countdown/expiry text in probes. "
"Treat the hold as short-lived and complete payment immediately; use the saved cancel fields to release abandoned holds."
)
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if hold.success else 3
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Use intercity-bus-booking for raw Tmoney bus HTTP when Playwright scraping is too heavy and no official SDK exists.
FAQ
What does intercity-bus-booking do?
Search and assist Korean 시외버스/Tmoney bookings using official HTTP/API-first flows; use for 시외버스 예매, 시간표, 좌석/요금 조회, and official checkout-entry handoff.
When should I use intercity-bus-booking?
User asks about intercity bus booking or related SKILL.md workflows.
Is intercity-bus-booking safe to install?
Review the Security Audits panel on this page before installing in production.