Short-term rental arbitrage - signing a long-term lease on a unit and subletting it on Airbnb or VRBO at a nightly rate premium - is one of the more capital-efficient ways to build a rental income operation. You do not need to own the property. You need a lease, a setup budget of $3,000-8,000 for furniture and photography, and a market where the STR nightly rate times realistic occupancy exceeds your monthly rent plus overhead with enough margin to justify the operational risk.

That last part - "exceeds your monthly rent" - is where most people do the math wrong. They look at a property on Zillow, see it listed at $1,800/month, and then check Airbnb rates for the area. But the Zillow asking price is not the long-term rental market rate for that specific unit. And knowing the real LTR market rate for a specific address is exactly what you need before committing to a master lease. This is where pulling rental comps via API is the correct starting point.

For investment analysis context, see our post on real estate investment analysis with rental data APIs. For per-neighborhood pricing analysis, see our guide on calculating rent per square foot by neighborhood. This post focuses specifically on the STR arbitrage evaluation workflow.

What STR Arbitrage Actually Requires

Before getting into the analysis framework, the business model basics. STR arbitrage has three layers of cost that must be cleared by nightly revenue:

  1. Base rent: The monthly LTR rent you pay to the building owner as master tenant. This is fixed. It does not go away in slow months.
  2. Platform costs: Airbnb takes 3% from the host side. Channel management software (Hospitable, Guesty) runs $30-80/month. Cleaning costs run $75-150 per turnover. At 15 turnovers/month, that is $1,125-2,250/month in cleaning alone.
  3. Setup amortization: Furniture and photography - typically $4,000-7,000 upfront - need to be amortized over the first 12-18 months to get to true profitability.

The math only works when: monthly STR gross revenue x (1 - platform/cleaning cost rate) exceeds base rent + setup amortization + utilities. The platform/cleaning cost rate typically runs 35-45% of gross revenue all-in, which means you need STR gross revenue of 1.7x-1.85x your monthly LTR cost just to break even. The target for a viable opportunity is STR gross at 2.0x LTR cost or higher.

Why Long-Term Comp Data Is the Right Starting Point

The LTR comp data serves two functions in STR arbitrage analysis:

First, it sets your cost basis correctly. If you are negotiating a master lease, knowing that the market LTR rate for a 2BR unit at that address is $1,650/month (not the $1,900 the landlord is asking) gives you negotiating leverage. Every $100/month you shave off the master lease goes straight to arbitrage margin. A lease negotiated $150 below asking on a 12-month term saves $1,800 over the lease period.

Second, it benchmarks whether the market supports an STR premium at all. STR premiums are highest in markets where LTR rents are relatively low compared to tourist demand. A beach town in South Carolina might see a 2BR LTR market rate of $1,200/month but Airbnb gross revenue of $3,500-4,500/month in peak season with a 65% annual average occupancy rate. That is a 2.9x-3.7x multiple - strong arbitrage territory. A 2BR in suburban Columbus might have a LTR market rate of $1,450/month and Airbnb gross of $2,100/month at 58% occupancy - a 1.45x multiple that does not clear the cost structure.

The Four-Step Analysis Framework

Step 1: Pull LTR Comps for the Target Address

This gives you the true LTR market rate - the cost basis you are working from as master tenant. Do not use the asking rent on Zillow or the landlord's stated price. Pull the actual comparable listed units.

import requests
import json

RENTCOMP_API_KEY = "your_api_key"
RENTCOMP_BASE = "https://api.rentcompapi.com/v1"


def get_ltr_cost_basis(address: str, bedrooms: int,
                        bathrooms: float, sqft: int) -> dict:
    """
    Pull LTR comps for a target address to establish cost basis.
    """
    resp = requests.post(
        f"{RENTCOMP_BASE}/comps",
        headers={"Authorization": f"Bearer {RENTCOMP_API_KEY}"},
        json={
            "address": address,
            "bedrooms": bedrooms,
            "bathrooms": bathrooms,
            "sqft": sqft,
            "radius_miles": 0.75,
            "max_age_days": 60,  # Use fresher data for arbitrage decisions
        },
        timeout=15
    )
    resp.raise_for_status()
    data = resp.json()

    stats = data.get("market_stats", {})
    return {
        "address": address,
        "ltr_median": stats.get("median"),
        "ltr_p25": stats.get("p25"),
        "ltr_p75": stats.get("p75"),
        "comp_count": data.get("comp_count", 0),
        "confidence": data.get("confidence_score", 0),
        "comps": data.get("comps", []),
    }


