● tutorial 04 · ages 15–18

Receive real money without Shopify.

This is the actual code running Side Quest's shop page. No monthly store fee, no template fighting your design. Just a button, a server function, and Stripe doing the one part you should never build yourself.

The last two tutorials taught you to design a logo and build a brand. This one covers the part most guides skip because it sounds scary. How does a click on a website turn into money in a real bank account, without you ever touching a credit card number?

💡 What you need: the site from Tutorial 03, a free Stripe account, and everything else you already have. A text editor and a browser.

The Side Quest badge
Every capsule of this comes from a shop page built exactly this way.
1

Two Rules First

before any code

Before a single line of code, two rules that never bend, no matter which platform you use. Shopify, a custom site, anything.

⚠️ Rule 1. You never touch a real card number. Not as text in a form, not saved in a database, not logged anywhere. Storing card data yourself is a legal requirement called PCI compliance, and the realistic way to meet it as one person is simple. Don't. Let a payment processor do it.

⚠️ Rule 2. Prices live on your server, never in the page the browser sees. If your buy button sends a price to the server, anyone can open their browser's dev tools and change $14.00 to $0.01 before clicking. The fix is that the button sends which item, and the server looks up what it costs. We'll build exactly that in Lesson 5.

Everything below exists to satisfy those two rules with the least amount of code possible.

2

The Buy Button

what the customer actually clicks

Start with the part a person sees. It's a plain button. Nothing about payment happens here. It says which item, and waits.

shop/index.html
<button class="buy" data-item="pin">
  Preorder — $14
</button>

data-item="pin" is the entire message this button sends. Not a price, not a name, just an id. That id is a key we'll look up on the server in Lesson 5. Keeping the button this plain is what makes Rule 2 possible.

Try it: add a second button with data-item="stickers". You don't need to touch anything else yet. It won't do anything until Lesson 3, and that's expected.

3

A Server You Don't See

Cloudflare Pages Functions

Everything so far has been a file sitting on a server, sent to whoever asks for it. That's what a "static site" means, and it's all you needed for Tutorial 03. But receiving money needs something that can think. Check a price, talk to Stripe, decide what to send back. That needs actual code running on a server, not just a file.

If you're hosting on Cloudflare Pages, you already have this. It's called a Function, and the rule is almost too simple. Any file you put in a folder named functions/ becomes a tiny bit of server, automatically, for free.

folder structure
site/
  index.html
  shop/
    index.html
  functions/
    api/
      checkout.js   ← becomes /api/checkout

That's the whole setup step. No server to rent, no separate service to sign up for. It deploys alongside the rest of your site because it is part of your site.

💡 A file's path becomes its web address. functions/api/checkout.js is reachable at yoursite.com/api/checkout. No routing config to write.

4

Talking to Stripe

the function itself

Here's the actual flow, before the code. The button gets clicked. Your page asks your own function for a pin. Your function asks Stripe to build this person a checkout page. Stripe hands back a URL. Your page sends the browser there. The customer types their card number on a page that Stripe built and Stripe owns. It never touches yours.

