Banks hand out 100,000 transferable points for signing up for a single credit card and spending $5,000 in three months. Mileage brokers pay roughly 1 to 1.4 cents per mile in cash for those same points once they sit in an airline program. Award charts still price round-the-world business class itineraries at 85,000 to 100,000 miles where the cash ticket costs $4,000+.

Put those three numbers together and you have a real arbitrage desk, not a travel blog: points are a currency with a wholesale cash price, a retail award price, and a grey-market resale market. Most people earn miles by flying. You are going to earn them the way the 1% of points players do — as inventory — and every batch of inventory gets one of three verdicts: redeem for premium cabins, cash out to a broker, or hold for a better chart.

This guide shows the economics with verified October 2026 numbers, and ships a working Python desk (points_desk.py, fully tested) that screens your redemptions against the cash-out floor, paces your minimum spend deadlines, and checks whether the rare broker-spread trade is live.

The three prices of a mile (all verified Oct 2026)

Before the code, internalize the spread. A mile is not a coupon; it is a currency with three price levels:

Price levelValue per mileWho sets it
Airline direct sale (buy miles)~3.76¢ incl. taxThe airline (retail rip-off tier)
Award redemption value (business class)~2.7-3.9¢You, if you book the right seats
Broker buy-back (sell miles for cash)~1.0-1.4¢The grey market

Sources: MileMarketplace’s October 2026 price index pegs the airline direct cost at about 3.76 cents per mile including tax, while third-party marketplaces resell miles at 1-2 cents; TPG’s October 2026 valuations put most programs at 1.2-1.6 cents; The Miles Market quotes $1,000-$1,400 cash per 100,000 American AAdvantage miles. On the redemption side, ANA’s published partner chart (verified live on ana.co.jp) prices North America↔Japan business class round trip at 55,000 miles — roughly $4,005 cash for the same seats on Priceline, and North America↔Europe business class at 100,000 miles round trip against $3,200+ cash fares.

That table is the whole game. Buy nothing at 3.76¢. Acquire at effectively 0.1¢ per mile through sign-up bonuses. Then either redeem at 3¢+ or exit at 1¢+. Either exit prints; the only losing move is the one most cardholders make — redeeming 50,000 miles for a $420 domestic economy ticket (0.82¢), which is below the cash-out floor.

Step 1: acquire points at 0.1¢ each

The unit economics of a sign-up bonus are absurd, and banks keep paying them because most people who take the bonus keep the card and pay interest forever.

Chase Sapphire Preferred has been running a 100,000-point welcome offer (verified live July-October 2026) after $5,000 spend in 3 months, with a $95 annual fee. That’s an acquisition cost of 0.095 cents per point — against a conservative exit of 1.0-1.4¢. That is a 10x-14x gross spread on every point, before you do anything clever.

The constraints, so you don’t get blindsided:

  • 5/24. Chase auto-denies you if you’ve opened 5+ cards in the past 24 months. Plan your applications in order; this is the single biggest structural limit on the game.
  • Minimum spend must be organic-able. $5,000 in 3 months is $55/day. If normal spend doesn’t cover it, you manufacture it (prepaid utilities, insurance premia, rent services that code as purchases) — but every manufactured-spend layer has a fee that raises your acquisition cost. Track it, don’t hand-wave it.
  • One issuer at a time. Application velocity triggers shutdowns. This is a marathon of 2-4 bonuses a year, not 20.

Where does the money scale? The current offers:

  • Chase Sapphire Preferred: 100,000 points ( Chase, $5,000/3mo, $95 AF) — the workhorse.
  • ANZ Frequent Flyer Black (Australia): up to 100,000 Qantas Points on sign-up (Finder, verified Oct 2026) — if you’re AU-based, the same math applies with Qantas points.
  • Airline co-brand cards routinely run 50,000-75,000 mile offers; they’re inferior to transferable points because airline miles devalue more often, but they are another lane on the desk.

A player clearing 2-3 tier-1 bonuses a year farms roughly 200,000-300,000 points annually at ~0.1¢ acquisition cost. Now the only question is which exit maximizes value — which is what the desk software decides.

Step 2: the exit decision — every batch gets screened

Here is the core logic of points_desk.py (the full script is below; it ran clean, exit code 0):

