Every couple of months, a broker decides it wants your portfolio more than its competitor does, and puts a number on it: move your account over and we pay you a percentage of everything you bring. Webull pays a 3% match on ACAT transfers (4% at $100k+). Moomoo pays a 3% match up to $600. Robinhood has run 3% matches with no cap during promo windows, and regularly pays 1% baseline on transfers in. Your bank pays you nothing for the same loyalty.
The play that churning communities on Reddit and Bogleheads have run for years is simple: your portfolio is not loyal to any broker. It is inventory. Park it wherever it pays the most to park, complete the hold period, move it to the next offer, repeat. A $25,000 account can realistically collect $1,500-$3,000 in credits per cycle with nothing more than a few forms and the patience to not touch the money. That is a double-digit annualized return on capital that was going to sit there anyway.
The reason more people don’t do it is not that it’s hidden. It’s that the details are boring and everyone skips them: hold periods, clawback clauses, outgoing transfer fees, installment payouts, and the opportunity cost of freezing capital. Skip one and your “free 3%” becomes a clawback letter. This article gives you the full map and a working Python planner that does the math for you, tested and runnable before you touch a single form.
How the ACAT machine actually works
ACATS (Automated Customer Account Transfer Service) is the NSCC-run system that moves brokerage accounts between firms. You open an account at Broker B, submit a transfer request with your account number at Broker A, and the assets move electronically in 5-7 business days. In-kind transfers move your actual shares without selling them, which means no taxable event on the move itself. The transfer is invisible to the IRS; only the bonus and any shares you actually sell get reported.
The key asymmetry: brokers charge you to leave ($75-$150 outgoing ACAT fee is standard) and pay you to arrive. The arriving broker knows this, so most of them reimburse your outgoing fee on top of the match. Firstrade’s standing offer reimburses up to $250. Webull reimburses up to $100. That rebate alone can be $100-$250 of the profit per hop.
One structural note before the numbers: this is a US brokerage game. ACATS is a US system, the brokers named here are US-regulated (FINRA/SEC), and you generally need a US person’s tax documentation (W-9, SSN/ITIN) to open these accounts. If that’s not you, the same logic applies to whatever equivalent your jurisdiction has, but the specific offers won’t.
The offer map (verified October 2026)
Numbers move constantly, so every figure below was pulled fresh from the brokers’ own promo pages this month. Always re-check the live terms page before you initiate a transfer — these are marketing offers, not laws.
Webull — 3% match (4% at $100k+), up to $100 fee reimbursement. First ACAT transfer of at least $2,000 qualifies. At $25,000 transferred: $750 bonus + $100 rebate = $850. The match is capped by their promo terms, and the bonus is subject to clawback if the assets leave early — treat it as roughly a 90-day hold and verify the exact window in your offer screen. This is currently the best speed-to-cash offer on the board.
Moomoo — 3% match, capped at $600 (on $20,000 of transfers). Verified terms (June 10 - December 31, 2026 promo window): the coupon unlocks in tranches at 90/180/270/360 days. So you don’t wait the full year for everything — a quarter unlocks every 90 days — but the last tranche keeps you there 12 months. Trading inside the account doesn’t affect unlocking, only withdrawals of transferred assets do.
Robinhood — baseline 1%, promos up to 3%. Robinhood’s standing ACATS bonus terms pay 1% of net transferred value with a two-year earn-out. During promo windows (they ran a 3% Webull-fleeing offer and monthly ACAT match events) the percentage jumps. The two-year earn-out on the baseline means this is a slow-release offer: good as a final destination, bad as a quick hop.
Public — up to 3%, but read the fine print twice. Public’s current Webull-transfer match pays 3% (capped at $1M, transfers initiated by 10/26/2026), but it’s paid in monthly installments over 72 months, and any withdrawals reduce your remaining payments. Their standing offer is a 1% match over 60 months. This is an annuity, not a lump — it prices terribly against a fast cycle, which the planner below will show you explicitly.
Firstrade — up to $250 fee rebate, no match. Transfer $2,500+, keep the account open 12 months, get your outgoing fee reimbursed up to $250. On its own this is weak, but as the final hop of a chain where you were going to settle somewhere anyway, it’s $250 for signing a form.
The math nobody does: opportunity cost and real yield
Here’s where most churn calculations fall apart. A 3% payout for a 360-day hold is not “3% free” — it’s 3% when a 4% APY Treasury/cash alternative was available the whole time. And a 3% payout spread over 72 months is worth dramatically less than 3% today. The correct question is never “what’s the match percentage” — it’s “what’s the annualized, risk-adjusted net credit after fees and forgone yield.”
Let’s be concrete with the three trap cases:
- Public 3% over 72 months: $750 on a $25k transfer, but annualized that’s roughly 0.5% — you’d earn 8x more parking the same money in a 4% money market fund.
- Moomoo $600 over the 360-day unlock schedule: strong headline, but at $25k+ portfolios the cap bites. Below $20,000 transferred, you get the full 3%; above that, the marginal dollars earn zero. At $25k the effective rate is 2.4% gross before you account for the frozen year.
- Robinhood 1% baseline over the two-year earn-out: 0.5% annualized. Fine as a landing spot, terrible as a destination you chose purely for the bonus.
The offers that actually clear the bar are the fast, uncapped-ish ones: Webull’s 3-4% with a ~90-day clawback window annualizes to roughly 14% on a $25k account including the fee rebate. That’s the engine of the whole strategy.
The planner: tested code
This script models every live offer against your portfolio size, computes the true annualized yield after opportunity cost, and simulates full rotation chains. It’s pure standard library — no API keys, no dependencies. Every figure in the offer table was verified against the pages linked in the source list, and the script ran end-to-end (exit code 0) before this article went up.
#!/usr/bin/env python3
"""
ACAT Transfer Bonus Rotation Planner
Track brokerage transfer-match offers, model real hold-period yield,
and flag whether a rotation is actually worth the freeze time.
Zero dependencies. Offer data is edited in OFFERS below -- the numbers
in this file are the verified figures from October 2026; swap in your
own as promos change.
"""
from datetime import date, timedelta
import json
import sys
# ---- Offer data (verified Oct 2026, from official broker pages) -----------
OFFERS = [
{
"broker": "Moomoo",
"url": "https://www.moomoo.com/us/support/topic4_603",
"match_pct": 0.03, # 3% match on first transfer
"cap": 600.0, # 3% of up to $20,000
"hold_days": 360, # unlocks in tranches at 90/180/270/360
"hold_note": "unlock tranches at 90/180/270/360 days",
"outgoing_fee_rebate": 0.0,
"min_transfer": 0.0,
},
{
"broker": "Webull",
"url": "https://www.webull.com/offers-promotions/transfer-fee-promo",
"match_pct": 0.03, # 3% (< $100k), 4% at $100k+
"cap": None, # no published cap on the match
"hold_days": 90, # typical promo clawback window; verify in-app
"hold_note": "bonus clawed back if assets leave early (~90 days)",
"outgoing_fee_rebate": 100.0,
"min_transfer": 2000.0,
},
{
"broker": "Firstrade",
"url": "https://www.firstrade.com/support/funding/acat",
"match_pct": 0.0, # no match, only fee rebate
"cap": None,
"hold_days": 365, # 12-month keep-open requirement
"hold_note": "account must stay open 12 months",
"outgoing_fee_rebate": 250.0,
"min_transfer": 2500.0,
},
{
"broker": "Public",
"url": "https://public.com/webull-transfer",
"match_pct": 0.03, # current Webull-fleeing promo
"cap": 1_000_000.0,
"hold_days": 365 * 6, # paid over 72 monthly installments
"hold_note": "paid over 72 months; withdrawals cut future installments",
"outgoing_fee_rebate": 0.0,
"min_transfer": 0.0,
},
]
# ---- Editable portfolio assumptions ---------------------------------------
TYPICAL_OUTGOING_FEE = 100.0 # most brokers charge $75-$150 per ACAT out
SETTLE_DAYS = 6 # ACATS usually completes in 5-7 business days
def gross_bonus(offer, portfolio):
pct = 0.04 if (offer["match_pct"] >= 0.03 and portfolio >= 100_000) \
else offer["match_pct"]
bonus = portfolio * pct
if offer["cap"] is not None:
bonus = min(bonus, offer["cap"])
return bonus
def model_rotation(offer, portfolio, outgoing_fee=TYPICAL_OUTGOING_FEE):
"""Return dict of the real economics of one ACAT hop into `offer`."""
if portfolio < offer["min_transfer"]:
return None # transfer too small to qualify
bonus = gross_bonus(offer, portfolio)
fee_rebate = min(outgoing_fee, offer["outgoing_fee_rebate"])
net = bonus + fee_rebate
# opportunity cost: same portfolio in a 4% APY cash/CD alternative
alt_yield = portfolio * 0.04 * (offer["hold_days"] / 365)
true_profit = net - alt_yield
annualized = (net / portfolio) * (365 / max(offer["hold_days"], 1))
return {
"broker": offer["broker"],
"portfolio": portfolio,
"bonus": round(bonus, 2),
"fee_rebate": round(fee_rebate, 2),
"net_credits": round(net, 2),
"hold_days": offer["hold_days"],
"hold_note": offer["hold_note"],
"alt_4apy_cost": round(alt_yield, 2),
"true_profit_vs_4pct": round(true_profit, 2),
"annualized_pct": round(annualized * 100, 2),
}
def chain_plan(start_broker_idx, portfolio, path):
"""Simulate a rotation chain: list of offer indices to hop through."""
log, total = [], 0.0
fee_out = TYPICAL_OUTGOING_FEE
for idx in path:
m = model_rotation(OFFERS[idx], portfolio, fee_out)
if m is None:
return None
log.append(m)
total += m["net_credits"]
# next hop's outgoing fee may or may not be rebated; assume not
fee_out = TYPICAL_OUTGOING_FEE
return {"hops": log, "total_net": round(total, 2)}
def main():
portfolio = 25_000.0
print(f"ACAT Rotation Planner -- portfolio ${portfolio:,.0f}")
print("=" * 64)
for offer in OFFERS:
m = model_rotation(offer, portfolio)
if m is None:
print(f"{offer['broker']:<10} skip (min transfer "
f"${offer['min_transfer']:,.0f})")
continue
print(f"{m['broker']:<10} bonus ${m['bonus']:>9,.2f} | rebate "
f"${m['fee_rebate']:>6,.2f} | net ${m['net_credits']:>9,.2f}")
print(f"{'':<10} hold {m['hold_days']:>4}d ({m['hold_note']})")
print(f"{'':<10} cost of parking vs 4% APY: "
f"${m['alt_4apy_cost']:,.2f} -> true profit "
f"${m['true_profit_vs_4pct']:,.2f}")
print(f"{'':<10} annualized: {m['annualized_pct']}%")
print("-" * 64)
# chain: Moomoo -> Webull -> Firstrade (each hold fully, then hop)
path = [0, 1, 2] # indices into OFFERS
chain = chain_plan(0, portfolio, path)
if chain:
months = sum(h["hold_days"] for h in chain["hops"]) / 30.44
print(f"\nCHAIN: Moomoo -> Webull -> Firstrade")
print(f" total credits: ${chain['total_net']:,.2f}")
print(f" capital frozen: {months:.0f} months "
f"(tranches unlock earlier at Moomoo)")
print(f" effective: "
f"${chain['total_net'] / (months / 12):,.0f}/month on "
f"${portfolio:,.0f} parked")
print("\nBEST SINGLE HOP at each portfolio size")
for p in (5_000, 10_000, 25_000, 50_000, 100_000):
best = None
for offer in OFFERS:
m = model_rotation(offer, p)
if m and (best is None or m["net_credits"] > best["net_credits"]):
best = m
if best:
print(f" ${p:>8,.0f} -> {best['broker']:<10} "
f"net ${best['net_credits']:>9,.2f} "
f"({best['hold_days']}d hold)")
print("\nOK: planner completed successfully")
return 0
if __name__ == "__main__":
sys.exit(main())
Here’s the actual output from the test run on a $25,000 portfolio:
ACAT Rotation Planner -- portfolio $25,000
================================================================
Moomoo bonus $ 600.00 | rebate $ 0.00 | net $ 600.00
hold 360d (unlock tranches at 90/180/270/360 days)
cost of parking vs 4% APY: $986.30 -> true profit $-386.30
annualized: 2.43%
----------------------------------------------------------------
Webull bonus $ 750.00 | rebate $100.00 | net $ 850.00
hold 90d (bonus clawed back if assets leave early (~90 days))
cost of parking vs 4% APY: $246.58 -> true profit $603.42
annualized: 13.79%
----------------------------------------------------------------
Firstrade bonus $ 0.00 | rebate $100.00 | net $ 100.00
hold 365d (account must stay open 12 months)
cost of parking vs 4% APY: $1,000.00 -> true profit $-900.00
annualized: 0.4%
----------------------------------------------------------------
Public bonus $ 750.00 | rebate $ 0.00 | net $ 750.00
hold 2190d (paid over 72 months; withdrawals cut future installments)
cost of parking vs 4% APY: $6,000.00 -> true profit $-5,250.00
annualized: 0.5%
----------------------------------------------------------------
CHAIN: Moomoo -> Webull -> Firstrade
total credits: $1,550.00
capital frozen: 27 months (tranches unlock earlier at Moomoo)
effective: $695/month on $25,000 parked
BEST SINGLE HOP at each portfolio size
$ 5,000 -> Webull net $ 250.00 (90d hold)
$ 10,000 -> Webull net $ 400.00 (90d hold)
$ 25,000 -> Webull net $ 850.00 (90d hold)
$ 50,000 -> Webull net $ 1,600.00 (90d hold)
$ 100,000 -> Webull net $ 4,100.00 (90d hold)
Read the output honestly. The planner is telling you that Public’s flashy 3% is the worst offer on the board once you model the 72-month installment schedule, and that Webull’s “boring” 3% is worth more than four times as much annualized because you get the money in one piece after roughly 90 days instead of drip-fed over six years. That’s exactly the kind of trap the tool exists to catch. The honest conclusions:
- Under ~$20,000: Moomoo and Webull are close to equivalent on gross; Webull wins on speed. Take Webull first if the clawback window fits your patience.
- At $25,000-$100,000: Webull is the clear first hop ($850 to $4,100 net per hop at ~14% annualized). Moomoo’s cap makes it a rounding error above $20k.
- The chain math is real but slow: three sequential hops on a $25k account is $1,550 in credits over ~27 months of frozen capital. Compare that honestly against just holding an index fund — the planner shows the true profit line so you don’t fool yourself.
- The actual money-maker move: scaling. A $100k portfolio nets $4,100 per Webull hop with 90 days of freeze. Two hops a year on a six-figure portfolio is $6,000-$8,000/year for forms and patience.
The clawback traps that eat people
Every failed churn story I’ve read follows the same three patterns. Avoid them and this stays profitable:
1. Withdrawing before the hold clears. Moomoo tranches unlock at 90/180/270/360 days; pull assets out at day 89 and the unearned tranches vanish — sometimes retroactively clawed from your balance. Public cuts future installments the moment assets leave. Robinhood’s terms claw back pro-rated bonuses. Set calendar reminders the day the transfer settles, not the day you submit it.
2. The one-transfer-per-customer rule. Webull’s promo is explicitly for your first brokerage transfer. These are new-customer offers. The standard community workaround is spouses, family accounts, and trusts — separate legal customers, separate bonuses. That’s within the terms as written, but it multiplies your admin burden and every account needs its own tax paperwork. Don’t manufacture fake identities; that’s fraud, not churning.
3. Forced liquidation on weird assets. Fractional shares, some OTC stocks, and most crypto holdings cannot ACAT in-kind — the receiving broker liquidates them at the sending broker before transfer, which can trigger taxable gains at the worst possible moment. Before any hop, move problem positions to cash deliberately, on your own schedule, or transfer only the clean holdings. Options positions and unsettled trades can also block or delay an ACAT entirely.
A fourth minor one: pending dividends and corporate actions during the 5-7 day transfer window get forwarded late by mail or reclaimed later. Annoying, not costly. Just don’t initiate a transfer the week before a big ex-dividend date.
Tax treatment: the part that makes it clean
The transfer itself is not a sale. In-kind ACAT moves cost basis and holding period unchanged, no 1099 for the move. What does get taxed: the bonus is taxable income (you’ll get a 1099 for it), and any position that got force-liquidated is a taxable sale. So on a $750 Webull bonus, expect roughly $150-$370 going to federal+state depending on your bracket — the planner’s numbers are pre-tax, and the marginal offers get much weaker after tax. That’s another reason the fast, large, lump-sum offers beat the slow drips: same tax rate, less frozen time.
One optimization: the match is a percentage of transferred assets, not of gains — so this works identically on a stale portfolio you’d hold anyway. You are being paid a toll to walk your own money across a bridge. There’s no law against it, the brokers publish the offers on purpose, and FINRA’s rules require the receiving broker to process ACAT requests promptly. The grey area is only in aggregate: brokers collectively lose money on serial churners, which is why the terms keep tightening and why you should screenshot the offer terms on day one — when a clawback dispute happens months later, the saved promo page is your entire case.
Your first rotation, step by step
- Screenshot the current Webull transfer promo terms page. Note the match %, cap, clawback window, and expiration date.
- Check your current broker’s outgoing ACAT fee (usually on their transfer/FAQ page). Webull’s $100 reimbursement covers most of them.
- Move fractional shares and any non-ACAT-eligible positions to whole shares or cash first. Note cost basis of anything you sell.
- Open the Webull account, submit the ACAT, and wait 5-7 business days. Don’t trade around the transfer; unsettled activity can bounce it.
- The day the transfer settles, set calendar reminders at day 45 and day 85 of the clawback window.
- When the bonus posts and the window clears, run the planner again with the current offer map and pick the next hop.
The strategy scales linearly with portfolio size and requires almost no time after setup. The edge isn’t information — it’s that the paperwork is annoying enough that most people with $100k sitting in a 0.01% APY account will never do it. You just did.
Sources (all accessed October 25, 2026)
- Moomoo Transfer In Promotion terms (3% match, $600 cap, tranche unlock schedule): moomoo.com/us/support/topic4_603
- Webull Transfer Bonus (3-4% match, $2,000 minimum, fee reimbursement): webull.com/offers-promotions/transfer-fee-promo
- Firstrade ACAT funding & $250 rebate terms: firstrade.com/support/funding/acat
- Robinhood ACATS Bonus Offer Terms (1%, two-year earn-out PDF): cdn.robinhood.com/assets/robinhood/legal/Robinhood_ACATS_Bonus_Offer_Terms_and_Conditions.pdf
- Public transfer match terms (3%, 72-month installments, $1M cap): public.com/webull-transfer and public.com/disclosures/matchprogram
- FINRA guidance on customer account transfers (ACATS rules): finra.org/rules-guidance/key-topics/customer-account-transfers
- Webull match percentages and minimums cross-checked at transferalpha.com/blog/webull-transfer-bonus and vitaldollars.com