def get_ltr_trend(address: str, bedrooms: int) -> dict:
    """
    Pull 12-month LTR rent trend for the target ZIP.
    Rising LTR rents squeeze arbitrage margins over time.
    """
    resp = requests.get(
        f"{RENTCOMP_BASE}/trends",
        headers={"Authorization": f"Bearer {RENTCOMP_API_KEY}"},
        params={
            "address": address,
            "bedrooms": bedrooms,
            "months": 12,
        },
        timeout=15
    )
    if not resp.ok:
        return {}
    return resp.json()

Step 2: Estimate STR Revenue

STR revenue data comes from AirDNA or Mashvisor for market-level averages, or from manual Airbnb research for comparable active listings. The manual approach: search Airbnb for the target ZIP, filter to same bedroom count and similar size, look at listings with 50+ reviews and good ratings (these represent successful, well-operated units). Check their pricing and calendar occupancy for the trailing 60 days. This gives you realistic gross revenue numbers, not aspirational ones.

AirDNA's Market Minder provides a neighborhood-level "Revenue" estimate that accounts for seasonality. Mashvisor provides a similar metric called "Airbnb Cash on Cash Return" that works back from their occupancy and ADR (average daily rate) estimates. Both tools cost $20-80/month for basic access and are worth the investment before committing to a $1,500+/month master lease.

Step 3: Calculate the STR Premium Multiple

def calculate_str_arbitrage_viability(
    ltr_data: dict,
    str_monthly_gross: float,
    str_occupancy_rate: float = 0.80,
    platform_cost_rate: float = 0.40,  # all-in: Airbnb fees + cleaning + supplies
    utilities_monthly: float = 150.0,
    setup_cost: float = 5000.0,
    setup_amortization_months: int = 18,
) -> dict:
    """
    Calculate whether an STR arbitrage opportunity is viable.

    str_monthly_gross: expected gross STR revenue at stated occupancy
    platform_cost_rate: fraction of gross going to platform, cleaning, supplies
    """
    ltr_median = ltr_data.get("ltr_median", 0)
    ltr_p25 = ltr_data.get("ltr_p25", 0)  # negotiation target
    confidence = ltr_data.get("confidence", 0)

    # Use 25th percentile as negotiation target (you should be able to get here)
    negotiated_rent_target = ltr_p25

    # Monthly cost structure
    setup_monthly = setup_cost / setup_amortization_months
    platform_costs = str_monthly_gross * platform_cost_rate
    total_monthly_costs = (
        ltr_median + utilities_monthly + setup_monthly
    )
    total_monthly_costs_negotiated = (
        negotiated_rent_target + utilities_monthly + setup_monthly
    )

    # Revenue at different occupancy scenarios
    str_revenue_at_occupancy = str_monthly_gross * str_occupancy_rate
    str_revenue_conservative = str_monthly_gross * 0.60  # off-season stress test
    str_net_revenue = str_revenue_at_occupancy - platform_costs
    str_net_conservative = str_revenue_conservative - (
        str_revenue_conservative * platform_cost_rate
    )

    # Premium multiples
    premium_multiple = str_revenue_at_occupancy / ltr_median if ltr_median else 0
    premium_multiple_negotiated = (
        str_revenue_at_occupancy / negotiated_rent_target
        if negotiated_rent_target else 0
    )

    # Monthly profit/loss
    monthly_profit = str_net_revenue - total_monthly_costs
    monthly_profit_negotiated = str_net_revenue - total_monthly_costs_negotiated
    monthly_profit_conservative = str_net_conservative - total_monthly_costs

    # Viability scoring
    if premium_multiple >= 2.0:
        viability = "STRONG"
    elif premium_multiple >= 1.6:
        viability = "VIABLE"
    elif premium_multiple >= 1.4:
        viability = "MARGINAL"
    else:
        viability = "NOT VIABLE"

    return {
        "address": ltr_data.get("address"),
        "ltr_market_median": ltr_median,
        "ltr_negotiation_target": negotiated_rent_target,
        "ltr_confidence": confidence,
        "str_gross_at_occupancy": round(str_revenue_at_occupancy, 0),
        "str_gross_conservative_60pct": round(str_revenue_conservative, 0),
        "str_net_after_platform_costs": round(str_net_revenue, 0),
        "total_monthly_costs_at_market": round(total_monthly_costs, 0),
        "total_monthly_costs_negotiated": round(total_monthly_costs_negotiated, 0),
        "monthly_profit_at_market_rent": round(monthly_profit, 0),
        "monthly_profit_negotiated": round(monthly_profit_negotiated, 0),
        "monthly_profit_conservative_scenario": round(monthly_profit_conservative, 0),
        "str_premium_multiple": round(premium_multiple, 2),
        "str_premium_multiple_negotiated": round(premium_multiple_negotiated, 2),
        "viability": viability,
        "risk_flag": (
            "NEGATIVE AT CONSERVATIVE OCCUPANCY"
            if monthly_profit_conservative < 0
            else "POSITIVE EVEN AT 60% OCCUPANCY"
        ),
    }