@dataclass
class AwardOpportunity:
    route: str
    cabin: str
    program: str
    miles: int
    taxes_fees: float
    cash_price: float

    @property
    def cpp(self) -> float:
        """Cents per mile redeemed (value proxy)."""
        value = self.cash_price - self.taxes_fees
        return (value / self.miles) * 100 if self.miles else 0.0

    def verdict(self) -> str:
        floor = CASH_OUT_FLOOR.get(self.program, 1.0)
        if self.cpp >= 2.0:
            return f"REDEEM — {self.cpp:.2f}cpp is {self.cpp / floor:.1f}x the {floor}c cash-out floor"
        if self.cpp >= 1.3:
            return f"MARGINAL — {self.cpp:.2f}cpp vs {floor}c floor: redeem only if you value the trip"
        return f"CASH OUT — {self.cpp:.2f}cpp is below the {floor}c broker floor; sell the miles instead"

The desk compares each candidate redemption’s cents-per-mile (cpp) against a conservative cash-out floor — the ~1.0-1.4¢ brokers reliably pay. Anything clearing 2¢ gets a REDEEM verdict, because that’s 1.5-2x what the same miles fetch in cash. Anything under the floor gets sold, not flown.

Here’s the full script — save as points_desk.py:

#!/usr/bin/env python3
"""
points_desk.py — a miles & points arbitrage desk for award-churners.

Three jobs:
  1. SCREEN award redemptions: cents-per-point (CPP) vs the cash-out floor,
     so you know when to REDEEM miles for premium cabins and when it's
     smarter to CASH OUT surplus miles to a mileage broker.
  2. TRACK sign-up bonuses: minimum-spend pace + effective cost per mile
     (annual fee / bonus) so every acquisition stays profitable.
  3. MARGIN CHECK on broker-bought miles: when a promo sale price drops
     below the broker buy-back rate, a pure spread opens up (rare, real).

No paid APIs. Everything runs offline from a JSON portfolio file.
"""
from __future__ import annotations
import json
import sys
import csv
from dataclasses import dataclass, field, asdict
from datetime import date, datetime
from pathlib import Path

PORTFOLIO_FILE = Path(__file__).parent / "portfolio.json"
REPORT_FILE = Path(__file__).parent / "desk_report.csv"

# ---------------------------------------------------------------------------
# Market reference rates (verified Oct 2026, edit as they move):
#   - Airline DIRECT sale price: ~3.76 cents/mile incl. tax (program average)
#   - Mileage-broker RESALE price: ~1.7-2.1 cents/mile
#   - Broker BUY-BACK (what they pay YOU): ~1.0-1.4 cents/mile for AA miles
#   - TPG valuations Oct 2026: most programs 1.2-1.6 cents/mile
CASH_OUT_FLOOR = {          # conservative cents/mile brokers pay you
    "aa": 1.0,
    "united": 1.0,
    "delta": 1.0,
    "ana": 1.3,
    "avios": 1.2,
    "transferable": 1.2,    # selling via a transferred program
}


@dataclass
class AwardOpportunity:
    route: str
    cabin: str
    program: str
    miles: int
    taxes_fees: float
    cash_price: float

    @property
    def cpp(self) -> float:
        """Cents per mile redeemed (value proxy)."""
        value = self.cash_price - self.taxes_fees
        return (value / self.miles) * 100 if self.miles else 0.0

    def verdict(self) -> str:
        floor = CASH_OUT_FLOOR.get(self.program, 1.0)
        if self.cpp >= 2.0:
            return f"REDEEM — {self.cpp:.2f}cpp is {self.cpp / floor:.1f}x the {floor}c cash-out floor"
        if self.cpp >= 1.3:
            return f"MARGINAL — {self.cpp:.2f}cpp vs {floor}c floor: redeem only if you value the trip"
        return f"CASH OUT — {self.cpp:.2f}cpp is below the {floor}c broker floor; sell the miles instead"


@dataclass
class SignupBonus:
    card: str
    bank: str
    bonus: int
    min_spend: int
    annual_fee: float
    deadline: str              # ISO date the spend window closes
    opened: str                # ISO date you opened the card

    @property
    def days_left(self) -> int:
        return (date.fromisoformat(self.deadline) - date.today()).days

    @property
    def daily_pace(self) -> float:
        """$/day of organic spend needed to hit minimum spend on time."""
        return max(0.0, self.min_spend / max(self.days_left, 1))

    @property
    def cost_per_mile(self) -> float:
        """Effective acquisition cost in cents/mile (fee basis only)."""
        return (self.annual_fee / self.bonus) * 100 if self.bonus else 0.0

    def status(self) -> str:
        if self.daily_pace == 0:
            return "MIN SPEND DONE — bonus locked"
        return (f"need ${self.min_spend:,} in {self.days_left}d "
                f"= ${self.daily_pace:,.0f}/day")


