actions
Game actions: money, pause and speed, fares, bonds, station names and the demand-bubble scale.
api.actions. 23 members. While a daily challenge is in progress and not yet completed or submitted, calling any member logs [Modding API] actions.<name> is blocked during active challenges, does nothing and returns undefined; the getters return undefined too. addMoney, subtractMoney, setMoney, setPause, setSpeed, setTicketPrice and setSpeedMultiplier apply on the next microtask. Every other member runs synchronously.
Action Summary#
| Member | Returns | Summary |
|---|---|---|
setMoney(amount) | void | Replace the balance with amount ($). |
addMoney(amount, reason?) | void | Add amount ($) to the balance as non-operational revenue. |
subtractMoney(amount, reason?) | void | Charge amount ($) once as an expense in the 'other' category. |
setPause(isPaused) | void | Write the pause flag. |
setSpeed(speed) | void | Select a speed tier. |
setSpeedMultiplier(speed, multiplier) | void | Scale the game seconds per real second of one speed tier. |
setTicketPrice(price) | void | Set the global fare ($). |
getTicketPrice() | number | The current global fare ($). |
addFareGroup() | FareGroup or null | Append an empty fare group with the 'flat' system and flatFare equal to the current global fare. |
updateFareGroup(groupId, updates) | boolean | Merge updates into one fare group. |
deleteFareGroup(groupId) | boolean | Remove a fare group and append its routes to the first remaining group. |
moveRouteToFareGroup(routeId, groupId) | boolean | Move a route from its current fare group to the end of groupId. |
addFareZone(groupId, outline) | FareZone or null | Append a zone built from a polygon outline to a fare group. |
updateFareZone(groupId, zoneId, outline) | boolean | Replace one zone's shape with a new outline. |
deleteFareZone(groupId, zoneId) | boolean | Remove one zone from a fare group. Later zones move up one position. |
issueBond(bondTypeId) | object | Take out a bond of type bondTypeId and credit its principal as non-operational revenue. |
payBond(bondId, amount) | object | Charge amount ($) as a 'bondPayments' expense and reduce the bond's remaining principal by the same amount. |
getBonds() | any[] | The live store bonds array. |
renameStation(stationId, newName) | boolean | Set a station's display name and recompute station groups. |
renameStationGroup(groupId, name) | boolean | Set or clear a station group's custom name. |
setDemandBubbleScale(scale) | void | Multiply the radius of every demand bubble drawn on the map. |
getDemandBubbleScale() | number | The current demand-bubble radius multiplier. |
issueCustomBond(principal) | object | Take out a bond at an arbitrary principal (the bonds-card slider), with interest rate, principal payment rate and required daily revenue interpolated between BOND_TYPES' SMALL/MEDIUM/LARGE anchors by … Full text below the table. |
issueCustomBond(principal): Take out a bond at an arbitrary principal (the bonds-card slider), with interest rate, principal payment rate and required daily revenue interpolated between BOND_TYPES' SMALL/MEDIUM/LARGE anchors by bondTermsForPrincipal, and extrapolated beyond LARGE. Credits principal as non-operational revenue, same as issueBond.
Money Actions#
setMoney#
api.actions.setMoney(amount: number): voidReplace the balance with amount ($).
Applies on the next microtask. The write bypasses the financial history and the sandbox expense skip. When the balance is already amount, no hook fires.
Parameter amount (number): new balance; $; any finite number, negative included.
Errors:
amountnot a number,NaNor infinite: logs[Modding API] setMoney requires a number, does nothing.- store not ready: logs
[Modding API] setMoney: store not ready, does nothing.
Hooks fired: onMoneyChanged (after the deferred write when the balance changed; change is the signed delta amount - previous, type is 'revenue' for an increase and 'expense' for a decrease, category is 'mod-setMoney').
Side effects: store money = amount.
Changed: onMoneyChanged receives the signed delta; a decrease is negative (PR #1090).
api.actions.setMoney(10_000_000);addMoney#
api.actions.addMoney(amount: number, reason?: string): voidAdd amount ($) to the balance as non-operational revenue.
Applies on the next microtask. amount is not validated: a negative value lowers the balance and a non-number corrupts it. The revenue is excluded from the hourly revenue history and from lifetime fare stats. reason is ignored.
| Param | Type | Default | Description |
|---|---|---|---|
amount | number | required | dollars to add; $; no clamping. |
reason | string | none | ignored. |
Errors: store not ready: logs [Modding API] addMoney: store not ready, does nothing.
Hooks fired: onMoneyChanged (once, after the deferred store write; change is amount, type is 'revenue', category is undefined).
Side effects: store money += amount; the financial history hour boundary is rolled before the write.
api.actions.addMoney(1_000_000);
// deferred: the balance changes on the next microtask, so react in onMoneyChangedsubtractMoney#
api.actions.subtractMoney(amount: number, reason?: string): voidCharge amount ($) once as an expense in the 'other' category.
Applies on the next microtask. In sandbox mode, and while the tutorial skips construction costs, the expense is recorded in the financial history and the balance stays unchanged. A balance that drops below $0 latches the store flag hasGoneBankrupt. reason is ignored.
| Param | Type | Default | Description |
|---|---|---|---|
amount | number | required | dollars to charge; $; must be a number >= 0. |
reason | string | none | ignored. |
Errors:
amountnegative,NaN, infinite or not a number: logs[Modding API] subtractMoney requires a positive number, does nothing.- store not ready: logs
[Modding API] subtractMoney: store not ready, does nothing.
Hooks fired: onMoneyChanged (once, after the deferred store write; change is -amount, type is 'expense', category is 'other'; fires in sandbox mode too).
Side effects: store money -= amount outside sandbox mode and the tutorial cost skip; hourly expense history and lifetime expense stats updated in every mode.
Changed: charged once, deferred, in category 'other'; before, a second synchronous charge under reason ('mod' when omitted) ran as well (PR #1090).
// charged once as an expense in the 'other' category
api.actions.subtractMoney(500_000);Game Flow Actions#
setPause#
api.actions.setPause(isPaused: boolean): voidWrite the pause flag.
Applies on the next microtask. The write is unconditional, so onPauseChanged fires on every call, including a call that repeats the current state.
Parameter isPaused (boolean): true pauses the clock, false resumes it.
Errors:
- store not ready: logs
[Modding API] setPause: store not ready, does nothing. - state missing inside the deferred call: logs
[Modding API] setPause: state not available in deferred call, does nothing.
Hooks fired: onPauseChanged (every call, after the deferred write, with isPaused).
Side effects: store timeConfig.paused = isPaused.
api.actions.setPause(true);
setTimeout(() => api.actions.setPause(false), 5000);setSpeed#
api.actions.setSpeed(speed: 'slow' | 'normal' | 'fast' | 'ultrafast'): voidSelect a speed tier.
Applies on the next microtask. A tier equal to the current one is skipped, so onSpeedChanged fires only on a change.
Parameter speed ('slow' | 'normal' | 'fast' | 'ultrafast'): one of 'slow', 'normal', 'fast', 'ultrafast'.
Errors:
- other value: logs
[Modding API] setSpeed requires one of: slow, normal, fast, ultrafast, does nothing. - store not ready: logs
[Modding API] setSpeed: store not ready, does nothing. - state missing inside the deferred call: logs
[Modding API] setSpeed: state not available in deferred call, does nothing.
Hooks fired: onSpeedChanged (after the deferred write, only when the tier changed, with the new tier).
Side effects: store timeConfig.timeSpeed = speed.
// 'slow', 'normal', 'fast' or 'ultrafast'
api.actions.setSpeed('fast');setSpeedMultiplier#
api.actions.setSpeedMultiplier(speed: 'slow' | 'normal' | 'fast' | 'ultrafast', multiplier: number): voidScale the game seconds per real second of one speed tier.
Applies on the next microtask. The multiplier applies to the value captured on the first call since startup or the last reloadMods, so repeated calls replace each other instead of compounding. The product is rounded to a whole number of game seconds per real second. A result of 0 (for example 'slow' with a multiplier below 0.5) is rejected by the clock, which logs a [Game Rules] warning naming the tier and the fallback once and uses the shipped default. Shipped values: slow 1, normal 25, fast 250, ultrafast 500. 'ultrafast' runs uncapped, as fast as the machine allows, so its value paces nothing; it is accepted so existing mods keep working. Logs [Modding API] Speed multiplier for '<speed>' set: <original> * <multiplier> = <new> (development builds only) on success.
| Param | Type | Default | Description |
|---|---|---|---|
speed | 'slow' or 'normal' or 'fast' or 'ultrafast' | required | one of 'slow', 'normal', 'fast', 'ultrafast'. |
multiplier | number | required | ratio; must be a number > 0; no upper clamp. |
Errors:
- other tier: logs
[Modding API] setSpeedMultiplier requires a valid speed tier, does nothing. multiplier<= 0,NaN, infinite or not a number: logs[Modding API] setSpeedMultiplier requires a positive number, does nothing.
Side effects: writes RULES.GAME_SECONDS_PER_SECOND[speed] and pushes the constants snapshot to the sim host worker when one is running; reloadMods restores the captured originals.
// applied to the original base value, so repeated calls do not compound
api.actions.setSpeedMultiplier('fast', 2);
api.actions.setSpeedMultiplier('fast', 1); // back to the defaultsetTicketPrice#
api.actions.setTicketPrice(price: number): voidSet the global fare ($).
Applies on the next microtask. The store mirrors the price into the first fare group's flatFare when that group uses the 'flat' system. That mirror does not fire onFareGroupsChanged. The same value is written to the rule DEFAULT_TICKET_COST, the fare a new game starts with (default $3). Logs [Modding API] Ticket price set to $<price> (development builds only) on success.
Parameter price (number): fare; $; must be a number >= 0.
Errors: price negative, NaN, infinite or not a number: logs [Modding API] setTicketPrice requires a non-negative number, does nothing.
Hooks fired: onTicketPriceChanged (after the deferred write, only when the price differs from the current one, with (newPrice, oldPrice)).
Side effects: store transitCost; first flat fare group flatFare; RULES.DEFAULT_TICKET_COST.
api.actions.setTicketPrice(3.5);getTicketPrice#
api.actions.getTicketPrice(): numberThe current global fare ($).
Returns: number. store transitCost, or the rule DEFAULT_TICKET_COST when the store value is unset; the rule default on an internal error; undefined during an active daily challenge.
const price = api.actions.getTicketPrice();
console.log(`Fare: ${price}`);Fare Groups#
addFareGroup#
api.actions.addFareGroup(): FareGroup | nullAppend an empty fare group with the 'flat' system and flatFare equal to the current global fare.
Other fields of the new group: name '', routeFares {}, routeIds [], transferPolicy 'count-within-group', boardingCharge 1.5, perKmRate 0.15, fareCap 6, distanceMode 'straight-line', zones [], zonePricingMode 'count', zoneBaseFare 2.5, zonePerZoneFare 1.
Returns: FareGroup | null. a structuredClone of the new group (read its id); null on an internal error; undefined during an active daily challenge.
Errors: internal error: logs [Modding API] Error adding fare group:, returns null.
Hooks fired: onFareGroupsChanged (once, with the full list).
Side effects: store fareGroups gains one group.
const group = api.actions.addFareGroup();
if (group) console.log(`New fare group ${group.id} (${group.fareSystem})`);updateFareGroup#
api.actions.updateFareGroup(groupId: string, updates: Partial<Omit<FareGroup, 'id' | 'routeIds'>>): booleanMerge updates into one fare group.
The store normalizes the merged group: legacy fields are filled and the retired transfer policy 'free-within-group' becomes 'count-within-group'. When the updated group is the first group, uses the 'flat' system after the merge and its flatFare differs from the global fare, the global fare is rewritten to match. This includes a switch back to 'flat' with a flatFare left over from before. zones written here skip the validation and clipping that addFareZone applies.
| Param | Type | Default | Description |
|---|---|---|---|
groupId | string | required | id of an existing fare group. |
updates | Partial<Omit<FareGroup, 'id' | 'routeIds'>> | required | fields to replace; the type excludes id and routeIds, the merge does not strip them. |
Returns: boolean. true when the group exists and the write ran; false otherwise; undefined during an active daily challenge.
Errors: unknown group: logs [Modding API] updateFareGroup: no fare group with id <groupId>, returns false.
Hooks fired:
- onFareGroupsChanged (once, with the full list).
- onTicketPriceChanged (only when the updated group is the first group, is flat after the merge and its
flatFarediffers from the global fare).
Side effects: store fareGroups; store transitCost in the flat-fare case.
const group = api.actions.addFareGroup();
// distance fares: 1.50 flag fall + 0.15/km, capped at 6
api.actions.updateFareGroup(group.id, {
name: 'Regional',
fareSystem: 'distance',
boardingCharge: 1.5,
perKmRate: 0.15,
fareCap: 6,
});deleteFareGroup#
api.actions.deleteFareGroup(groupId: string): booleanRemove a fare group and append its routes to the first remaining group.
The last remaining group cannot be deleted.
Parameter groupId (string): id of an existing fare group.
Returns: boolean. true on success; false when the group is unknown or is the only group; undefined during an active daily challenge.
Errors: last group or unknown id: logs [Modding API] deleteFareGroup: cannot delete fare group <groupId>, returns false.
Hooks fired: onFareGroupsChanged (once, with the remaining list).
Side effects: store fareGroups.
const group = api.actions.addFareGroup();
// its routes move to the first remaining group; the last group cannot be deleted
const ok = api.actions.deleteFareGroup(group.id);moveRouteToFareGroup#
api.actions.moveRouteToFareGroup(routeId: string, groupId: string): booleanMove a route from its current fare group to the end of groupId.
A route already in groupId returns true without a write or a hook.
| Param | Type | Default | Description |
|---|---|---|---|
routeId | string | required | id of a route that is in some fare group. |
groupId | string | required | id of the destination fare group. |
Returns: boolean. true on success or when the route is already there; false when the route or group is unknown; undefined during an active daily challenge.
Errors: unknown route or group: logs [Modding API] moveRouteToFareGroup: unknown route <routeId> or fare group <groupId>, returns false.
Hooks fired: onFareGroupsChanged (once, on an actual move).
Side effects: store fareGroups.
const group = api.actions.addFareGroup();
const [route] = api.gameState.getRoutes({ includeTempRoutes: false });
if (route) api.actions.moveRouteToFareGroup(route.id, group.id);addFareZone#
api.actions.addFareZone(groupId: string, outline: [number, number][]): FareZone | nullAppend a zone built from a polygon outline to a fare group.
The outline is closed automatically and clipped against the group's other zones, so an outline drawn around an existing zone becomes a ring. The group's fare system is not checked. Zones price journeys only under the 'zone' system. The new zone id is mod-fare-zone-<Date.now()>.
| Param | Type | Default | Description |
|---|---|---|---|
groupId | string | required | id of an existing fare group. |
outline | [number, number][] | required | [lng, lat][]; at least 3 finite points; unclosed. |
Returns: FareZone | null. a structuredClone of the created zone; null when rejected; undefined during an active daily challenge.
Errors:
- unknown group: logs
[Modding API] fare zone action: no fare group with id <groupId>, returns null. - bad outline shape: logs
[Modding API] fare zone action: outline must be [lng, lat][] with at least 3 points, returns null. - self-intersecting outline: logs
[Modding API] fare zone action: outline crosses itself, returns null. - nothing left after clipping: logs
[Modding API] fare zone action: outline lies entirely inside existing zones, returns null. - clipped area below 10,000 m^2: logs
[Modding API] fare zone action: zone is too small, returns null.
Hooks fired: onFareGroupsChanged (once, through the group update).
Side effects: store fareGroups[group].zones.
const group = api.actions.addFareGroup();
const zone = api.actions.addFareZone(group.id, [[-122.45, 37.75], [-122.39, 37.75], [-122.39, 37.8], [-122.45, 37.8]]);
// zone.polygons holds the clipped MultiPolygon rings, zone.id starts with "mod-fare-zone-"updateFareZone#
api.actions.updateFareZone(groupId: string, zoneId: string, outline: [number, number][]): booleanReplace one zone's shape with a new outline.
Validation and clipping match addFareZone. The zone's own old shape is excluded from the clip. The zone keeps its id and list position.
| Param | Type | Default | Description |
|---|---|---|---|
groupId | string | required | id of an existing fare group. |
zoneId | string | required | id of a zone in that group. |
outline | [number, number][] | required | [lng, lat][]; at least 3 finite points; unclosed. |
Returns: boolean. true on success; false when rejected; undefined during an active daily challenge.
Errors:
- unknown group: logs
[Modding API] fare zone action: no fare group with id <groupId>, returns false. - unknown zone: logs
[Modding API] fare zone action: no zone <zoneId> in fare group <groupId>, returns false. - outline problems: the addFareZone messages, returns false.
Hooks fired: onFareGroupsChanged (once, through the group update).
Side effects: store fareGroups[group].zones.
const group = api.actions.addFareGroup();
const zone = api.actions.addFareZone(group.id, [[-122.45, 37.75], [-122.39, 37.75], [-122.39, 37.8], [-122.45, 37.8]]);
// same validation and clipping as addFareZone
if (zone) api.actions.updateFareZone(group.id, zone.id, [[-122.46, 37.74], [-122.38, 37.74], [-122.38, 37.81], [-122.46, 37.81]]);deleteFareZone#
api.actions.deleteFareZone(groupId: string, zoneId: string): booleanRemove one zone from a fare group. Later zones move up one position.
| Param | Type | Default | Description |
|---|---|---|---|
groupId | string | required | id of an existing fare group. |
zoneId | string | required | id of a zone in that group. |
Returns: boolean. true on success; false when the group or zone is unknown; undefined during an active daily challenge.
Errors: unknown group or zone: logs [Modding API] deleteFareZone: no zone <zoneId> in fare group <groupId>, returns false.
Hooks fired: onFareGroupsChanged (once, through the group update).
Side effects: store fareGroups[group].zones.
const group = api.actions.addFareGroup();
const zone = api.actions.addFareZone(group.id, [[-122.45, 37.75], [-122.39, 37.75], [-122.39, 37.8], [-122.45, 37.8]]);
// later zones shift down one number
if (zone) api.actions.deleteFareZone(group.id, zone.id);Bonds#
issueBond#
api.actions.issueBond(bondTypeId: string): { success: boolean; message?: string; }Take out a bond of type bondTypeId and credit its principal as non-operational revenue.
Bond type ids are 'small', 'medium' and 'large'. Refused in sandbox mode, in competitive multiplayer, whenever challenge data is present (a daily challenge, completed or not), and when yesterday's hourly revenue total is below the type's requiredDailyRevenue. Type defaults: 'small' $100,000,000 principal, 6% daily interest, needs $10,000,000 revenue yesterday; 'medium' $500,000,000, 4%, needs $100,000,000; 'large' $1,000,000,000, 2%, needs $200,000,000.
Parameter bondTypeId (string): 'small', 'medium' or 'large'.
Returns: object. { success: true } on success; { success: false, message } otherwise, with one of Bonds are not available in sandbox mode, Bonds are not available in competitive multiplayer, Bonds are not available in daily challenges, Invalid bond type, Insufficient revenue. Need $<x>M revenue yesterday, had $<y>M, Bond system not available, Failed to issue bond; undefined during an active daily challenge.
| Field | Type | Description |
|---|---|---|
success | boolean | true when the bond was issued. |
message | string | Reason text, present only when success is false. |
Errors: internal error: logs [Modding API] Error issuing bond:, returns { success: false, message: 'Failed to issue bond' }.
Hooks fired: onMoneyChanged (on success; change is the principal, type is 'revenue').
Side effects: store bonds gains a bond; store money += principal.
// bond type ids: 'small', 'medium', 'large'
const result = api.actions.issueBond('small');
if (!result.success) console.log(`No bond: ${result.message}`);payBond#
api.actions.payBond(bondId: string, amount: number): { success: boolean; message?: string; }Charge amount ($) as a 'bondPayments' expense and reduce the bond's remaining principal by the same amount.
The full amount is charged even when it exceeds the remaining principal. The principal floors at $0. A bond whose remaining principal reaches $0 is removed. In sandbox mode, and while the tutorial skips construction costs, the charge is recorded in the financial history and the balance stays unchanged; the principal still drops. amount is not validated: a negative value passes the balance check, raises the remaining principal and, outside those two cases, credits the balance.
| Param | Type | Default | Description |
|---|---|---|---|
bondId | string | required | id of a bond from getBonds. |
amount | number | required | dollars to pay; $; must not exceed the current balance; not otherwise validated. |
Returns: object. { success: true } on success; { success: false, message } with Bond not found, Insufficient funds, Bond system not available or Failed to pay bond; undefined during an active daily challenge.
| Field | Type | Description |
|---|---|---|
success | boolean | true when the payment was charged. |
message | string | Reason text, present only when success is false. |
Errors: internal error: logs [Modding API] Error paying bond:, returns { success: false, message: 'Failed to pay bond' }.
Hooks fired: onMoneyChanged (on success; change is -amount, type is 'expense', category is 'bondPayments').
Side effects: store bonds updated or shortened; store money -= amount outside sandbox mode and the tutorial cost skip; hourly expense history and lifetime expense stats updated in every mode.
const [bond] = api.actions.getBonds();
if (bond) {
const result = api.actions.payBond(bond.id, 50_000_000);
if (!result.success) console.log(result.message);
}getBonds#
api.actions.getBonds(): any[]The live store bonds array.
Returns: any[]. the store array itself (mutations reach the store); an empty array when unset or on an internal error; undefined during an active daily challenge.
// the store array itself; read only, or use gameState.getBonds()
const owed = api.actions.getBonds().reduce((sum, b) => sum + (b.remainingPrincipal), 0);
console.log(`Bond debt: ${owed}`);Stations & Station Groups#
renameStation#
api.actions.renameStation(stationId: string, newName: string): booleanSet a station's display name and recompute station groups.
Without a stored license validation the store skips the write. The call still returns true. The name is stored as given, without trimming. When the write happens outside challenges, a new name containing rockefeller (any case) when the old name did not pays $1,000,000, once per map.
| Param | Type | Default | Description |
|---|---|---|---|
stationId | string | required | id of an existing station. |
newName | string | required | non-empty after trim. |
Returns: boolean. true when the station exists and the arguments are valid; false otherwise; undefined during an active daily challenge.
Errors:
- bad arguments: logs
[Modding API] renameStation requires a station id and a non-empty name, returns false. - unknown station: logs
[Modding API] renameStation: station "<stationId>" not found, returns false.
Hooks fired: onMoneyChanged (only on the Rockefeller payout, through addRevenue).
Side effects: store stations, stationsMap, stationGroups; achievement and stats counters.
const [station] = api.gameState.getStations();
if (station) api.actions.renameStation(station.id, `${station.name} Central`);renameStationGroup#
api.actions.renameStationGroup(groupId: string, name: string | null): booleanSet or clear a station group's custom name.
A string sets customName and name on the group. null clears customName and recomputes the groups so the derived name returns.
| Param | Type | Default | Description |
|---|---|---|---|
groupId | string | required | id of an existing station group. |
name | string or null | required | non-empty string after trim, or null to restore the derived name. |
Returns: boolean. true when the group exists and the arguments are valid; false otherwise; undefined during an active daily challenge.
Errors:
- bad arguments: logs
[Modding API] renameStationGroup requires a group id and a non-empty name (or null to reset), returns false. - unknown group: logs
[Modding API] renameStationGroup: group "<groupId>" not found, returns false.
Side effects: store stationGroups.
const [group] = api.gameState.getStationGroups();
if (group) {
api.actions.renameStationGroup(group.id, 'Downtown Interchange');
api.actions.renameStationGroup(group.id, null); // restores the derived name
}Demand Bubbles#
setDemandBubbleScale#
api.actions.setDemandBubbleScale(scale: number): voidMultiply the radius of every demand bubble drawn on the map.
The store clamps the value to the range 0.05 to 10. Rendering only. Demand values are unchanged.
Parameter scale (number): radius multiplier; ratio; 1 is the default; must be a finite number > 0.
Errors: scale <= 0, NaN, Infinity or not a number: logs [Modding API] setDemandBubbleScale requires a positive finite number, does nothing.
Side effects: infrequent-updates store demandBubbleScale.
api.actions.setDemandBubbleScale(0.5); // half-size demand bubblesgetDemandBubbleScale#
api.actions.getDemandBubbleScale(): numberThe current demand-bubble radius multiplier.
Returns: number. ratio; 1 by default; 1 on an internal error; undefined during an active daily challenge.
const scale = api.actions.getDemandBubbleScale(); // 1 by defaultissueCustomBond#
api.actions.issueCustomBond(principal: number): { success: boolean; message?: string; }Take out a bond at an arbitrary principal (the bonds-card slider), with interest rate, principal payment rate and required daily revenue interpolated between BOND_TYPES' SMALL/MEDIUM/LARGE anchors by bondTermsForPrincipal, and extrapolated beyond LARGE. Credits principal as non-operational revenue, same as issueBond.
Same refusal conditions as issueBond (sandbox mode, competitive multiplayer, daily challenges, insufficient revenue), checked against the interpolated requiredDailyRevenue for this principal. The issued bond's bondTypeId is 'custom', not one of 'small'/'medium'/'large'.
Parameter principal (number): dollars; any non-negative value. Below SMALL's principal, terms extrapolate linearly from the SMALL/MEDIUM slope. Above LARGE's principal, requiredDailyRevenue and the hourly payment keep climbing, but interestRate holds flat at LARGE's rate rather than continuing to fall (continuing the decline would cross 0% well inside the slider's range).
Returns: object. { success: true } on success; { success: false, message } otherwise, same message set as issueBond.
| Field | Type | Description |
|---|---|---|
success | boolean | true when the bond was issued. |
message | string | Reason text, present only when success is false. |
Errors: internal error: logs [Modding API] Error issuing custom bond:, returns { success: false, message: 'Failed to issue bond' }.
Hooks fired: onMoneyChanged (on success; change is the principal, type is 'revenue').
Side effects: store bonds gains a bond with bondTypeId: 'custom'; store money += principal.
const result = api.actions.issueCustomBond(750_000_000);
if (!result.success) console.log(`No bond: ${result.message}`);Example: Day/Night Economy Mod#
Night fares between 22:00 and 06:00, a daily spending report and a weekly bonus. Money actions are deferred to the next microtask, so the mod reacts through hooks instead of reading the balance back.
(function () {
const api = window.SubwayBuilderAPI;
const DAY_FARE = api.actions.getTicketPrice();
const NIGHT_FARE = Math.round(DAY_FARE * 0.6 * 100) / 100;
api.hooks.onHourChange((hour) => {
if (hour === 22) api.actions.setTicketPrice(NIGHT_FARE);
if (hour === 6) api.actions.setTicketPrice(DAY_FARE);
});
let expensesToday = 0;
api.hooks.onMoneyChanged((balance, change, type) => {
if (type === 'expense') expensesToday += Math.abs(change);
});
api.hooks.onDayChange((endedDay) => {
if (expensesToday > 1_000_000) {
api.ui.showNotification(`Day ${endedDay} spending: ${expensesToday.toLocaleString()}`, 'warning');
}
expensesToday = 0;
if (endedDay % 7 === 0) {
api.actions.addMoney(100_000);
api.ui.showNotification('Weekly bonus: 100,000', 'success');
}
});
})();