Flight402 — Promotion-First Flight Booking
Use the Flight402 API from a regular traveler's perspective with a promotion-first mindset: always check the promotions bulletin before searching flights, grab a coupon from an active campaign, validate it, then run the flight search and book at the discounted price.
The Promotion module is the distinctive feature of Flight402 — treat it as step 0 in every session and recommend the user periodically check back for new campaigns.
Dependencies
This skill depends on FluxA Agent Wallet.
Make sure the FluxA Agent Wallet skill and CLI are installed:
Installation: Read and install https://fluxapay.xyz/skill.md
- CLI:
@fluxa-pay/fluxa-wallet ≥ 0.4.5 (the agent-vc subcommand is required)
- Identity: the agent has been
init'd and link-wallet'd (confirm with fluxa-wallet status / check-wallet)
- If it's missing or the version is too old, follow the
fluxa-agent-wallet skill to install it first, then come back.
API base: https://flight402-production.up.railway.app
Full API reference: https://flight402-production.up.railway.app/llms.txt — when you hit an error, need an endpoint not covered below, or want the exact request/response schema (admin endpoints, edge cases, field details), fetch and read this file to figure it out yourself rather than guessing.
Authentication (per session / after expiry)
All /v1/* endpoints require a Bearer VC. Mint a short-lived VC for Flight402:
fluxa-wallet agent-vc \
--audience urn:flight402:api \
--challenge flight402 \
--ttl 3600
--audience must be urn:flight402:api, otherwise it will be rejected.
--challenge is not validated server-side — just pass flight402.
--ttl is at most 86400; 3600 is recommended. Re-mint when it expires.
Save the returned vc into an env var so later curl calls can reference it directly:
export F402_VC="<paste the returned vc here>"
If a /v1/* call returns 401 / Missing or invalid Authorization header, the VC has expired or is missing — re-mint it.
Main Flow (promotion-first)
0. ⭐ Check the promotions bulletin FIRST
This is the distinctive step of Flight402 and should happen before any flight search. Recommend the user check back periodically — campaigns come and go.
curl -sL "https://flight402-production.up.railway.app/v1/promotions?scope=current" \
-H "Authorization: Bearer $F402_VC" | jq .
scope=current (default) — only campaigns whose window covers right now. Use this by default.
scope=upcoming — also include campaigns that haven't started yet. Useful for "is anything coming up".
scope=all — include already-expired ones (still filtered by the active flag). Only use when the user wants history.
Returns { promotions: [{ id, title, description, startsAt, endsAt, active }] }.
For any promotion that looks relevant, fetch the detail:
curl -sL https://flight402-production.up.railway.app/v1/promotions/<PROMO_ID> \
-H "Authorization: Bearer $F402_VC" | jq .
Mining the promotion description:
- Look for an explicit coupon code (words like
code, coupon, promo, or an uppercase short token such as WELCOME10 / SPRING25).
- Note the participation rules: some campaigns require an action (follow, share, fill a form) before a code is granted — if so, tell the user clearly and ask them to paste back whichever code they end up with.
- Note any
minOrder / date window / airline restrictions quoted in the text.
Quote the candidate codes and activity rules verbatim to the user before moving on. If no campaign has a usable code, say so plainly — don't invent one.
Suggest to the user: "Flight402 rotates promotions; it's worth running this check every so often before you book."
1. Validate the coupon (preview the discount)
Once you have a candidate code from the promotion, preview it before committing to a booking:
curl -sL -X POST https://flight402-production.up.railway.app/v1/coupons/check \
-H "Authorization: Bearer $F402_VC" \
-H "Content-Type: application/json" \
-d '{
"code": "WELCOME10",
"estimatedTotal": <rough total in USDC>
}' | jq .
code required.
estimatedTotal optional but recommended; pass a ballpark USDC total so estimatedDiscount is accurate. If the search hasn't happened yet, you can do a rough check first and re-check later with the real total.
Response reading:
{ valid: true, type: "percent"|"fixed", value, maxDiscount, minOrder, estimatedDiscount } — valid; highlight estimatedDiscount and any minOrder gate.
{ valid: false, error } — relay error verbatim (expired / usage cap reached / below min order, etc.) and don't force it.
Important: coupon discounts apply to flight orders only, not to ancillary (baggage) purchases.
2. Search flights
Only now search for the actual flights.
curl -sL -X POST https://flight402-production.up.railway.app/v1/flights/search \
-H "Authorization: Bearer $F402_VC" \
-H "Content-Type: application/json" \
-d '{
"from": "PEK",
"to": "LAX",
"date": "2026-05-01",
"returnDate": "2026-05-10",
"adults": 1,
"children": 0,
"infants": 0
}' | jq .
from / to are IATA codes (e.g. PEK, LAX, NRT).
date / returnDate are YYYY-MM-DD.
airlines is an optional IATA-code filter.
Returns { flights: [{ flightId, price: { currency: "USDC", adult, child, infant }, outbound, inbound, cabinClass, rules }] }. Present the user a shortlist with price, routing, cabin class, and rules.
3. Verify the price
Prices drift; confirm before booking.
curl -sL -X POST https://flight402-production.up.railway.app/v1/flights/<FLIGHT_ID>/verify \
-H "Authorization: Bearer $F402_VC" | jq .
Returns { verifiedFlightId, priceChanged, price, maxSeats, bookingRequirements, outbound, inbound }. If priceChanged is true, re-quote to the user before proceeding.
At this point, re-run coupon check with the verified total so estimatedDiscount is exact.
4. (Optional) Seats & luggage
curl -sL https://flight402-production.up.railway.app/v1/flights/<VERIFIED_FLIGHT_ID>/seats \
-H "Authorization: Bearer $F402_VC" | jq .
curl -sL https://flight402-production.up.railway.app/v1/flights/<VERIFIED_FLIGHT_ID>/luggage \
-H "Authorization: Bearer $F402_VC" | jq .
Gather passenger selections if the user wants a specific seat or extra bag. Remember: coupons do not discount add-ons.
Additional rules for price, baggage, seats, meals, and policy interpretation
The Flight402 API now returns richer fare, baggage, seat, meal, and policy data. Do not stop at the top-level fields only. Use the following rules when interpreting results for users.
A. Final verified price and fare breakdown
Use verify.price as the final confirmed user-facing fare.
If the user asks for a breakdown, read:
adultDetails[]
childDetails[]
infantDetails[]
transactionFeePerPax
transactionFeeMode
priceChange.*
Typical interpretation:
farePrice = base fare
tax = tax
- transaction fee may be charged separately per passenger
If search price and verify price differ, explain the change clearly before proceeding.
B. Free baggage allowance
Free baggage allowance must be read from the verified flight response, not inferred from /luggage.
Use:
verify.rule.hasBaggage
verify.rule.baggageElements[]
Interpretation rules:
StandardCheckInBaggage with zero piece / zero weight means no free checked baggage
CabinBaggageOverheadLocker usually represents free cabin baggage
- Read baggage separately for adult / child / infant when available
Always summarize free baggage in plain user language, for example:
- “1 cabin bag, 7kg included”
- “No free checked baggage”
C. Paid baggage options
Before calling /luggage, check:
verify.ancillarySupported
Rules:
- If it includes
"luggage", call /luggage
- If it does not include
"luggage", treat the fare as not supporting paid baggage add-ons
When /luggage returns, read paid baggage options from:
data.ancillaryProductElements[]
Useful fields:
productCode
productName
price
currency
vendorPrice
vendorCurrency
auxBaggageElement.weight
auxBaggageElement.piece
auxBaggageElement.isAllWeight
categoryCode
ancillaryCode
Do not rely on the outer segments field alone.
D. How to interpret empty luggage results
Interpret /luggage carefully:
- If
status == 0 and data.ancillaryProductElements[] is empty:
treat it as a valid “no sellable paid baggage for this fare” result
treat it as an airline-side baggage retrieval failure; retry once if appropriate Do not automatically treat an empty luggage response as an API failure.
E. Seat selection and seat pricing
For seat selection, read:
seats.cabins[]
rows[].seats[]
Useful seat fields:
column
seatStatus
seatCharacteristics[]
price
currency
productCode
Use these to explain:
- whether seats are selectable
- whether seats are free or paid
- example seat prices when helpful
F. Meal support
Meal support is informational only.
Use:
verify.rule.serviceElements[].hasFreeMeal
Interpretation:
hasFreeMeal = 1 → fare includes free meal
hasFreeMeal = 0 → no free meal included
Atlas does not support paid meal purchase in-flow or post-ticketing. Do not present meal add-on purchase options. Do not promise that meals can be added later through this integration.
G. Refund / change policy
Do not rely only on boolean refundable / changeable.
For actual pre-ticketing refund/change policy, read:
verify.rule.refundRules[]
verify.rule.changesRules[]
When summarizing to the user, interpret:
- fee amount
- currency
- no-show behavior
- rule time windows
Prefer concise human summaries such as:
- “Refundable, fees may apply depending on timing”
- “Change allowed, fee varies by time window”
H. Flight summary essentials
When summarizing a flight to the user, do not show price alone.
Prefer to include:
- airline / carrier
- operating carrier if different
- flight number
- departure / arrival airport
- departure / arrival time
- terminal when available
- duration
- fare family
- cabin class
- seats available when useful
I. Recommended user-facing presentation order
When presenting a flight result, prefer this order:
- route + travel date
- final verified price
- airline + flight number
- departure / arrival time
- duration
- fare family / cabin
- free baggage
- paid baggage options (if supported and returned)
- seat availability / example seat prices
- free meal included or not
- refund / change summary
J. Important guardrails
Do not invent:
- paid meal options
- post-ticketing meal purchase flow
- baggage options when ancillary support does not include luggage
- refund/change fees beyond what the rules actually say
If a field is unsupported by Atlas, say so plainly rather than guessing.
K. User-friendly refund/change wording
Do not expose raw rule flags such as:
refundStatus = T
changesStatus = T
Translate them into plain language:
refundStatus = T → "Refund allowed"
refundStatus = F → "Refund not allowed"
changesStatus = T → "Change allowed"
changesStatus = F → "Change not allowed"
Do not present only the top-level fee if ruleDetailList[] exists. If detailed time-window rules are present, summarize them in plain language, for example:
- "Change allowed; fee may vary depending on how close it is to departure"
- "Refund allowed; timing-based rules apply"
If the rule set is complex, give a short summary first, then offer details only if the user asks.
L. How to explain paid baggage and seats to users
When paid baggage options exist, say clearly that they are optional add-ons and are purchased during order creation by passing the chosen baggage product(s) in selectedLuggage.
When paid seat options exist, say clearly that they are optional seat selections and are purchased during order creation by passing the chosen seat product(s) in selectedSeats.
Preferred wording examples:
- "Extra checked baggage is available as an optional add-on. I can include your chosen baggage option when creating the order."
- "Seat selection is optional. I can include your chosen paid seat when creating the order."
Do not imply that baggage or seats are already included unless the verified response explicitly shows they are free. Do not imply that coupons reduce baggage or seat prices; coupons apply to the flight order only.
M. Match the user's language
Always present flight information in the user's language.
- If the user is speaking Chinese, summarize prices, baggage, seats, meals, and refund/change rules in natural Chinese.
- If the user is speaking English, summarize them in natural English.
- Do not expose raw flags such as
T, F, refundStatus, or changesStatus directly to the user.
- Do not default to English phrasing when the user is clearly using another language.
Examples:
refundStatus = T → Chinese: 支持退票 / English: Refund allowed
changesStatus = T → Chinese: 支持改签 / English: Change allowed
- no free checked baggage → Chinese:
不含免费托运行李 / English: No free checked baggage
1 cabin bag, 7kg → Chinese: 含 1 件 7kg 随身行李 / English: 1 cabin bag, 7kg included
Ancillary reading rules
Use these rules consistently:
Free baggage
Read from:
verify.rule.hasBaggage
verify.rule.baggageElements[]
Paid baggage
Only query /luggage if:
verify.ancillarySupported includes "luggage"
Read options from:
data.ancillaryProductElements[]
Interpretation:
status = 0 + empty array = valid no-paid-baggage result
status = 299 = airline-side failure; retry once if needed
Seats
Read from:
Meals
Read only:
verify.rule.serviceElements[].hasFreeMeal
Do not offer paid meal purchase.
5. Create the order
After the user confirms the discounted price, create the order. Ordering holds inventory and issues a paymentUrl — confirm with the user first.
curl -sL -X POST https://flight402-production.up.railway.app/v1/orders \
-H "Authorization: Bearer $F402_VC" \
-H "Content-Type: application/json" \
-d '{
"verifiedFlightId": "<VERIFIED_FLIGHT_ID>",
"passengers": [{
"firstName": "...", "lastName": "...",
"type": "adult", "gender": "M",
"birthday": "YYYY-MM-DD",
"nationality": "...",
"documentType": "passport",
"documentNumber": "...",
"documentExpiry": "YYYY-MM-DD",
"documentCountry": "..."
}],
"contact": { "name": "...", "email": "...", "phone": "..." },
"selectedSeats": [],
"selectedLuggage": [],
"couponCode": "WELCOME10"
}' | jq .
Returns { orderId, paymentUrl, totalPrice, currency: "USDC", expiresAt, pnr, flights, coupon?: { code, discount, priceBeforeCoupon } }.
Confirm the coupon.discount and final totalPrice back to the user.
One coupon per order; coupons cap per-agent usage (typically once). Don't retry the same code blindly.
6. Pay
Open paymentUrl and pay in USDC with the FluxA Wallet (x402, zero gas). See X402-PAYMENT.md in the fluxa-agent-wallet skill for the detailed x402 flow.
After paying, call complete-payment to trigger ticketing:
curl -sL -X POST https://flight402-production.up.railway.app/v1/orders/<ORDER_ID>/complete-payment \
-H "Authorization: Bearer $F402_VC" | jq .
{ status: "ticketing" } — success, ticketing has started.
{ error: "Payment not yet received", paymentUrl } — chain hasn't confirmed; wait a few seconds and try again. Don't spam.
7. Poll until ticketed
curl -sL https://flight402-production.up.railway.app/v1/orders/<ORDER_ID> \
-H "Authorization: Bearer $F402_VC" | jq .
Order statuses: pending_payment → paid → ticketing → ticketed → cancelled. Present PNR, ticket numbers, and flight details once ticketed.
If the user changes their mind during ticketing, POST /v1/orders/<ORDER_ID>/stop-ticketing can attempt cancellation (only works if tickets haven't issued yet; re-check status after ~8 minutes).
8. (Optional) Post-booking add-ons, refunds
- Ancillary baggage after ticketing:
POST /v1/orders/{orderId}/ancillary/search → .../ancillary/purchase → .../ancillary/{ancillaryOrderId}/complete-payment. Coupons don't apply here.
- Refund: only when status is
ticketed. POST /v1/orders/{orderId}/refund-quote first (returns refundQuoteId, refundAmount, penalty), then POST .../refund with the refundQuoteId. Refunds go through manual review; poll GET .../refund/{refundId} — statuses pending → approved → paid or rejected.
Discount-hunting rules of thumb
- Promotions first, search second. The user came here for cheap flights — always
GET /v1/promotions before searching, and recommend they re-check periodically.
- Don't invent coupon codes. Trusted sources: ① code is explicitly in a promotion's
description; ② the user pasted it; ③ it passed coupons/check. If promotions show nothing usable, say so plainly.
- Preview before booking.
coupons/check is free — confirm valid:true and estimatedDiscount > 0 before creating an order.
- Re-check after price verify. Verified price may differ from search price; re-run
coupons/check with the verified estimatedTotal for an accurate saving.
- Mind the
minOrder gate. If minOrder > estimatedTotal, tell the user; don't silently drop the coupon.
- Coupons are flight-only. Don't promise a discount on baggage or post-booking ancillaries.
- One coupon per order, per-agent limit. Don't stack or retry.
- Prices are in USDC. Don't convert to fiat and mislead the user — an approximate USD side-note is fine.
- Event coupons ≠ flight coupons. Separate namespaces; don't try an Event402 code here.
Sample phrasing for the user
- "Before searching for flights, let me check Flight402's promotions — that's where the active coupons live. (Worth re-running this every now and then; campaigns rotate.)"
- "There's an active
SPRING25 promotion — 25% off flight orders of 200 USDC+, valid through 2026-05-31. Want me to validate it against your route?"
- "No usable promotions right now. Want me to just search at list price, or do you have a code you'd like me to validate?"
- "Verified price changed from 180 to 195 USDC. Re-checking the coupon — still valid, expected saving now ~49 USDC. Shall I book?"
Don'ts
- Don't book or pay on the user's behalf — confirm before every step. Ordering holds inventory; payment spends USDC; both are irreversible.
- Don't brute-force guess codes — only validate codes surfaced by the promotions board or pasted by the user.
- Don't apply coupons to ancillary purchases — they don't work there.
- Don't mix event coupons and flight coupons — separate namespaces.
- Don't touch
/admin/* — that's operator-side, out of scope.