# Example: 2BR in Nashville Gulch neighborhood
ltr = get_ltr_cost_basis(
    address="600 12th Ave S, Nashville, TN 37203",
    bedrooms=2,
    bathrooms=2.0,
    sqft=1050
)

# AirDNA shows $4,200/month gross at 82% occupancy for this zip
analysis = calculate_str_arbitrage_viability(
    ltr_data=ltr,
    str_monthly_gross=4200,
    str_occupancy_rate=0.82,
    platform_cost_rate=0.42,
    utilities_monthly=160,
    setup_cost=6000,
    setup_amortization_months=18,
)

print(json.dumps(analysis, indent=2))

Market Selection: Where STR Arbitrage Works

The best STR arbitrage markets share a common profile: strong tourist or business travel demand, relatively moderate LTR rents (so the multiple is high), and STR regulations that permit whole-unit short-term rentals without owner-occupancy requirements. In rough order of attractiveness:

Strong markets (2.0x+ premium typical): Gulf Coast beach towns (Destin, Panama City, Myrtle Beach), mountain vacation areas (Gatlinburg, Branson, Lake Tahoe edges), music and festival cities with year-round events (Nashville Gulch, Austin East Side, New Orleans French Quarter adjacencies).

Viable markets (1.6x-2.0x typical): Major metros with strong corporate travel - Atlanta Midtown, Charlotte South End, Dallas Uptown, Denver RiNo. These markets are less seasonal but have lower peak ceilings.

Marginal or difficult markets: NYC (Local Law 18 effectively bans whole-unit STR arbitrage as of 2023 - you must be present during guest stays, which kills the business model), San Francisco (permit requirements and HOA restrictions make it nearly impossible in most buildings), Chicago (surcharge of 4-6% on top of Airbnb fees plus permit requirements).

The 12-Month LTR Trend: The Slow Margin Killer

Rising LTR rents are the slow killer of STR arbitrage economics. If you sign a 2-year lease at $1,650/month and LTR market rents rise 8% in year 1, your master lease is below market at renewal - and the landlord will know it. When the lease comes up, expect a renewal at $1,782+. Meanwhile, STR supply in most markets grows faster than STR demand, which keeps ADR flat or declining in many metros over 18-24 month windows.

The GET /trends endpoint gives you the 12-month trajectory of LTR rents for the target ZIP and bedroom count. Use it. If the trend shows 6%+ annual growth, your projected economics in year 2 look materially worse than year 1. Build the escalation into your model before committing.

Legal and Lease Considerations

Three categories of legal risk that kill STR arbitrage operations:

Landlord STR clauses: Most standard lease agreements prohibit subletting without landlord consent. Some explicitly prohibit STR platforms. Read the lease before signing - this is non-negotiable. Operators who skip this step get evicted after 3 months, losing their setup investment entirely. If the landlord's lease prohibits STR, either negotiate an STR rider or walk away from the unit.

HOA restrictions: Condo buildings and HOA-governed communities frequently ban STR activity in their CC&Rs. The HOA can assess fines, pursue injunctions, and in extreme cases get the property owner to evict you. Check the HOA rules before signing any lease in a building that appears to be a condo or managed community.

City permit requirements: Nashville requires a short-term rental permit and a $125 fee. Austin requires a permit and limits STR licenses to owner-occupied properties in most residential zones. Denver requires a license and has a cap on non-owner-occupied STR licenses in each neighborhood. Check the current regulatory status in your target city - this changes frequently as cities respond to housing pressure.

