There is an API sitting in plain sight that most people who argue about gift card arbitrage on Reddit have never opened. GCX — the marketplace formerly known as Raise — publishes its entire inventory as a public XML feed at https://gcx.app/api/v1/gift-cards. No key, no login, no scraping. When I pulled it for this article it contained 97,174 live cards across 341 brands, each with its face value, asking price and discount percentage.
That single endpoint turns gift card arbitrage from folklore into a data problem. You can see every discount on the board, rank them, and decide where your money actually earns the most — before spending a dollar.
This article builds a working screener around that feed (tested end to end, exit code 0), then walks through the two ways this actually makes money: using discounted cards as your own payment rail, and flipping the spread on liquid brands. Both plays have hard limits, and I’ll give you the numbers that show exactly where they stop working.
How the money works
Gift card arbitrage has exactly two profitable forms. Everything else you read online is a variation of one of these.
Play A — Own the spend. You were going to spend the money anyway. Groceries at Target, gas, Uber rides, DoorDash, an Amazon habit you’re not fixing. If you can buy a $100 Target card for $92, your groceries cost 8% less and no resale step exists. This is the closest thing to free money in personal finance, and it scales to whatever you already spend. Someone running $4,000/month of life through discounted cards at an average 6% capture keeps ~$2,880/year for a few minutes of work per restock.
Play B — Flip the spread. Buy a card below what the resale market pays for it. The benchmark: CardCash pays sellers up to 92% of face value for in-demand brands (per CNBC’s October 2026 breakdown of the gift card resale market), and selling on GCX itself nets you at most 85 cents on the dollar because GCX charges a 15% seller commission. So a card you buy at an 18% discount and can sell near the top of that range has a few dollars of real spread per $100. Below a 15% discount on the buy side, the flip is dead on arrival.
Neither play is passive income. Play A is a payment optimization with near-zero risk. Play B is a genuine spread trade with real downside, and it lives or dies on one number: the discount you buy at versus the haircut the sell side takes. The screener below finds where that gap exists today.
The feed
Here’s what a raw record looks like:
<gift_card id="30690272">
<merchant>Kohl's</merchant>
<merchant_id>80</merchant_id>
<link>https://gcx.app/buy-kohl-s-gift-cards</link>
<value>500.0</value>
<price>474.24</price>
<electronic>true</electronic>
<voucher>true</voucher>
<discount>5.2</discount>
</gift_card>
Every field you need for a decision is right there: value (face), price (what GCX asks), discount (percent off face), and electronic (instant delivery vs. plastic in the mail). The feed is roughly 40 MB and refreshes as inventory moves, so a screener run gives you the live board.
Three practical notes before the code:
- Discount is not uniform within a brand. The feed showed Outback Steakhouse cards ranging from shallow single digits up to 18% off. The best card in a brand is the one you buy; the average is noise. That’s why the screener scores on the best discount per brand, not the mean.
- E-cards beat plastic for speed. Instant delivery means you can buy at 2 PM and resell the same afternoon. Physical cards mean shipping windows and more surface area for things to go wrong.
- The feed updates continuously. A brand at 30% off today can be at 12% next week. This is a snapshot business — you check the feed, act on what’s there, repeat.
The screener
The script pulls the feed once, streams it through lxml (so 40 MB of XML never becomes one giant DOM), groups cards by brand, and scores each brand for the play you’re running. Play A wants deep discounts on brands you’ll actually spend. Play B wants deep discounts on the short list of liquid brands that instant buyers pay top dollar for.
#!/usr/bin/env python3
"""
GCX (Raise) gift card arbitrage screener.
GCX runs a public inventory feed at https://gcx.app/api/v1/gift-cards
(plain XML, no key, no login). Every discounted card listed on the
marketplace is in there -- roughly 90,000+ cards across 300+ brands.
This script pulls the feed, groups it by brand, and scores each one for
the two ways real people make money off discounted gift cards:
PLAY A -- "Own the spend": buy discounted cards for money you were
already going to spend (groceries, gas, Amazon, Uber).
You keep the discount instead of a middleman. Zero
additional risk because the card gets consumed either way.
PLAY B -- "Flip the spread": buy a deep-discount card and resell it.
A brand is only worth flipping when its discount is deeper
than what instant buyers pay for it (CardCash pays up to
92% of face; selling on GCX itself nets at most 85% after
the 15% seller commission).
Usage:
python3 gcx_screener.py # top 20, saves CSV
python3 gcx_screener.py --min-disc 15 --min-cards 5 --top 30
python3 gcx_screener.py --play b # liquid flip-able only
Requires: pip install curl_cffi lxml
"""
import argparse
import csv
import io
import sys
import time
from collections import defaultdict
try:
from curl_cffi import requests
except ImportError:
sys.exit("pip install curl_cffi first")
FEED_URL = "https://gcx.app/api/v1/gift-cards"
# Brands with real resale demand: these are the names instant buyers
# (CardCash and friends) pay top dollar for. A 30% discount on a dying
# mall brand is a value trap; 15% on Uber is inventory.
LIQUID_BRANDS = {
"amazon", "walmart", "target", "apple", "best buy", "home depot",
"lowe's", "uber", "uber eats", "lyft", "doordash", "grubhub",
"airbnb", "delta", "southwest", "starbucks", "chipotle", "sephora",
"nike", "adidas", "gamestop", "domino's", "pizza hut",
"buffalo wild wings", "outback steakhouse", "cheesecake factory",
}
def fetch_feed(retries=3):
"""Pull the full GCX inventory XML (~40 MB)."""
last = None
for attempt in range(1, retries + 1):
try:
r = requests.get(FEED_URL, impersonate="chrome", timeout=120)
if r.status_code == 200 and len(r.content) > 1_000_000:
return r.content
last = f"HTTP {r.status_code}, {len(r.content)} bytes"
except Exception as e:
last = str(e)
time.sleep(3 * attempt)
sys.exit(f"feed fetch failed after {retries} attempts: {last}")
def parse_cards(raw):
"""Stream-parse so 40 MB of XML never sits in one DOM."""
from lxml import etree
cards = []
for _event, el in etree.iterparse(io.BytesIO(raw), events=("end",),
tag="gift_card"):
try:
cards.append({
"merchant": (el.findtext("merchant") or "").strip(),
"value": float(el.findtext("value") or 0),
"price": float(el.findtext("price") or 0),
"discount": float(el.findtext("discount") or 0),
"electronic": (el.findtext("electronic") or "") == "true",
})
except (TypeError, ValueError):
pass
el.clear()
while el.getprevious() is not None:
del el.getparent()[0]
return cards
def analyze(cards, min_disc, min_cards, play):
by_brand = defaultdict(list)
for c in cards:
by_brand[c["merchant"]].append(c)
rows = []
for brand, cs in by_brand.items():
# e-cards only when possible: instant delivery, no shipping lag
e_cards = [c for c in cs if c["electronic"]]
pool = e_cards if e_cards else cs
if len(pool) < min_cards:
continue
best = max(c["discount"] for c in pool)
if best < min_disc:
continue
liquid = brand.lower().strip("®©™ ").strip() in LIQUID_BRANDS
if play == "b" and not liquid:
continue
rows.append({
"brand": brand,
"best_discount_pct": round(best, 1),
"cards_listed": len(pool),
"face_value_available": round(sum(c["value"] for c in pool)),
"liquid": "YES" if liquid else "-",
"deepest_card_value": max(pool, key=lambda c: c["discount"])["value"],
})
rows.sort(key=lambda r: (-r["best_discount_pct"], -r["face_value_available"]))
return rows
def main():
ap = argparse.ArgumentParser(description="GCX gift card arbitrage screener")
ap.add_argument("--min-disc", type=float, default=10.0,
help="minimum best-discount %% (default 10)")
ap.add_argument("--min-cards", type=int, default=3,
help="minimum listed cards per brand (default 3)")
ap.add_argument("--top", type=int, default=20, help="rows to print")
ap.add_argument("--play", choices=["a", "b"], default="a",
help="a = all brands, b = liquid flip-able only")
ap.add_argument("--csv", default="gcx_opportunities.csv")
args = ap.parse_args()
print("[*] fetching GCX inventory feed (~40 MB)...", file=sys.stderr)
cards = parse_cards(fetch_feed())
print(f"[*] parsed {len(cards):,} live cards "
f"({time.strftime('%Y-%m-%d %H:%M UTC', time.gmtime())})",
file=sys.stderr)
rows = analyze(cards, args.min_disc, args.min_cards, args.play)
if not rows:
sys.exit("no brands match -- loosen --min-disc / --min-cards")
with open(args.csv, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader()
w.writerows(rows)
print(f"\n{'BRAND':<32}{'BEST%':>7}{'CARDS':>7}{'FACE $':>10}{'LIQ':>5}")
print("-" * 61)
for r in rows[: args.top]:
print(f"{r['brand'][:31]:<32}{r['best_discount_pct']:>6.1f}"
f"{r['cards_listed']:>7}{r['face_value_available']:>10,}"
f"{r['liquid']:>5}")
print(f"\n[+] {len(rows)} brands at {args.min_disc}%+ -> full list: {args.csv}")
if __name__ == "__main__":
main()
I ran this against the live feed on 25 October 2026. Both invocations below exited 0; outputs are verbatim.
Play A — everything at 20%+ discount, at least 10 cards listed (python3 gcx_screener.py --min-disc 20 --min-cards 10 --top 15):
[*] fetching GCX inventory feed (~40 MB)...
[*] parsed 97,174 live cards (2026-10-25 15:17 UTC)
BRAND BEST% CARDS FACE $ LIQ
-------------------------------------------------------------
Zaxby's 30.0 343 10,670 -
UNO Pizzeria & Grill® 30.0 30 990 -
Kenneth Cole 30.0 10 650 -
California Pizza Kitchen 29.2 433 21,610 -
Landry's 28.1 191 7,927 -
Smashburger® 26.9 131 3,275 -
Potbelly 26.0 51 1,475 -
Lucille's BBQ 25.0 175 8,750 -
Bravo Cucina Italiana 24.4 249 6,225 -
Macaroni Grill 24.2 71 2,347 -
Regal Cinemas 22.9 547 27,196 -
Aeropostale 22.6 359 12,304 -
Del Frisco's Grille 22.1 11 400 -
Topgolf 22.0 1667 86,898 -
Krispy Kreme® 21.6 769 11,035 -
[+] 24 brands at 20.0%+ -> full list: article_run1.csv
Play B — liquid brands only, 5%+ discount (python3 gcx_screener.py --play b --min-disc 5 --min-cards 3 --top 12):
BRAND BEST% CARDS FACE $ LIQ
-------------------------------------------------------------
Outback Steakhouse 18.0 617 28,638 YES
Buffalo Wild Wings 15.0 296 13,362 YES
DoorDash 13.7 2979 146,760 YES
Uber 12.0 1708 164,280 YES
adidas 11.4 221 15,012 YES
Uber Eats 10.3 120 8,300 YES
Pizza Hut 8.0 27 1,225 YES
Southwest® 6.6 222 86,645 YES
Sephora 6.4 1481 90,480 YES
GameStop 5.7 42 6,372 YES
[+] 10 brands at 5.0%+ -> full list: article_run2.csv
Look at the two tables side by side and you can already feel the trap this market sets.
Where the deep discounts live — and why most are a trap
The 20%+ table is a graveyard of mid-chain restaurants and mall apparel: Zaxby’s, California Pizza Kitchen, Macaroni Grill, Aeropostale. The discounts are real — Topgolf had $86,898 of face value listed at up to 22% off, California Pizza Kitchen $21,610 at up to 29.2% — but real in the wrong direction. These brands are deep-discounted because nobody wants them. If you don’t eat at CPK four times a month, a 29% discount on it is worth exactly zero to you, and if you try to flip it, the buyers who exist want it even cheaper than GCX does.
The liquid table is the mirror image: Uber at 12%, DoorDash at 13.7%, Outback at 18% — brands with actual demand, discounted less because supply of willing sellers is thinner.
Now run the flip math on the best liquid candidate. Outback Steakhouse at 18% off means you buy $100 of face for $82. Sell it back into the market and the ceiling is GCX’s own best case: 85% of face after the 15% commission. That’s +$3 per $100 cycled. Sell the same $100 card to an instant buyer at a typical ~88-90% offer and you land somewhere between break-even and +$8, depending entirely on the offer you get that day. The same day’s feed showed Uber’s deepest card at 12% off — buy at $88, and even the 85% GCX ceiling puts you at −$3. The spread on liquid brands is real but thin: single-digit dollars per $100, and negative the moment the buy discount slips under ~15%.
That’s the honest sizing for Play B: it is not a “thousands per month” machine at retail scale. A thousand dollars of capital cycling the best liquid brand at $3-8 per $100, with maybe one full cycle per week once you count verification and payout waits, produces $120-320/month while your capital is locked and exposed. People who make real money here do it with volume (repeated larger cycles on whichever brand is widest that week) or with Play A on top of their entire household spend.
Play A, scaled, is the more reliable number: capture 6-15% on money you were already spending and the savings compound monthly with zero resale risk. A household pushing $5,000/month of spend through discounted cards at an 8% blended capture banks $4,800/year — boring, repeatable, and it doesn’t depend on anybody’s buy offer.
The operational playbook
Run the screener daily, act on snapshots. The board moves. Outback at 18% on a Tuesday can be gone by Thursday. The whole edge is being the person who sees the repricing first and has capital ready. A cron job running the script once a morning and emailing you brands over your threshold is twenty lines more code.
For Play A: buy brands in your actual spending pattern. Uber, DoorDash, Target, Amazon, your gym’s smoothie chain, whatever. Buy the deepest e-card in the brand, spend it down, restock when it’s empty. Never buy a brand you don’t spend on just because the percentage is big.
For Play B: the exit comes before the entry. Before you buy anything, check what CardCash’s current offer is for that brand (their payout page shows up to 92% of face for the strongest names) and what similar cards list for on GCX itself. No exit ≥ your entry + 5%? Pass. There is always another snapshot tomorrow.
Prefer e-cards. Every card in the Play B table’s liquid hits was electronic — instant code delivery. You want the version of this trade with the shortest possible holding window.
The risks (real ones, in order of how much they cost people)
Funds aren’t instant. CardCash pays via PayPal, ACH, or virtual Visa, with timelines running up to several business days for some offers. During that window your capital is out and your risk exposure doesn’t stop. Flash sellers treat this float as part of the cost of doing business; beginners don’t budget for it and stall.
A dead card is your loss, not the platform’s. If a card you bought doesn’t redeem, you eat it. GCX backs marketplace purchases with a 90-day money-back guarantee, and CardCash advertises 45-day buyer protection — but guarantees have conditions, and the claim process is exactly as fast as you’d expect. Verify every card the day it arrives (brand’s balance-check page, screenshot everything) so you’re inside the window if something’s wrong. This is also why single-brand concentration is dumb: cap any one brand at a fraction of your float.
Cards bought with stolen funds get clawed back. This is the ugly underbelly of the resale market: a card funded by a stolen credit card can be voided by the issuer weeks later, after you’ve resold it — leaving chargebacks and a loss that’s entirely yours. Stick to the established marketplaces with verification processes; the “too good to be true” discounts on Telegram channels and DMs are where those cards end up.
Platform risk cuts both ways. Selling side: marketplaces can change commission, payout terms, or delist brands without notice. Buying side: GPT/rewards platforms vary on whether reselling cards bought with rewards violates their terms — Swagbucks and Freecash don’t explicitly prohibit it, but “commercial use” language exists, and platforms ban first and answer tickets later. If you’re sourcing inventory from rewards redemptions, read the terms and don’t build the whole operation on one account.
Tax and reporting. Regular resale is a trading activity to most tax authorities, not a hobby. The UK’s HMRC explicitly treats regular gift card reselling as trading; platforms may report activity; depending on your country, thresholds for “professional income” are lower than you’d guess. If you’re doing this at thousands per month, it’s a business — run it like one, with records and a real answer for the tax question in your jurisdiction.
Gift cards can expire or carry fees in specific US states and on specific card types — the federal CARD Act of 2009 protects most store gift cards for five years from issuance or last load, but state rules and card terms add wrinkles, and a card sitting in a drawer through a holding period is a card exposed to all of them. Another reason holding windows should be measured in days.
What I’d actually do with $2,000
No mystery: split it. Put $1,500 to work in Play A against the spending you already do — set up a restock cadence on the two or three brands your life actually touches, harvest 6-15% on every reload, done. Take the remaining $500 and run Play B only when the screener finds liquid brands at 15%+ — which, from this snapshot, means roughly one brand per week is even eligible. Log every trade: buy discount, exit route, days held, net after fees. After 20 trades you’ll know your real spread, and you can size up or shut it down with data instead of YouTube numbers.
The screener is the asset here, not any single trade. The feed is public, the code above runs as-is, and checking it costs you one cron job. Most people in this market never look at the full board — they buy whatever deal crosses their feed. That’s the asymmetry you’re exploiting.
Sources for the numbers in this piece: GCX inventory feed, pulled 25 October 2026 (97,174 cards, 341 brands — all discount and face-value figures verbatim from that run); CNBC Select, “How to Sell Unwanted Gift Cards for Cash,” updated 2 October 2026 (GCX 15% seller commission / cards sold at up to 30% off; CardCash pays sellers up to 92%, no direct fees; Prepaid2Cash fees); CardCash seller terms (45-day buyer protection, up to 92% payout advertising); GCX marketplace terms (90-day money-back guarantee, 4,000+ retailers); Gameflip seller fee schedule (8% + 2% + 2% digital, from Gameflip’s own become-a-seller page) as an alternative exit route. Everything else is arithmetic from those inputs.