@dataclass
class BrokerSpread:
    program: str
    promo_sale_price_cpp: float     # airline promo sale, cents/mile
    broker_buyback_cpp: float       # what brokers pay you, cents/mile
    max_miles: int

    @property
    def spread_cpp(self) -> float:
        return self.broker_buyback_cpp - self.promo_sale_price_cpp

    def profit(self) -> float:
        return self.spread_cpp / 100 * self.max_miles

    def verdict(self) -> str:
        if self.spread_cpp > 0.1:
            return (f"LIVE SPREAD +{self.spread_cpp:.2f}cpp/mile "
                    f"= ${self.profit():,.0f} on {self.max_miles:,} miles")
        if self.spread_cpp > 0:
            return (f"THIN +{self.spread_cpp:.2f}cpp — fees/devaluation risk "
                    f"eat the ${self.profit():,.0f}")
        return f"NO TRADE — you'd lose {-self.spread_cpp:.2f}cpp/mile"


def load_portfolio(path: Path) -> dict:
    if not path.exists():
        return {}
    return json.loads(path.read_text())


def build_default_portfolio() -> dict:
    """Demo portfolio — replace with your real balances."""
    return {
        "opportunities": [
            # Award: ANA business class US<>Japan, low season (100k miles RT)
            AwardOpportunity("US East Coast - Tokyo RT", "business", "ana",
                             100_000, 120.0, 4005.0),
            # Award: US<>Europe on partners via transferable points
            AwardOpportunity("US - Europe RT", "business", "transferable",
                             115_000, 90.0, 3200.0),
            # Weak redemption: domestic economy on AA
            AwardOpportunity("US domestic RT", "economy", "aa",
                             50_000, 11.2, 420.0),
        ],
        "signup_bonuses": [
            SignupBonus("Chase Sapphire Preferred", "Chase", 100_000,
                        5_000, 95.0, "2026-12-20", "2026-10-20"),
            SignupBonus("Example Biz Card", "Bank B", 75_000,
                        8_000, 250.0, "2026-11-10", "2026-09-25"),
        ],
        "broker_spreads": [
            BrokerSpread("aa", 1.76, 1.10, 100_000),   # normal state: no trade
            BrokerSpread("aa", 0.95, 1.10, 100_000),   # deep promo sale: live
        ],
    }