Due diligence checklist before signing any STR arbitrage master lease: (1) Confirm lease permits subletting or negotiate an STR rider. (2) Pull the HOA CC&Rs if applicable. (3) Check city permit requirements and availability. (4) Run the LTR comp analysis to know your real cost basis. (5) Pull AirDNA or Mashvisor data for the ZIP. (6) Model the conservative (60% occupancy) scenario - if it is negative, the opportunity is too risky.

Putting It Together: The Complete Evaluation Script

def evaluate_str_arbitrage_opportunity(
    address: str,
    bedrooms: int,
    bathrooms: float,
    sqft: int,
    airdna_monthly_gross: float,
    asking_rent: float,
    api_key: str
) -> None:
    """
    Full STR arbitrage evaluation for a target unit.
    Prints a formatted decision report.
    """
    import os
    os.environ["RENTCOMP_API_KEY"] = api_key

    global RENTCOMP_API_KEY
    RENTCOMP_API_KEY = api_key

    print(f"\nSTR Arbitrage Analysis: {address}")
    print("=" * 60)

    # Pull LTR comps
    ltr = get_ltr_cost_basis(address, bedrooms, bathrooms, sqft)
    if not ltr["ltr_median"]:
        print("ERROR: Could not retrieve LTR comp data. Check address.")
        return

    ltr_median = ltr["ltr_median"]
    asking_premium = ((asking_rent - ltr_median) / ltr_median * 100
                       if ltr_median else 0)

    print(f"\nLTR Market Analysis:")
    print(f"  Asking rent:         ${asking_rent:,.0f}/mo")
    print(f"  LTR market median:   ${ltr_median:,.0f}/mo  ({ltr['confidence']}% confidence, {ltr['comp_count']} comps)")
    print(f"  LTR 25th percentile: ${ltr['ltr_p25']:,.0f}/mo  (negotiation target)")
    print(f"  Asking vs market:    {asking_premium:+.1f}%")

    if asking_premium > 10:
        print(f"  NOTE: Asking rent is {asking_premium:.0f}% above market - negotiate hard or pass")

    # Pull LTR trend
    trend = get_ltr_trend(address, bedrooms)
    annual_change = trend.get("annual_change_pct", 0)
    if annual_change:
        print(f"\nLTR Rent Trend (12 months):")
        print(f"  Annual change: {annual_change:+.1f}%")
        if annual_change > 6:
            print(f"  WARNING: Rapidly rising LTR rents will compress margins at renewal")

    # Calculate viability
    result = calculate_str_arbitrage_viability(
        ltr_data=ltr,
        str_monthly_gross=airdna_monthly_gross,
    )

    print(f"\nSTR Revenue Analysis:")
    print(f"  Gross STR (80% occ): ${result['str_gross_at_occupancy']:,.0f}/mo")
    print(f"  Gross STR (60% occ): ${result['str_gross_conservative_60pct']:,.0f}/mo  (stress test)")
    print(f"  Net after costs:     ${result['str_net_after_platform_costs']:,.0f}/mo")

    print(f"\nProfit/Loss Scenarios:")
    print(f"  At market rent:      ${result['monthly_profit_at_market_rent']:+,.0f}/mo")
    print(f"  At negotiated rent:  ${result['monthly_profit_negotiated']:+,.0f}/mo")
    print(f"  Conservative (60%):  ${result['monthly_profit_conservative_scenario']:+,.0f}/mo")

    print(f"\nSTR Premium Multiple: {result['str_premium_multiple']:.2f}x")
    print(f"Viability:            {result['viability']}")
    print(f"Risk flag:            {result['risk_flag']}")
    print()


# Run analysis
evaluate_str_arbitrage_opportunity(
    address="2105 Adelicia St, Nashville, TN 37212",
    bedrooms=2,
    bathrooms=2.0,
    sqft=1000,
    airdna_monthly_gross=4100,
    asking_rent=1950,
    api_key="your_api_key"
)

Run this analysis on every unit you are evaluating before signing anything. The 20 minutes it takes to pull the data and run the numbers is trivial compared to the cost of a bad master lease commitment.

Ready to Pull Rental Comps via API?

Join the waitlist and get 80% off founding member pricing - for life.

Join the Waitlist