source/skills/payments/SKILL.md
Walks the merchant through Stripe onboarding, captures their API keys, and writes them to Vercel env vars. Handles the common case where KYC takes days by supporting a preview-mode deploy path.
npx skillsauth add mitcheman/bodega paymentsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Sets up Stripe so the store can take money. Most likely to pause for hours or days — Stripe KYC requires legal name, tax ID, bank info, and sometimes ID documents.
.bodega.md. Apply the resume contract from
setup/SKILL.md. Substep labels (in order):
mode-chosen → publishable-key-stored → secret-key-stored →
payments-config-recorded. Resume picks up at
payments.last_completed_step + 1.state.payments: done, verify keys still work. Ask if the user
wants to rotate or re-enter.handoff: true, note that the merchant (not the operator) is
the one doing Stripe KYC.STRIPE_SECRET_KEY and
STRIPE_PUBLISHABLE_KEY are set in the shell env (or STRIPE_API_KEY,
which is Stripe's own canonical env-var name for the secret), skip
to "Headless path" below. No browser, no chat-paste flow.Use this when running unattended (CI, automation, or an agent run against a pre-existing Stripe account). The merchant has already created the account and pulled keys; this skill just provisions them on Vercel.
# Both keys read from the agent's shell env, not chat:
PUBLISHABLE_KEY="${STRIPE_PUBLISHABLE_KEY:?STRIPE_PUBLISHABLE_KEY is required for headless mode}"
SECRET_KEY="${STRIPE_API_KEY:-${STRIPE_SECRET_KEY:?STRIPE_SECRET_KEY (or STRIPE_API_KEY) is required for headless mode}}"
# Shovel both into Vercel env without echoing.
vercel env add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY production <<< "$PUBLISHABLE_KEY"
vercel env add STRIPE_SECRET_KEY production <<< "$SECRET_KEY"
Detect mode from the prefix: pk_live_/sk_live_ → live; pk_test_/
sk_test_ → test. Write stripe.mode to .bodega.md accordingly.
Skip the rest of the interactive flow (Steps 1–4) and jump to Step 5. Headless mode never warns about chat-transcript leaks because the keys never touched chat — they came from env.
Phase 1 does not use Stripe Connect. Each merchant sets up their own vanilla Stripe account. The link is:
https://dashboard.stripe.com/register?email=<URL-encoded merchant.email>
URL-encode the email. Stripe's register endpoint receives it via
query string; without encoding, +-tagged emails ([email protected])
decode to a literal space (user [email protected]) and the form
prefills wrong. Use encodeURIComponent() (or shell jq -rR @uri)
before substitution.
Next, we need to set up payments through Stripe. They handle the cards so you don't have to worry about that part.
You'll need about 10 minutes and:
- Your bank account number + routing number
- Your SSN (sole proprietor) or EIN (if you have a business)
This is federally required — every online store does this, from Etsy to Shopify. Not something we can skip.
Open this in your browser: → https://dashboard.stripe.com/register?email=<email>
Tell me "done" when finished. Take your time; you can do this on your phone.
Next, Stripe — for payments. Your [partner/friend/client] needs to do this part because it's attached to their bank account and legal name.
I'll email them instructions. They'll need ~10 minutes and their bank info. Takes 10 min to 2 days depending on whether Stripe asks for ID docs.
[Send email to merchant.email with the registration link]
We can keep going without waiting — I can deploy your site in preview mode (customers see it, checkout is disabled with "Store opening soon") and flip it on when their Stripe is live.
Want to wait or deploy in preview mode?
Stripe onboarding: https://dashboard.stripe.com/register?email=<email> [Handoff: emailed to merchant.email]
Keys needed: pk_live_... and sk_live_... Can proceed in preview mode if KYC isn't done. Your call.
Before capturing keys, ask the merchant which mode they're in:
| Mode | When to use | Keys look like |
|---|---|---|
| Test | KYC not complete yet, or local development against Stripe | pk_test_... / sk_test_... |
| Live | KYC complete, ready to take real money | pk_live_... / sk_live_... |
Default to live if the merchant says KYC is done. Default to test
otherwise — checkout works end-to-end against Stripe's test card
numbers, no real money moves, and you can flip to live by re-running
{{command_prefix}}bodega:payments once KYC clears.
Don't conflate test mode with preview mode. Preview mode (Step below) ships a public site with checkout disabled. Test mode ships a fully working checkout that uses Stripe's sandbox. Both are valid while waiting on KYC; they solve different things.
Ask the merchant:
Are you in test mode or live mode?
a. Test mode — Stripe hasn't verified my business yet (or I'm just trying things out). Real cards won't charge. b. Live mode — Stripe is verified, I'm ready to take money.
If unsure, pick test — we can flip to live anytime.
mode = test | live? Default test if KYC ispending.
Store the answer as stripe.mode in .bodega.md.
Merchant grabs two values from Stripe Dashboard → Developers → API keys, matching the mode picked in Step 3:
pk_test_... or pk_live_...)sk_test_... or sk_live_...)The agent must never see the secret key, because Claude Code (and
similar agents) write entire session transcripts to local JSONL files
under ~/.claude/projects/.../*.jsonl. Once a sk_* value lands in
a transcript, it lives on disk indefinitely.
The default flow inverts the previous design: the agent only handles the public key; the secret key is entered by the user directly into their own terminal, in a separate window the agent never reads.
The publishable key is meant to be public — it ships in client JS to every visitor's browser. Pasting it into chat is fine.
Ask:
Paste your publishable key (starts with
pk_test_orpk_live_):
Validate format (pk_test_ or pk_live_ prefix, ~107-char length).
Write to Vercel env:
vercel env add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY production <<< "<pk_value>"
(The NEXT_PUBLIC_ prefix is required for Next.js to ship the value
to client-side code. The <Checkout> component reads it in the
browser to initialize Stripe Elements.)
Tell the user:
Now the secret key. This one's different — it can charge cards and read customer data, so I shouldn't see it. Here's what I want you to do:
1. Open a new terminal window (Terminal app on Mac, Command Prompt on Windows). Don't paste it in this chat. 2. Paste this command into the new window (everything between the lines, exactly):
``` vercel env add STRIPE_SECRET_KEY production ```3. When it asks "What's the value of STRIPE_SECRET_KEY?" — paste your secret key there and hit Enter. 4. When it asks which environments to apply to, accept the default (production). 5. Come back here and tell me "done."
I'll verify the key landed without ever seeing the value myself.
In a separate terminal (out of agent context):
vercel env add STRIPE_SECRET_KEY production, paste at prompt. Tell me "done"; I'll verify withvercel env ls(no value exposure).
When they say done, verify the env var name appears in the project:
vercel env ls production | grep -q '^STRIPE_SECRET_KEY\b' \
|| { echo "❌ STRIPE_SECRET_KEY missing on Vercel."; exit 1; }
Can't verify the value isn't empty here. On Vercel CLI 52+,
vercel env pulldoes not decrypt encrypted/sensitive values to disk — there's no way to read the value back from the CLI to confirm it's non-empty. Avercel env lsrow for a name with an empty value looks identical to a row for a name with a real value.The safe path: use the interactive form of
vercel env add(paste at the prompt, press Enter) which handles the trailing newline correctly. Avoidprintf "%s" "$VALUE" \| vercel env addandecho -n "$VALUE" \| vercel env add— both strip the newline and silently write empty strings (CLI prints "Added" regardless). The deploy SKILL's post-deploy smoke test (Step 7.5) catches emptySTRIPE_SECRET_KEYbecause the first webhook registration attempt 401s against Stripe.
The merchant email is fine to write from chat:
vercel env add BODEGA_MERCHANT_EMAIL production <<< "<merchant.email>"
Never commit any of these to a file in the repo. They live in Vercel only.
If the user is on a phone, in a sandbox without a separate terminal, or otherwise can't follow the agent-safe path:
vercel env add STRIPE_SECRET_KEY production <<< "<sk_value>").{{command_prefix}}bodega:payments with the rotated value via the
agent-safe flow.This fallback is intentionally inconvenient — the cost of the warning
Stripe webhooks need the live URL. Defer registration to the deploy
skill's post-deploy step. Note in .bodega.md:
state:
payments: done # or "pending"
webhook_configured: false # deploy will set true
Update .bodega.md:
state:
payments: done # or "pending"
stripe:
mode: live # "live" or "test"; from Step 3
account_email: [email protected]
keys_stored: vercel-env
publishable_key_preview: "pk_live_...<last4>" # or pk_test_...
# secret key never recorded, ever
Return to setup.
If the merchant can't complete KYC in this session:
state:
payments: pending
preview_mode: true
The deploy skill reads preview_mode: true and:
/checkout with "Store opening soon — drop your email"When Stripe keys arrive later, user runs {{command_prefix}}bodega:payments
again. We detect preview_mode: true, capture keys, trigger redeploy.
vercel env or a direct Vercel API call.development
Roll back a Bodega-provisioned project. Walks the user through removing the Vercel project, blob store, GitHub repo, Stripe webhook, and (optionally) `.bodega.md` itself. The merchant's Stripe account stays — that's their data.
business
Reports the current state of the store — what's set up, what's pending, what the URLs are, and what to do next.
testing
First-time Bodega setup. Detects whether the folder has an existing project (adapt) or is empty (greenfield), asks about voice and beneficiary, writes .bodega.md, and orchestrates the full flow through hosting, payments, deploy, and admin.
testing
Re-ask the voice and beneficiary questions and update .bodega.md. Useful when the user's preference changes or the store is being handed off to someone new.