1
Click
Preorder
2
Your site
asks Stripe
3
Stripe builds
a checkout page
4
Customer
pays there
functions/api/checkout.js — the shape of it
// runs when the browser calls /api/checkout
export async function onRequestPost(context) {
  const { request, env } = context;
  const body = await request.json();

  // ask Stripe to create a checkout page
  const res = await fetch(
    "https://api.stripe.com/v1/checkout/sessions",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.STRIPE_SECRET_KEY}`
      },
      body: /* what to sell — Lesson 5 */
    }
  );

  const session = await res.json();
  return new Response(
    JSON.stringify({ url: session.url })
  );
}

env.STRIPE_SECRET_KEY is your Stripe password, effectively. It never appears in this file as actual text. You set it separately in the Cloudflare dashboard, so it's never something you'd accidentally publish. Cloudflare hands it to your function only while it's running.

⚠️ There are two kinds of Stripe key. One starts with pk_ and is public, fine to expose, meant for the browser. One starts with sk_ and is secret. That's the one above, and it must never appear in anything sent to a browser. Functions are exactly where a secret key belongs, because this code only ever runs on the server, never on a visitor's device.

5

Prices Live on the Server

closing the loophole from Lesson 1

Now the part that actually enforces Rule 2. Before your function talks to Stripe, it looks up what the clicked item costs, from a list it owns, not from anything the browser sent.

functions/api/checkout.js — the price lookup
// the only place prices exist. not in the HTML, not in the browser.
const CATALOG = {
  pin: { name: "Enamel Pin", amount: 1400 },      // cents
  stickers: { name: "Sticker Sheet", amount: 1300 },
  tee: { name: "Tee", amount: 3000 }
};

// body.item is just the id the button sent — "pin", "stickers", "tee"
const item = CATALOG[body.item];

if (!item) {
  return new Response("Unknown item", { status: 400 });
}
// item.amount is what actually gets charged — never anything from the browser

Walk through the attack this defeats. Someone opens dev tools, finds your fetch call, and tries resending it with {"item":"pin","price":1}. Doesn't matter. Your function never reads a price from that request. It only reads item, looks up CATALOG[item], and that's the number Stripe actually charges. The tampered field is simply ignored.

💰 This one pattern, never trust a number the browser sends you and always look it up yourself, is the single most important idea in this whole tutorial. It isn't special to payments. It's how you'd protect a game's high scores, a raffle entry count, anything where someone could gain by lying to your server.

6

Test Before Real

Stripe's test mode

Stripe gives every account two parallel modes. Test and live. Test mode works identically to the real thing, same code and same checkout page, except no real money ever moves. You'll know which mode you're in because test keys start with sk_test_ and live ones start with sk_live_.

Build and break things in test mode using Stripe's official fake card:

test card — works only in test mode
Card number:  4242 4242 4242 4242
Expiry:       any future date
CVC:          any 3 digits
ZIP:          any 5 digits

Click your button, land on Stripe's real checkout page, pay with that card, and land back on your site. Full round trip, zero risk. Do this until it works without surprises before you ever switch the key to sk_live_.

Try it: in the Stripe dashboard, toggle to test mode and look at the payment you just made. It shows up exactly like a real one would. Same dashboard, same data, just clearly labeled as a test.

7

Closing the Window

the preorder math, for real

None of this exists only to receive money. It exists to run a preorder correctly. Collect orders, and only actually produce something if enough people wanted it.

The good news is that you don't need to build a counting system. Stripe's dashboard already lists every payment, filterable by what was bought. When your window closes:

  • Open Stripe → Payments, filter by product
  • Count the orders for each item
  • Below your supplier's minimum? Go to the Refunds tab and refund those, one click each
  • At or above? Place the bulk order, funded by money already sitting in your Stripe balance

💰 This is the part that makes a preorder safe to run as one person. You're never spending your own money to guess at demand. The customer's payment arrives first. Production only happens after you already know, for certain, how many to make.

💡 Want to see this exact system live? The Side Quest shop page runs on precisely the code in this tutorial. View its page source, or ask for the repository, and you're looking at a real working example, not a simplified stand-in.

Your Turn

practice challenges
Level 1

Add a Fourth Item

Add one more entry to CATALOG and a matching button. That's the entire process for adding a product.

Level 2

Break Your Own Security

Open dev tools, find the fetch call, try resending it with a fake price. Confirm it gets ignored. Understanding the attack is how you trust the defense.

Level 3

Add a Quantity Limit

Change the function so no single order can request more than 5 of an item. Where in checkout.js would that check belong, before or after the price lookup?

?

Glossary

words you'll see everywhere
PCI compliance — the legal requirement around handling card data. The realistic answer for one person is to not handle it at all, and let Stripe do it.
Function / serverless function — a small piece of code that runs on a server only when called, instead of a program running all the time.
API — a defined way for two programs to talk. Calling Stripe's API means sending Stripe a request in the shape it expects.
Secret key — a password for your code to use, never shown to a browser or committed to a public file.
Environment variable — a secret stored in your hosting platform's settings instead of in a file, so it's never accidentally published.
Checkout session — a one time, temporary payment page Stripe generates on request and hosts itself.
Test mode — a parallel, safe version of your Stripe account for practicing without moving real money.
Fulfillment — actually producing and shipping what was ordered, after the money and the count are both settled.