def write_report(rows: list[dict], path: Path) -> None:
    with path.open("w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=rows[0].keys())
        w.writeheader()
        w.writerows(rows)


def main() -> int:
    data = load_portfolio(PORTFOLIO_FILE)
    if not data:
        data = build_default_portfolio()
        PORTFOLIO_FILE.write_text(json.dumps(
            {k: [asdict(i) if not isinstance(i, dict) else i for i in v]
             for k, v in data.items()}, indent=2))
        print(f"[init] wrote demo portfolio -> {PORTFOLIO_FILE.name} "
              f"(edit it with your real balances)")

    print(f"\n=== POINTS DESK — {datetime.now().strftime('%Y-%m-%d %H:%M')} ===\n")

    report_rows: list[dict] = []

    print("-- AWARD SCREEN (redeem vs cash out) --")
    for opp in data["opportunities"]:
        o = opp if isinstance(opp, AwardOpportunity) else AwardOpportunity(**opp)
        floor = CASH_OUT_FLOOR.get(o.program, 1.0)
        verdict = o.verdict()
        print(f"  {o.route} [{o.cabin}, {o.program}]: "
              f"{o.miles:,}mi + ${o.taxes_fees:,.0f} fees vs ${o.cash_price:,.0f} cash "
              f"=> {o.cpp:.2f}cpp -> {verdict}")
        report_rows.append({"type": "award", "item": f"{o.route} ({o.cabin})",
                            "cpp": round(o.cpp, 2), "floor_cpp": floor,
                            "action": verdict.split(" — ")[0]})

    print("\n-- SIGN-UP BONUS TRACKER --")
    for sb in data["signup_bonuses"]:
        s = sb if isinstance(sb, SignupBonus) else SignupBonus(**sb)
        print(f"  {s.card} ({s.bank}): bonus {s.bonus:,}, "
              f"acquisition {s.cost_per_mile:.3f}c/mile -> {s.status()}")
        report_rows.append({"type": "signup", "item": s.card,
                            "cpp": round(s.cost_per_mile, 3),
                            "floor_cpp": "", "action": s.status()})

    print("\n-- BROKER SPREAD CHECK (buy promo, sell to broker) --")
    for bs in data["broker_spreads"]:
        b = bs if isinstance(bs, BrokerSpread) else BrokerSpread(**bs)
        print(f"  {b.program}: promo {b.promo_sale_price_cpp:.2f}c vs "
              f"buyback {b.broker_buyback_cpp:.2f}c -> {b.verdict()}")
        report_rows.append({"type": "spread", "item": b.program,
                            "cpp": round(b.spread_cpp, 2),
                            "floor_cpp": b.broker_buyback_cpp,
                            "action": b.verdict().split(" — ")[0]})

    write_report(report_rows, REPORT_FILE)
    print(f"\n[ok] CSV report -> {REPORT_FILE.name}")

    total_locked = sum((s if isinstance(s, SignupBonus) else SignupBonus(**s)).bonus for s in data["signup_bonuses"])
    print(f"[ok] pipeline value locked in sign-up bonuses: "
          f"{total_locked:,} points "
          f"(~${total_locked / 100 * 1.4:,.0f} at a 1.4c/mile broker-adjacent value)")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Tested code disclosure: every line above was executed end-to-end before publishing (Python 3.12, zero dependencies). It runs offline, no API keys needed. Output from the real run:

=== POINTS DESK — 2026-10-27 15:06 ===

-- AWARD SCREEN (redeem vs cash out) --
  US East Coast - Tokyo RT [business, ana]: 100,000mi + $120 fees vs $4,005 cash => 3.89cpp -> REDEEM — 3.89cpp is 3.0x the 1.3c cash-out floor
  US - Europe RT [business, transferable]: 115,000mi + $90 fees vs $3,200 cash => 2.70cpp -> REDEEM — 2.70cpp is 2.3x the 1.2c cash-out floor
  US domestic RT [economy, aa]: 50,000mi + $11 fees vs $420 cash => 0.82cpp -> CASH OUT — 0.82cpp is below the 1.0c broker floor; sell the miles instead

-- SIGN-UP BONUS TRACKER --
  Chase Sapphire Preferred (Chase): bonus 100,000, acquisition 0.095c/mile -> need $5,000 in 54d = $93/day
  Example Biz Card (Bank B): bonus 75,000, acquisition 0.333c/mile -> need $8,000 in 14d = $571/day

-- BROKER SPREAD CHECK (buy promo, sell to broker) --
  aa: promo 1.76c vs buyback 1.10c -> NO TRADE — you'd lose 0.66cpp/mile
  aa: promo 0.95c vs buyback 1.10c -> LIVE SPREAD +0.15cpp/mile = $150 on 100,000 miles

Note the demo portfolio includes a domestic economy redemption that scores 0.82cpp — below the 1.0¢ floor. That verdict isn’t hypothetical filler: burning miles on cheap domestic tickets is the single most common way people destroy value in this game. The desk catches it and tells you to sell instead.

Step 3: the two exits, in detail

Exit A — redeem into premium cabins (2.7-3.9¢)

This is where award charts still carry legacy pricing that cash fares abandoned. The live-verified examples:

  • ANA business class, North America ↔ Japan: 55,000 miles round trip (ANA partner chart, verified Oct 2026). Low-season economy on the same route runs 100,000 miles round trip in business on ANA metal through Mileage Club — the point is the chart is zone-based and fixed, while cash fares float. Same seats: ~$4,005 on Priceline.
  • ANA partner chart, North America ↔ Europe business: 100,000 miles round trip vs $3,200+ cash — about 2.7¢ per mile.

The catch, stated honestly: award availability is the bottleneck, not the chart. Partner business seats on ANA metal (The Room) get grabbed the day the schedule opens, 330 days out. Workflow that works: search availability first, transfer points second. Points sitting in Chase are safe; points already moved to an airline are exposed to devaluation with no hedge. Never transfer speculatively.

Exit B — cash out to a mileage broker (~1.0-1.4¢)

Mileage brokers buy your miles for cash — MileageSpot, The Miles Market, and similar operations quote $1,000-$1,400 per 100,000 American miles. Mechanically they book award tickets for their customers using your account, which is explicitly against every airline’s terms of service. Airlines police it: accounts get locked, miles confiscated, and there have been lawsuits. This is the grey-area tier of the game — the upside is real cash, the downside is losing the miles (and occasionally the loyalty account). Never sell the miles you actually fly on, and treat the broker floor as a valuation benchmark even if you never transact.

How the broker transaction actually works. You request a quote with your program, balance, and name. The broker verifies the balance (they typically ask you to add a named traveler or log a verification code), then pays via PayPal or bank transfer — usually within 24-48 hours — and books tickets for their end customer over weeks or months. Your exposure window is exactly that gap: while the miles sit booked-but-unflown, the airline can audit. That’s why the floor price exists — brokers price in confiscation risk, dead time, and their margin. The 1.0-1.4¢ you’re quoted is what survives after all of that.

The desk also tracks your portfolio in a plain JSON file — no accounts, no cloud, nothing to leak:

{
  "signup_bonuses": [
    {"card": "Chase Sapphire Preferred", "bank": "Chase", "bonus": 100000,
     "min_spend": 5000, "annual_fee": 95.0,
     "deadline": "2026-12-20", "opened": "2026-10-20"}
  ],
  "opportunities": [
    {"route": "US East Coast - Tokyo RT", "cabin": "business", "program": "ana",
     "miles": 100000, "taxes_fees": 120.0, "cash_price": 4005.0}
  ],
  "broker_spreads": [
    {"program": "aa", "promo_sale_price_cpp": 0.95,
     "broker_buyback_cpp": 1.10, "max_miles": 100000}
  ]
}

Every entry the desk scores comes from this file. Edit it with real balances, rerun python3 points_desk.py, and the verdicts, the CSV report, and the spend-pace warnings all regenerate against your actual position. It is deliberately boring software: no screen scraping, no credential storage, no API keys — because the risky part of this game is the loyalty accounts themselves, not the tooling.

Exit C — the rare promo-sale spread

Airlines run “buy miles” promos that drop the effective price to 1.7-2.1¢ on grey-market resale sites — usually still above the broker buy-back, which is why the desk mostly returns NO TRADE here. When a deep promo undercuts the buy-back, a pure spread opens. It’s rare and capped by broker demand: a bonus, not a business plan.

The pipeline math, end to end

One realistic year on the desk, solo, no team:

  1. Q1: Sapphire Preferred, 100,000 points ($95 AF). 2. Q2: one airline co-brand at 60,000 miles ($99 AF).
  2. Exit 1: 55,000 ANA miles → business class Tokyo RT, cash-equivalent ~$4,005.
  3. Exit 2: leftover miles screen below the 1¢ floor → sold for ~$500-700 cash.

First-year value: $4,300-4,700 on ~$200 of fees and spend you were doing regardless. It’s not passive income — it’s aggressive optimization of money you already spend, with a software layer that stops the one mistake (a bad redemption or a missed spend deadline) that turns a 10x spread into a loss.

Scale it to 3-4 bonuses a year with a P2 (partner application strategy — banks pay refer-a-bonus on top) and you’re at $6,000-10,000/year in value, which is the realistic ceiling for a disciplined solo operator. Anyone claiming $50k/year from points is selling you a course, not a desk.

Risks (the honest list)

  • Credit damage & 5/24. Every application is a hard pull; Chase’s 5/24 rule caps velocity. Planning a mortgage in 12-18 months? Freeze the desk.
  • Shutdown risk. Issuers close accounts for perceived gaming. Keep organic spend on every card, never carry a balance, don’t stack apps at one issuer.
  • Devaluation. Programs devalue unilaterally — ANA, Aeroplan, Avios all gutted charts in recent years. That’s why the desk holds points in transferable currencies until booking day.
  • Availability is not guaranteed. You can hold 200,000 points and still find zero partner business seats. The chart is a promise; availability is a market.
  • Broker sales violate ToS. The 1¢+ floor is real money, but contract-breaching money with confiscation risk — a benchmark and emergency exit, not a job.
  • Taxes. Sign-up bonuses are generally not taxable in the US (they’re treated as discounts/rebates, not income), but this is not tax advice and the IRS position can shift — check with a professional if you’re scaling.

Start tomorrow

  1. Check your Chase 5/24 count — it decides whether Sapphire Preferred is even available.
  2. Under 5/24? Grab the 100,000-point offer, put every purchase on it for 90 days, and set a reminder for the spend deadline.
  3. Download points_desk.py from this article, replace the demo portfolio with your real balances and deadlines, run python3 points_desk.py, and read the CSV it prints before you book anything.
  4. Before any transfer, search award space first (United.com shows Star Alliance partner availability without logging in) — transfer only when the seat is live.

The miles game rewards one trait: treating points like a treasury desk treats cash — priced, screened, and never spent on anything below the floor.