← back to Pattern Vault
wpb-uploaders: Etsy createDraftListing payload mapping + --dry-run (38 payloads, gated on OAuth+taxonomy); per-platform STATUS.md
6662111a27300e6090e3250c579fcf071073d105 · 2026-07-09 18:14:49 -0700 · Steve
Files touched
A wpb-uploaders/STATUS.mdM wpb-uploaders/platforms/etsy.jsM wpb-uploaders/run.js
Diff
commit 6662111a27300e6090e3250c579fcf071073d105
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 9 18:14:49 2026 -0700
wpb-uploaders: Etsy createDraftListing payload mapping + --dry-run (38 payloads, gated on OAuth+taxonomy); per-platform STATUS.md
---
wpb-uploaders/STATUS.md | 23 ++++++++++++++
wpb-uploaders/platforms/etsy.js | 66 ++++++++++++++++++++++++++++++++++++-----
wpb-uploaders/run.js | 4 +++
3 files changed, 85 insertions(+), 8 deletions(-)
diff --git a/wpb-uploaders/STATUS.md b/wpb-uploaders/STATUS.md
new file mode 100644
index 0000000..348b036
--- /dev/null
+++ b/wpb-uploaders/STATUS.md
@@ -0,0 +1,23 @@
+# WPB Uploaders — build status (2026-07-09)
+
+Honest state of each platform. NONE is armed. Real uploads flow only after the per-platform
+blocker clears AND Steve arms it (`touch platforms/<key>.ARMED`).
+
+| Platform | Code state | Exact blocker to arming | Type |
+|----------|-----------|--------------------------|------|
+| **Spoonflower** (Fernwick) | fail-loud ✓, filechooser ✓, consent net-block + iframe-dismiss ✓ | **File doesn't register post-filechooser** — the "Choose Files" native chooser fires and `setFiles` runs, but the upload never starts (stays on the empty `/new` form, no preview/Save). Needs a **HEADED watch session** to see the post-select state (preview? validation reject? anti-automation no-op?). | web (armed) |
+| **Etsy** (SteveAbramsStudios) | `createDraftListing` payload mapping ✓ (38 built, `--dry-run` proves it) | **(1) ETSY_OAUTH_TOKEN** (Steve-gated OAuth for the shop); **(2) taxonomy_id unresolved** (`plan.taxonomyIdResolved` is null — resolve via getSellerTaxonomyNodes once authed) | API |
+| **Creative Market** | disarmed stub (navigates to upload URL, fail-loud) | **Session capture** — need Steve to log in once → `sessions/creativemarket-state.json`; then live DOM calibration | web |
+| **Redbubble** | disarmed stub, fail-loud | **Session capture** + calibration; **AI-hostile** → anti-bot cadence critical (ban risk) | web |
+| **Society6** | disarmed stub, fail-loud | **Account still PENDING confirmation** — cannot even session-capture yet | web |
+| **Patternbank** | disarmed stub, fail-loud | **Account still PENDING confirmation** — cannot even session-capture yet | web |
+
+## What's autonomously reversible from here
+- Spoonflower: nothing more headless — needs the headed session (Steve or a watched run).
+- Etsy: once Steve provides OAuth, wire the real `fetch` POST behind the token check + resolve taxonomy — the mapping is done.
+- CM/Redbubble: once Steve captures a session, calibrate each `uploadFn` against the live DOM.
+- Society6/Patternbank: blocked on account confirmation — nothing to do until then.
+
+## Standing safeguards (never loosened)
+Fail-loud (real id or `failed[]`), ≤4/day cap, human-typed fields, dry-run-unless-`.ARMED`,
+for-sale stays OFF until payout/tax per platform. Arming is drafted to `~/.claude/yolo-queue/pending-approval/`.
diff --git a/wpb-uploaders/platforms/etsy.js b/wpb-uploaders/platforms/etsy.js
index 84cb74d..43fe876 100644
--- a/wpb-uploaders/platforms/etsy.js
+++ b/wpb-uploaders/platforms/etsy.js
@@ -1,17 +1,67 @@
/* Etsy uploader — brand: Wallpaper's Back · shop SteveAbramsStudios
- * DISARMED. Etsy is API-based (v3 createDraftListing) — NOT a Playwright web upload.
+ * Etsy is API-based (v3 createDraftListing) — NOT a Playwright web upload.
* The listing plan already exists at pattern-vault/data/etsy-listings-plan.json (38 planned,
- * price $149 digital download). Arming = obtaining Etsy OAuth for the shop + pushing drafts.
- * This module is API-shaped, so base.js's Playwright driver is bypassed for Etsy (see run.js). */
-const path = require('path'), os = require('os');
+ * digital download @ $149). This module BUILDS the createDraftListing payloads from that plan
+ * and can print them (--dry-run, $0, no network). The live POST is gated behind ETSY_OAUTH_TOKEN
+ * (Steve-gated) AND a resolved taxonomy_id + shop_id.
+ */
+const path = require('path'), os = require('os'), fs = require('fs');
+
+const PLAN = path.join(os.homedir(), 'Projects/pattern-vault/data/etsy-listings-plan.json');
+
+// Map one plan entry + the plan's shared `fixed` block → an Etsy v3 createDraftListing payload.
+function toPayload(entry, fixed, taxonomyId) {
+ const b = entry.body || {};
+ return {
+ quantity: b.quantity || fixed.quantity || 999,
+ title: (b.title || '').slice(0, 140), // Etsy title max 140 chars
+ description: b.description || '',
+ price: fixed.price, // $149 digital
+ who_made: fixed.who_made || 'i_did',
+ when_made: fixed.when_made || '2020_2026',
+ taxonomy_id: taxonomyId || null, // MUST be resolved before live POST
+ type: fixed.type || 'download', // digital download
+ is_supply: false,
+ is_digital: true,
+ state: 'draft', // ALWAYS draft — never auto-publish
+ _imageFile: entry.imageFile || null, // uploaded separately via uploadListingImage
+ };
+}
+
+function buildPayloads() {
+ if (!fs.existsSync(PLAN)) return { error: `no plan file at ${PLAN}` };
+ const plan = JSON.parse(fs.readFileSync(PLAN, 'utf8'));
+ const fixed = plan.fixed || {};
+ const taxonomyId = plan.taxonomyIdResolved || null;
+ const payloads = (plan.plan || []).map(e => toPayload(e, fixed, taxonomyId));
+ return { count: payloads.length, taxonomyResolved: !!taxonomyId, price: fixed.price, payloads };
+}
+
module.exports = {
key: 'etsy', name: 'Etsy', brand: "Wallpaper's Back", apiBased: true,
shop: 'SteveAbramsStudios',
- planFile: path.join(os.homedir(), 'Projects/pattern-vault/data/etsy-listings-plan.json'),
+ planFile: PLAN,
endpoint: 'POST /v3/application/shops/{shop_id}/listings (createDraftListing)',
- // returns {id,url} per created draft, else {reason}. Needs ETSY_OAUTH_TOKEN + shop_id.
+ buildPayloads,
+
+ // dryRun: print the payloads we WOULD send (proves the mapping, $0, no network).
+ dryRun() {
+ const built = buildPayloads();
+ if (built.error) return built;
+ return {
+ dryRun: true, count: built.count, price: built.price,
+ taxonomyResolved: built.taxonomyResolved,
+ blockers: [
+ !process.env.ETSY_OAUTH_TOKEN && 'ETSY_OAUTH_TOKEN missing (Steve-gated OAuth)',
+ !built.taxonomyResolved && 'taxonomy_id unresolved (plan.taxonomyIdResolved is null)',
+ ].filter(Boolean),
+ sample: built.payloads.slice(0, 2),
+ };
+ },
+
+ // live path — DISARMED: refuses without a token; even with one, the POST stays Steve-gated.
async uploadFn() {
- if (!process.env.ETSY_OAUTH_TOKEN) return { reason: 'etsy DISARMED — no ETSY_OAUTH_TOKEN; obtain shop OAuth then push drafts from etsy-listings-plan.json' };
- return { reason: 'etsy uploadFn not yet wired — DISARMED (token present but draft-push not enabled; Steve-gated)' };
+ if (!process.env.ETSY_OAUTH_TOKEN) return { reason: 'etsy DISARMED — no ETSY_OAUTH_TOKEN; run `node run.js etsy --dry-run` to preview payloads' };
+ return { reason: 'etsy DISARMED — token present but live createDraftListing POST is Steve-gated (arm via pending-approval)' };
},
};
diff --git a/wpb-uploaders/run.js b/wpb-uploaders/run.js
index 07026dd..26fd846 100644
--- a/wpb-uploaders/run.js
+++ b/wpb-uploaders/run.js
@@ -32,6 +32,10 @@ function loadPlatform(key) { return require(path.join(__dirname, 'platforms', `$
const manifest = manifestArg || path.join(require('os').homedir(), 'Projects/pattern-vault/data/catalog.json');
if (cfg.apiBased) { // Etsy path — no browser
+ if (process.argv.includes('--dry-run') && cfg.dryRun) {
+ console.log(JSON.stringify({ platform: cfg.key, apiBased: true, ...cfg.dryRun() }, null, 2));
+ return;
+ }
const r = await cfg.uploadFn();
console.log(JSON.stringify({ platform: cfg.key, apiBased: true, result: r }, null, 2));
return;
← 7fb0d68 spoonflower: proven filechooser selector + network-block con
·
back to Pattern Vault
·
SPOONFLOWER UPLOAD WORKS (real id 22723313 proven): late-con f72c062 →