A brand I work with sells the same catalog across four systems that don't talk to each other: Amazon FBA, Amazon's AWD (Warehousing & Distribution), a third-party warehouse for overflow, and a Shopify store. Simple question, no simple answer: how many units of SKU X do I actually have, everywhere, and how fast am I selling them?
Answering it meant logging into four dashboards, exporting four spreadsheets, and reconciling them by hand โ because each system names the same physical product differently. FBA calls it a sellerSku. The catalog calls it a modelNumber. AWD uses its own SKU string. Finance reports use SellerSKU with different casing. Shopify has its own variant SKUs. So the reconciliation wasn't even the hard part โ matching the rows to each other was.
This is a build log for the portal I put together to kill that problem, built with Claude Code over a weekend. It's not a magic AI-runs-your-business story. It's a deterministic data pipeline I could have hand-written in a week, except Claude Code compressed the week into two evenings. I'll walk the stack, the thing that broke it, the honest cost, and what any operator can lift.
The problem, in hours
Before the portal, getting one clean "units on hand + velocity" view across all four systems was roughly a 90-minute manual job, and nobody did it more than once a week because it was miserable. FBA summaries in one tab, AWD inventory in another, a 3PL login with its own auth, a Shopify export, then a spreadsheet where you pray the SKUs line up.
The cost of not doing it is worse than the 90 minutes. When you can't see total on-hand across FBA + AWD + 3PL against real sell-through, you make two expensive mistakes: you stock out on Amazon while units sit in the 3PL, and you reorder things you're actually deep on because one dashboard looked low. Both are real money โ lost Buy Box on the stockout, tied-up cash and storage fees on the overstock. The manual view was too slow to prevent either.
So the target wasn't a dashboard for its own sake. It was: one Postgres database, refreshed on a schedule, that holds every source and joins them on a single reliable key.
The stack
Nothing exotic. That's the point โ the leverage was in assembling it fast, not in any clever component.
- Node.js scripts, one per source, each isolated so one failure doesn't block the rest
- Amazon SP-API for FBA inventory, AWD inventory + velocity, finances, and the returns / sales-and-traffic reports (async Reports API: create โ poll โ download โ decompress)
- Tactical Logistics API (the 3PL) for warehouse on-hand
- Shopify Admin API for catalog, live inventory, and 90-day order lines
- Postgres as the single source of truth
- An orchestrator,
refresh-all.mjs, that runs every source in dependency order
Dependency order matters and it's the kind of thing you get wrong at 11pm. FBA has to run first, because pulling FBA inventory is what seeds the product table and the SKU crosswalk that everything downstream needs to resolve ASINs. AWD and finances can't resolve their own SKUs to ASINs without that seed. So the orchestrator is explicit about it:
const STEPS = [
['FBA inventory + product + crosswalk', 'ingest-fba.mjs'],
['AWD + velocity', 'ingest-awd-velocity.mjs'],
['Tactical 3PL', 'ingest-3pl.mjs'],
['Shopify catalog + orders + inventory','ingest-shopify.mjs'],
['returns + sales & traffic', 'ingest-returns-sales.mjs'],
['finances (paced, slow)', 'ingest-finances.mjs'],
];
Each step spawns as its own child process. If the 3PL login times out, the Shopify and finance pulls still complete. The whole refresh reports a per-step OK/FAIL summary at the end so you can see at a glance what didn't land. When I ran it end to end, the full multi-source refresh completed in a few minutes โ versus the 90-minute manual reconciliation it replaced.
How I actually used Claude Code
I didn't ask a model to "build me an inventory portal." That's how you get a plausible-looking pile of code that fails on the first real API response. I used it the way it's actually good: probe first, then build against what came back.
For every source, step one was a throwaway probe-*.mjs script โ hit the endpoint, dump the raw JSON, look at the real field names. SP-API's documentation and SP-API's actual payloads are not always the same shape, and neither is a 3PL's. The workflow with Claude Code was roughly:
"Here's the raw JSON from the FBA
/fba/inventory/v1/summariesendpoint (pasted). Write an ingest script that upsertssellerSku,asin, and the fulfillable/inbound quantities into afba_inventorytable, paginating onnextToken, capped at 20 pages. Never overwrite thecostcolumn on the product table โ COGS is entered manually and must survive a refresh."
That last sentence is the kind of constraint that matters more than the code. COGS is hand-entered and stable; an automated refresh must never clobber it. Getting Claude Code to write upserts that touch inventory fields but leave cost alone was one line of instruction, and it saved a category of silent data corruption I'd have found weeks later when a margin number looked insane.
The pattern repeated per source: paste the real payload, state the table shape, state the constraints (pagination caps, what not to overwrite, retry-on-throttle), review the diff, run it against live data, fix the one thing that broke. Two evenings, six sources.
What broke: the crosswalk
The pipeline ran clean on every individual source and still gave wrong answers, because the SKUs didn't join. This is the actual hard problem in any multi-system inventory project, and it's invisible until you check it.
The failure looks like this: FBA reports 400 units of PUP-BLU-2PK. AWD reports 600 units of pup-blu-2pk. Finance reports revenue against PUP_BLU_2PK. Shopify sells PUPIBOO-BLUE-2. To a human these are obviously the same product. To a JOIN they are four different rows, and your "total on hand" is silently wrong โ you think you have 400 when you have 1,000, or the finance numbers attach to nothing.
I only caught it because I stopped to measure join coverage instead of trusting it. I built a build-crosswalk.mjs whose only job was to answer: of every SKU in every source, how many auto-join to a single ASIN? The fix had three parts:
- Build the crosswalk truth from the sources that carry both keys โ the velocity report, FBA summaries, and the catalog all have SKU and ASIN in the same row. Those become the map.
- Normalize aggressively before matching โ lowercase, trim, collapse whitespace.
PUP-BLU-2PKandpup-blu-2pkare the same key or nothing works. - Then test the orphans โ measure how many AWD SKUs and finance
SellerSKUs fail to join to the map, and surface them, so the unmatched ones get a manual alias instead of silently vanishing from the totals.
The lesson generalizes to any inventory or finance consolidation: don't assume your systems agree on identifiers. Measure the join coverage explicitly, and treat the SKU crosswalk as its own first-class thing to maintain โ not an afterthought you bury inside an ingest script. A pipeline that runs perfectly and joins wrong is more dangerous than one that crashes, because it looks like it's working.
The honest cost and payoff
Build cost: two evenings of my time plus a few dollars of Claude Code usage. Hand-writing the same thing โ reading SP-API docs, handling the async Reports lifecycle, the 3PL's bearer-token auth, Shopify pagination, and the crosswalk โ would have been a solid week. The compression came from pasting real payloads and getting correct ingest code back in seconds, not from any autonomy.
Runtime cost: effectively nothing. It's a scheduled Node refresh hitting APIs the brand already has access to. No model runs at refresh time โ this is deterministic ETL on a cron, not an agent making decisions. I want to be precise about that, because there's a lot of "AI agent manages your inventory" marketing that this is not. The intelligence went into building it; the thing that runs is boring on purpose, which is exactly what you want touching your inventory data.
Payoff: the 90-minute weekly reconciliation became a report you open. More importantly, the decision it enables โ "I have 1,000 units across FBA and AWD, I'm selling 40 a day, that's 25 days of cover, reorder now" โ is now answerable in seconds instead of being skipped because the data was too annoying to assemble. The stockout-while-3PL-is-full mistake and the reorder-what-you're-deep-on mistake both get caught before they cost anything.
What an operator can copy
You don't need this exact portal. You need the pattern:
- Pick the one question you keep answering by hand across multiple logins. Inventory across fulfillment channels is a common one; so is "true contribution margin per SKU after every fee." If you're exporting and reconciling spreadsheets weekly, that's your candidate.
- Probe before you build. Dump the real API responses first. Build against reality, not documentation.
- Isolate each source so one broken login doesn't kill the whole refresh, and print a pass/fail summary.
- Treat identifier matching as its own problem. The moment you pull from two systems, assume they name things differently, and measure the join coverage before you trust any total.
- Protect hand-entered data (COGS, aliases, notes) from getting overwritten by automated refreshes.
- Keep the runtime dumb. Use the AI to build the pipeline; let the pipeline run deterministically. Your inventory numbers should not depend on a model's mood at 6am.
The unlock here wasn't intelligence at runtime. It was collapsing a week of undifferentiated API plumbing into a weekend, so a real operator problem โ where are my units and how fast are they moving โ finally got solved instead of tolerated.