@furlpay/travel-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/server.ts ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * FurlPay Travels MCP server (stdio transport, JSON-RPC 2.0).
4
+ *
5
+ * Composes Travala's Travel MCP (search) with FurlPay's payment rails (pay) and
6
+ * exposes them as MCP tools for Claude, Cursor, and AI agents. Runs on demo data
7
+ * with no keys.
8
+ *
9
+ * Configure in an MCP client:
10
+ * {
11
+ * "mcpServers": {
12
+ * "furlpay-travels": {
13
+ * "command": "npx", "args": ["-y", "@furlpay/travel-mcp"],
14
+ * "env": { "FURLPAY_API_KEY": "fp_live_sk_...", "TRAVALA_API_KEY": "..." }
15
+ * }
16
+ * }
17
+ * }
18
+ */
19
+ import { buildTools } from "./tools";
20
+ import { TravelClient } from "./travel";
21
+
22
+ const client = new TravelClient();
23
+ const tools = buildTools(client);
24
+
25
+ function send(msg: unknown): void {
26
+ process.stdout.write(JSON.stringify(msg) + "\n");
27
+ }
28
+
29
+ async function handle(line: string): Promise<void> {
30
+ let req: any;
31
+ try {
32
+ req = JSON.parse(line);
33
+ } catch {
34
+ return;
35
+ }
36
+ const { id, method, params } = req;
37
+ try {
38
+ if (method === "initialize") {
39
+ return send({
40
+ jsonrpc: "2.0",
41
+ id,
42
+ result: {
43
+ protocolVersion: "2024-11-05",
44
+ capabilities: { tools: {} },
45
+ serverInfo: { name: "furlpay-travels", version: "0.1.0" },
46
+ },
47
+ });
48
+ }
49
+ if (method === "tools/list") {
50
+ return send({
51
+ jsonrpc: "2.0",
52
+ id,
53
+ result: { tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) },
54
+ });
55
+ }
56
+ if (method === "tools/call") {
57
+ const tool = tools.find((t) => t.name === params?.name);
58
+ if (!tool) throw new Error(`Unknown tool: ${params?.name}`);
59
+ const result = await tool.handler(params.arguments || {});
60
+ return send({
61
+ jsonrpc: "2.0",
62
+ id,
63
+ result: { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] },
64
+ });
65
+ }
66
+ if (typeof method === "string" && method.startsWith("notifications/")) return;
67
+ throw new Error(`Method not found: ${method}`);
68
+ } catch (e) {
69
+ send({ jsonrpc: "2.0", id, error: { code: -32000, message: (e as Error).message } });
70
+ }
71
+ }
72
+
73
+ let buffer = "";
74
+ process.stdin.setEncoding("utf8");
75
+ process.stdin.on("data", (chunk) => {
76
+ buffer += chunk;
77
+ let idx: number;
78
+ while ((idx = buffer.indexOf("\n")) >= 0) {
79
+ const line = buffer.slice(0, idx).trim();
80
+ buffer = buffer.slice(idx + 1);
81
+ if (line) void handle(line);
82
+ }
83
+ });
84
+
85
+ process.stderr.write(`furlpay-travels MCP ready (${client.live ? "live" : "demo"} mode)\n`);
package/src/tools.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { TravelClient } from "./travel";
2
+
3
+ export interface McpTool {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: Record<string, unknown>;
7
+ handler: (args: any) => Promise<unknown> | unknown;
8
+ }
9
+
10
+ /** Build the MCP toolset bound to a TravelClient. */
11
+ export function buildTools(client: TravelClient): McpTool[] {
12
+ return [
13
+ {
14
+ name: "travel_search_stays",
15
+ description:
16
+ "Search Travala's 2.2M+ hotels (Marriott, Hilton, IHG, …) for a city and date range. Returns quotes with a quoteId to book.",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ city: { type: "string" },
21
+ checkIn: { type: "string", description: "YYYY-MM-DD" },
22
+ checkOut: { type: "string", description: "YYYY-MM-DD" },
23
+ maxNightlyUsd: { type: "number", description: "Optional nightly price cap in USD" },
24
+ guests: { type: "number" },
25
+ },
26
+ required: ["city", "checkIn", "checkOut"],
27
+ },
28
+ handler: (a) => client.searchStays(a),
29
+ },
30
+ {
31
+ name: "travel_search_flights",
32
+ description: "Search flights via Travala for a route and date. Returns quotes with a quoteId.",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ from: { type: "string", description: "Origin IATA code" },
37
+ to: { type: "string", description: "Destination IATA code" },
38
+ date: { type: "string", description: "YYYY-MM-DD" },
39
+ },
40
+ required: ["from", "to", "date"],
41
+ },
42
+ handler: (a) => client.searchFlights(a),
43
+ },
44
+ {
45
+ name: "travel_set_agent_budget",
46
+ description: "Set a USDC spend cap for an agent before it books autonomously.",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: { agentId: { type: "string" }, limitUsd: { type: "number" } },
50
+ required: ["agentId", "limitUsd"],
51
+ },
52
+ handler: (a) => {
53
+ client.setAgentBudget(a.agentId, a.limitUsd);
54
+ return client.getAgentBudget(a.agentId);
55
+ },
56
+ },
57
+ {
58
+ name: "travel_authorize_booking",
59
+ description:
60
+ "Authorize payment for a booking within the agent's budget. source='travala' pays via x402/USDC on Base and accrues the 10% cbBTC rebate; source='legacy' issues a single-use, MCC-locked Visa virtual card for Web2 merchants (Airbnb, Skyscanner).",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ amountUsd: { type: "number" },
65
+ source: { type: "string", enum: ["travala", "legacy"] },
66
+ currency: { type: "string", description: "Default USDC" },
67
+ mcc: { type: "string", description: "Merchant Category Code lock for the legacy route, e.g. 7011 (lodging)" },
68
+ agentId: { type: "string" },
69
+ reference: { type: "string", description: "Quote/booking reference" },
70
+ },
71
+ required: ["amountUsd", "source"],
72
+ },
73
+ handler: (a) => client.authorizeBooking(a),
74
+ },
75
+ {
76
+ name: "travel_confirm_booking",
77
+ description: "Confirm an authorized booking (after passkey step-up in a real flow).",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: { bookingId: { type: "string" } },
81
+ required: ["bookingId"],
82
+ },
83
+ handler: (a) => client.confirmBooking(a.bookingId),
84
+ },
85
+ {
86
+ name: "travel_cancel_booking",
87
+ description: "Cancel a booking and void its authorization.",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: { bookingId: { type: "string" } },
91
+ required: ["bookingId"],
92
+ },
93
+ handler: (a) => client.cancelBooking(a.bookingId),
94
+ },
95
+ {
96
+ name: "travel_list_rebates",
97
+ description: "List accumulated 10% cbBTC developer rebates from Travala-routed bookings (7% developer / 3% treasury split).",
98
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
99
+ handler: () => client.listRebates(),
100
+ },
101
+ ];
102
+ }
package/src/travala.ts ADDED
@@ -0,0 +1,169 @@
1
+ import { Flight, Stay } from "./types";
2
+
3
+ /**
4
+ * Thin client for the Travala Travel MCP (search side of the composition).
5
+ *
6
+ * Travala's Agentic AI Travel Protocol exposes 2.2M+ hotels (Marriott, Hilton,
7
+ * IHG, …) — expanding to flights — to AI agents, settling in gasless USDC over
8
+ * x402 on Base. This wrapper is search-only; payment/settlement is FurlPay's job.
9
+ *
10
+ * With no key set it returns deterministic demo inventory so the whole
11
+ * search → pay → book loop runs offline.
12
+ */
13
+
14
+ const DEFAULT_MCP = "https://travel-mcp.travala.com/mcp";
15
+ const round2 = (n: number) => Math.round(n * 100) / 100;
16
+
17
+ function hash(s: string): number {
18
+ let h = 2166136261;
19
+ for (let i = 0; i < s.length; i++) {
20
+ h ^= s.charCodeAt(i);
21
+ h = Math.imul(h, 16777619);
22
+ }
23
+ return h >>> 0;
24
+ }
25
+
26
+ const HOTEL_BRANDS = ["Marriott", "Hilton", "IHG", "Accor", "Hyatt"];
27
+
28
+ function nightsBetween(checkIn: string, checkOut: string): number {
29
+ const a = Date.parse(checkIn);
30
+ const b = Date.parse(checkOut);
31
+ if (isNaN(a) || isNaN(b) || b <= a) return 1;
32
+ return Math.max(1, Math.round((b - a) / 86_400_000));
33
+ }
34
+
35
+ export class TravalaClient {
36
+ constructor(
37
+ private readonly url: string = process.env.TRAVALA_MCP_URL || DEFAULT_MCP,
38
+ private readonly apiKey?: string,
39
+ private readonly fetchImpl: typeof fetch = globalThis.fetch,
40
+ ) {}
41
+
42
+ get live(): boolean {
43
+ return Boolean(this.apiKey);
44
+ }
45
+
46
+ async searchStays(params: {
47
+ city: string;
48
+ checkIn: string;
49
+ checkOut: string;
50
+ maxNightlyUsd?: number;
51
+ guests?: number;
52
+ }): Promise<Stay[]> {
53
+ const nights = nightsBetween(params.checkIn, params.checkOut);
54
+ if (!this.live) return this.demoStays(params, nights);
55
+ try {
56
+ const rows = await this.call("search_stays", { ...params });
57
+ const stays = (rows as any[]).map((r) => normaliseStay(r, params, nights));
58
+ return params.maxNightlyUsd ? stays.filter((s) => s.nightlyUsd <= params.maxNightlyUsd!) : stays;
59
+ } catch {
60
+ return this.demoStays(params, nights);
61
+ }
62
+ }
63
+
64
+ async searchFlights(params: { from: string; to: string; date: string }): Promise<Flight[]> {
65
+ if (!this.live) return this.demoFlights(params);
66
+ try {
67
+ const rows = await this.call("search_flights", { ...params });
68
+ return (rows as any[]).map((r) => normaliseFlight(r, params));
69
+ } catch {
70
+ return this.demoFlights(params);
71
+ }
72
+ }
73
+
74
+ // -- transport (Travala MCP is JSON-RPC over HTTP) ----------------------
75
+
76
+ private async call(tool: string, args: Record<string, unknown>): Promise<unknown> {
77
+ const res = await this.fetchImpl(this.url, {
78
+ method: "POST",
79
+ headers: {
80
+ "Content-Type": "application/json",
81
+ Accept: "application/json",
82
+ ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
83
+ },
84
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: tool, arguments: args } }),
85
+ });
86
+ if (!res.ok) throw new Error(`Travala MCP HTTP ${res.status}`);
87
+ const json: any = await res.json();
88
+ if (json.error) throw new Error(json.error.message || "Travala MCP error");
89
+ const content = json.result?.content?.[0];
90
+ const text = content?.text ?? "[]";
91
+ return typeof text === "string" ? JSON.parse(text) : text;
92
+ }
93
+
94
+ // -- deterministic demo inventory ---------------------------------------
95
+
96
+ private demoStays(
97
+ params: { city: string; checkIn: string; checkOut: string; maxNightlyUsd?: number; guests?: number },
98
+ nights: number,
99
+ ): Stay[] {
100
+ const seed = hash(params.city.toLowerCase());
101
+ const stays: Stay[] = Array.from({ length: 5 }, (_, i) => {
102
+ const brand = HOTEL_BRANDS[(seed + i) % HOTEL_BRANDS.length];
103
+ const nightly = round2(80 + ((seed >> (i + 1)) % 260)); // ~$80–$340
104
+ return {
105
+ quoteId: `tvl_demo_${(seed % 0xffff).toString(16)}_${i}`,
106
+ name: `${brand} ${titleCase(params.city)} ${["Central", "Riverside", "Airport", "Old Town", "Bay"][i]}`,
107
+ brand,
108
+ city: titleCase(params.city),
109
+ stars: 3 + (i % 3),
110
+ checkIn: params.checkIn,
111
+ checkOut: params.checkOut,
112
+ nights,
113
+ nightlyUsd: nightly,
114
+ totalUsd: round2(nightly * nights),
115
+ source: "demo" as const,
116
+ };
117
+ });
118
+ return params.maxNightlyUsd ? stays.filter((s) => s.nightlyUsd <= params.maxNightlyUsd!) : stays;
119
+ }
120
+
121
+ private demoFlights(params: { from: string; to: string; date: string }): Flight[] {
122
+ const seed = hash(params.from + params.to);
123
+ const carriers = ["BA", "LH", "AF", "EK", "QR"];
124
+ return Array.from({ length: 3 }, (_, i) => ({
125
+ quoteId: `tvlf_demo_${(seed % 0xffff).toString(16)}_${i}`,
126
+ carrier: carriers[(seed + i) % carriers.length],
127
+ from: params.from.toUpperCase(),
128
+ to: params.to.toUpperCase(),
129
+ date: params.date,
130
+ cabin: ["economy", "premium", "business"][i % 3],
131
+ totalUsd: round2(180 + ((seed >> (i + 2)) % 900)),
132
+ source: "demo" as const,
133
+ }));
134
+ }
135
+ }
136
+
137
+ function normaliseStay(r: any, p: { city: string; checkIn: string; checkOut: string }, nights: number): Stay {
138
+ const nightly = Number(r.nightlyUsd ?? r.price ?? 0);
139
+ return {
140
+ quoteId: String(r.quoteId ?? r.id ?? ""),
141
+ name: String(r.name ?? "Hotel"),
142
+ brand: String(r.brand ?? "Independent"),
143
+ city: String(r.city ?? p.city),
144
+ stars: Number(r.stars ?? 3),
145
+ checkIn: p.checkIn,
146
+ checkOut: p.checkOut,
147
+ nights,
148
+ nightlyUsd: nightly,
149
+ totalUsd: Number(r.totalUsd ?? nightly * nights),
150
+ source: "travala",
151
+ };
152
+ }
153
+
154
+ function normaliseFlight(r: any, p: { from: string; to: string; date: string }): Flight {
155
+ return {
156
+ quoteId: String(r.quoteId ?? r.id ?? ""),
157
+ carrier: String(r.carrier ?? "XX"),
158
+ from: p.from.toUpperCase(),
159
+ to: p.to.toUpperCase(),
160
+ date: p.date,
161
+ cabin: String(r.cabin ?? "economy"),
162
+ totalUsd: Number(r.totalUsd ?? r.price ?? 0),
163
+ source: "travala",
164
+ };
165
+ }
166
+
167
+ function titleCase(s: string): string {
168
+ return s.replace(/\w\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());
169
+ }
package/src/travel.ts ADDED
@@ -0,0 +1,184 @@
1
+ import { FurlPayPay, MCC } from "./furlpay";
2
+ import { TravalaClient } from "./travala";
3
+ import {
4
+ Booking,
5
+ BookingSource,
6
+ Flight,
7
+ RebateAccrual,
8
+ Stay,
9
+ TravelOptions,
10
+ } from "./types";
11
+
12
+ export { MCC } from "./furlpay";
13
+ export * from "./types";
14
+
15
+ // Travala pays a 10% cbBTC developer rebate on MCP-driven bookings. We split it
16
+ // 7% to the integrating developer, 3% to the FurlPay treasury.
17
+ const REBATE_RATE = 0.1;
18
+ const DEVELOPER_SHARE = 0.7;
19
+
20
+ /**
21
+ * FurlPay Travels — composes Travala's Travel MCP (search) with FurlPay's payment
22
+ * rails (pay). An agent searches inventory, FurlPay checks the spend budget and
23
+ * issues a credential (x402 proof for the Travala/Base route, or a single-use
24
+ * MCC-locked Visa VCN for legacy merchants), and the booking accrues the 10%
25
+ * cbBTC rebate.
26
+ *
27
+ * Clone-and-run: with no keys, search and payment simulate end-to-end.
28
+ */
29
+ export class TravelClient {
30
+ private readonly travala: TravalaClient;
31
+ private readonly pay: FurlPayPay;
32
+ private readonly developerWallet?: string;
33
+ private readonly bookings = new Map<string, Booking>();
34
+ /** Per-agent USDC spend caps for autonomous booking. */
35
+ private readonly budgets = new Map<string, { limit: number; spent: number }>();
36
+
37
+ constructor(opts: TravelOptions = {}) {
38
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
39
+ this.travala = new TravalaClient(
40
+ opts.travalaMcpUrl || process.env.TRAVALA_MCP_URL,
41
+ process.env.TRAVALA_API_KEY,
42
+ fetchImpl,
43
+ );
44
+ this.pay = new FurlPayPay(
45
+ opts.furlpayApiKey || process.env.FURLPAY_API_KEY,
46
+ opts.furlpayBaseUrl,
47
+ fetchImpl,
48
+ );
49
+ this.developerWallet = opts.developerWallet || process.env.FURLPAY_DEVELOPER_WALLET;
50
+ }
51
+
52
+ get live(): boolean {
53
+ return this.pay.live;
54
+ }
55
+
56
+ // -- search -------------------------------------------------------------
57
+
58
+ searchStays(p: { city: string; checkIn: string; checkOut: string; maxNightlyUsd?: number; guests?: number }): Promise<Stay[]> {
59
+ return this.travala.searchStays(p);
60
+ }
61
+
62
+ searchFlights(p: { from: string; to: string; date: string }): Promise<Flight[]> {
63
+ return this.travala.searchFlights(p);
64
+ }
65
+
66
+ // -- budget policy ------------------------------------------------------
67
+
68
+ setAgentBudget(agentId: string, limitUsd: number): void {
69
+ const existing = this.budgets.get(agentId);
70
+ this.budgets.set(agentId, { limit: limitUsd, spent: existing?.spent ?? 0 });
71
+ }
72
+
73
+ getAgentBudget(agentId: string): { limit: number; spent: number; remaining: number } {
74
+ const b = this.budgets.get(agentId) ?? { limit: Infinity, spent: 0 };
75
+ return { limit: b.limit, spent: b.spent, remaining: b.limit - b.spent };
76
+ }
77
+
78
+ // -- book ---------------------------------------------------------------
79
+
80
+ /**
81
+ * Authorize a booking. `source: "travala"` pays via x402/USDC on Base; any other
82
+ * merchant uses `source: "legacy"` → a single-use MCC-locked Visa VCN.
83
+ * Enforces the agent budget before authorizing.
84
+ */
85
+ async authorizeBooking(params: {
86
+ amountUsd: number;
87
+ source: BookingSource;
88
+ currency?: string;
89
+ mcc?: string;
90
+ agentId?: string;
91
+ reference?: string;
92
+ }): Promise<Booking> {
93
+ if (params.amountUsd <= 0) throw new Error("amountUsd must be > 0");
94
+
95
+ const agentId = params.agentId ?? "agent_default";
96
+ const budget = this.budgets.get(agentId);
97
+ if (budget && budget.spent + params.amountUsd > budget.limit) {
98
+ throw new Error(
99
+ `booking $${params.amountUsd} exceeds remaining budget $${(budget.limit - budget.spent).toFixed(2)} for ${agentId}`,
100
+ );
101
+ }
102
+
103
+ const authorization = await this.pay.authorize({
104
+ route: params.source,
105
+ amountUsd: params.amountUsd,
106
+ currency: params.currency,
107
+ mcc: params.mcc ?? (params.source === "legacy" ? MCC.LODGING : undefined),
108
+ });
109
+
110
+ const booking: Booking = {
111
+ id: "bk_" + Math.random().toString(36).slice(2, 10),
112
+ source: params.source,
113
+ reference: params.reference ?? "(unassigned)",
114
+ amountUsd: params.amountUsd,
115
+ status: "authorized",
116
+ authorization,
117
+ rebate: params.source === "travala" ? this.accrueRebate("", params.amountUsd) : undefined,
118
+ createdAt: new Date().toISOString(),
119
+ };
120
+ if (booking.rebate) booking.rebate.bookingId = booking.id;
121
+
122
+ if (budget) budget.spent += params.amountUsd;
123
+ this.bookings.set(booking.id, booking);
124
+ return booking;
125
+ }
126
+
127
+ /** Mark an authorized booking confirmed (after passkey step-up in a real flow). */
128
+ confirmBooking(id: string): Booking {
129
+ const b = this.require(id);
130
+ if (b.status === "cancelled") throw new Error(`booking ${id} is cancelled`);
131
+ b.status = "confirmed";
132
+ return b;
133
+ }
134
+
135
+ cancelBooking(id: string): Booking {
136
+ const b = this.require(id);
137
+ b.status = "cancelled";
138
+ const budget = this.budgets.get("agent_default");
139
+ return b;
140
+ }
141
+
142
+ getBooking(id: string): Booking | undefined {
143
+ return this.bookings.get(id);
144
+ }
145
+
146
+ // -- rebates ------------------------------------------------------------
147
+
148
+ /** All cbBTC rebate accruals to date and the developer's cumulative share. */
149
+ listRebates(): { developerWallet?: string; accruals: RebateAccrual[]; developerTotalUsd: number; treasuryTotalUsd: number } {
150
+ const accruals = [...this.bookings.values()]
151
+ .filter((b) => b.rebate && b.status !== "cancelled")
152
+ .map((b) => b.rebate!) as RebateAccrual[];
153
+ return {
154
+ developerWallet: this.developerWallet,
155
+ accruals,
156
+ developerTotalUsd: round2(accruals.reduce((s, r) => s + r.developerUsd, 0)),
157
+ treasuryTotalUsd: round2(accruals.reduce((s, r) => s + r.treasuryUsd, 0)),
158
+ };
159
+ }
160
+
161
+ private accrueRebate(bookingId: string, amountUsd: number): RebateAccrual {
162
+ const total = round2(amountUsd * REBATE_RATE);
163
+ const developerUsd = round2(total * DEVELOPER_SHARE);
164
+ return {
165
+ bookingId,
166
+ currency: "cbBTC",
167
+ totalUsd: total,
168
+ developerUsd,
169
+ treasuryUsd: round2(total - developerUsd),
170
+ };
171
+ }
172
+
173
+ private require(id: string): Booking {
174
+ const b = this.bookings.get(id);
175
+ if (!b) throw new Error(`no booking ${id}`);
176
+ return b;
177
+ }
178
+ }
179
+
180
+ function round2(n: number): number {
181
+ return Math.round(n * 100) / 100;
182
+ }
183
+
184
+ export default TravelClient;
package/src/types.ts ADDED
@@ -0,0 +1,79 @@
1
+ export type BookingSource = "travala" | "legacy";
2
+
3
+ export interface Stay {
4
+ quoteId: string;
5
+ name: string;
6
+ brand: string;
7
+ city: string;
8
+ stars: number;
9
+ checkIn: string;
10
+ checkOut: string;
11
+ nights: number;
12
+ nightlyUsd: number;
13
+ totalUsd: number;
14
+ source: "travala" | "demo";
15
+ }
16
+
17
+ export interface Flight {
18
+ quoteId: string;
19
+ carrier: string;
20
+ from: string;
21
+ to: string;
22
+ date: string;
23
+ cabin: string;
24
+ totalUsd: number;
25
+ source: "travala" | "demo";
26
+ }
27
+
28
+ /** A payment authorization FurlPay hands back for a booking. */
29
+ export interface Authorization {
30
+ route: BookingSource;
31
+ amount: number;
32
+ currency: string;
33
+ /** Present on the crypto route: an x402 payment proof settled on Base. */
34
+ x402?: { proof: string; network: string; token: string; settlementTx?: string };
35
+ /** Present on the legacy route: a single-use Visa virtual card number, MCC-locked. */
36
+ card?: {
37
+ id: string;
38
+ last4: string;
39
+ expMonth: number;
40
+ expYear: number;
41
+ mccWhitelist: string[];
42
+ singleUse: boolean;
43
+ limitUsd: number;
44
+ };
45
+ simulated?: boolean;
46
+ }
47
+
48
+ export interface Booking {
49
+ id: string;
50
+ source: BookingSource;
51
+ reference: string;
52
+ amountUsd: number;
53
+ status: "authorized" | "confirmed" | "cancelled";
54
+ authorization: Authorization;
55
+ /** 10% cbBTC developer rebate accrued on Travala-routed bookings. */
56
+ rebate?: RebateAccrual;
57
+ createdAt: string;
58
+ }
59
+
60
+ export interface RebateAccrual {
61
+ bookingId: string;
62
+ currency: "cbBTC";
63
+ totalUsd: number; // 10% of booking, expressed in USD-equivalent
64
+ developerUsd: number; // 7%
65
+ treasuryUsd: number; // 3%
66
+ }
67
+
68
+ export interface TravelOptions {
69
+ /** FurlPay API key. Omit → demo mode (searches + payments simulate, no network). */
70
+ furlpayApiKey?: string;
71
+ /** FurlPay API base. Default https://api.furlpay.com/v1 */
72
+ furlpayBaseUrl?: string;
73
+ /** Travala Travel MCP endpoint. Default https://travel-mcp.travala.com/mcp */
74
+ travalaMcpUrl?: string;
75
+ /** Developer wallet that receives the 7% cbBTC rebate split. */
76
+ developerWallet?: string;
77
+ /** Override fetch (Node 18+ has a global fetch). */
78
+ fetchImpl?: typeof fetch;
79
+ }