Skip to content
Back to Prompts

Build a global checkout

One prompt, no questions: your coding agent builds and tests a hosted checkout platform on a BlindPay development instance. Merchants onboard with KYB, create checkout sessions through an API, and get paid in dollars while shoppers pay by Pix, SPEI, ACH, wire, RTP, Argentine bank transfer, or PSE.

Payins
Quotes
Compliance
Payouts

Paste this prompt into a coding agent running in an empty folder, then walk away. It builds a Stripe Checkout-style platform: merchant onboarding, a checkout sessions API with signed webhooks, a hosted payment page for seven local rails, and a demo store that uses it. Every flow is tested against your development instance before the agent says a word. Selling only your own products? Sign up one merchant and use it as a single-merchant checkout.

Prompt

text
Build a working global checkout platform on BlindPay in this folder, unattended, and prove it works.

You are a senior payments engineer. Build first, speak last: no questions, no plan for approval, no "should I continue". Every decision is either settled below or yours to make. When something is unclear, pick the option that keeps the app working, note it for the final report, and keep going. The person who started you will look at the screen only when you finish, so the deliverable is a running app plus proof that every flow works. You address them once, in the final report.

## The product

A hosted checkout for online businesses that sell to Latin America and the US, in the style of Stripe Checkout. A merchant signs up, passes business verification in about a minute, gets a test API key, and creates checkout sessions from their own backend. Each session has a URL. The shopper opens it, picks their country, and pays with the bank rail they already use: Pix in Brazil, SPEI in Mexico, a bank transfer in Argentina, PSE in Colombia, or ACH, wire, or RTP in the US. The shopper sees the exact amount in their own currency. The merchant receives the full USD price as stablecoins in a BlindPay-managed wallet, gets a signed webhook, and can withdraw to a US bank account.

It is a platform: any number of merchants can sign up, and the platform earns a fee on every payment. One demo merchant, a coffee roaster with a small online store, ships inside the app to show the whole loop.

Name: if a name was given after this prompt, use it. Otherwise invent a short, original name that is not an existing payments company, plus a simple wordmark.

BlindPay is the money layer: terms of service, KYB, custody in managed wallets, payin quotes, payins, payouts, and partner fees. It all runs on a BlindPay development instance, where the test stablecoin USDB stands in for USDC, verification approves itself, payins settle by themselves, and no real money moves. Every page shows a "Test mode" badge.

Build only what this prompt lists. Subscriptions, invoices, refunds, disputes, and multi-currency pricing are out of scope for this build, so they get no screens and no placeholders.

## Credentials

Read BLINDPAY_API_KEY and BLINDPAY_INSTANCE_ID from the environment or from a .env file in this folder. Never print, log, or commit the key: check that it is set without echoing it, and keep .env in .gitignore. Call GET https://api.blindpay.com/v1/instances/{instance_id} first. The type must be "development". If it is "production", stop and say so: this build creates businesses and moves money without asking.

If a value is missing, still build everything and run every check that needs no API call. Then ask once, as your final message: create a development instance and an API key at https://app.blindpay.com, add BLINDPAY_API_KEY and BLINDPAY_INSTANCE_ID to .env, and reply. When they do, run the verification below and send the final report.

## Settled decisions

- Stack: Next.js with the App Router, TypeScript in strict mode, Tailwind CSS, npm, Node 20 or newer. One process serves the pages, the dashboard API, the public merchant API, and the demo store. No UI kit; lucide-react icons are fine.
- Folder: build in this folder. If it already holds a project (anything besides .env and dotfiles), build in a new subfolder named after the app and copy .env into it. create-next-app refuses a folder that contains .env, so scaffold in a temporary folder and move the files in, or write the config files by hand.
- Port: 3456, or PORT if set. If the port is busy, take the next free one. Build every URL (checkout URLs, redirect URLs, webhook URLs for the demo store) from the incoming request's origin or one APP_URL computed at startup, so a port change never breaks one.
- Processes: stop only processes you started, by the PID you saved when you started them. Never kill by name, pattern, or port: other apps on this machine may match.
- Storage: one JSON file at .data/db.json behind a small module with an in-process lock and atomic writes (write a temp file, then rename). No database server, no native modules. .data/ is gitignored.
- Dashboard auth: merchants sign up with business name and email and sign in with email, session in a signed httpOnly cookie. Generate SESSION_SECRET once into .env. Call it demo auth in the README.
- Merchant API auth: each merchant gets one test secret key, "sk_test_" plus 32 random characters, shown in full once at creation and on "Roll key". Store only its SHA-256 hash. The public API takes it as Authorization: Bearer sk_test_....
- BlindPay access: plain fetch from one server-only module, with cache: "no-store". The BlindPay key never reaches the browser; the browser only talks to the app's own routes.
- Network base_sepolia and token USDB for every wallet, quote, payin, and payout.
- Pricing: merchants price in USD. Every payin quote uses currency_type "receiver" with request_amount set to the session amount and cover_fees true, so the merchant receives exactly the price and the shopper's local amount includes the fees.
- Platform fee: on first start, create one BlindPay partner fee (1% plus $0.30 on payins, 0 on payouts), store its pf_ id in .data, and pass partner_fee_id on every payin quote. Record partner_fee_amount from each quote as platform revenue.
- Money: integer minor units everywhere in the app, formatted with Intl.NumberFormat in the payer's locale (pt-BR, es-MX, es-AR, es-CO, en-US). Those locales print MXN, ARS, and COP with a bare "$", so prefix the ISO code ("MXN 823", "ARS 72.237") wherever a shopper could read it as dollars. No floating point math on amounts.
- Status from BlindPay: polling, no BlindPay webhooks. BlindPay cannot reach localhost, and a tunnel is one more thing to break. One in-process sync loop (start it from instrumentation.ts, guarded by a globalThis flag so it starts once) runs every 10 seconds while any payin is open: one GET /instances/{instance_id}/payins?limit=50 call covers every merchant, then a GET by id only for open payins missing from that page. Pages poll the app's own API every 3 seconds and never call BlindPay directly.
- Merchant webhooks: the app sends its own webhooks to merchant endpoints, signed with HMAC-SHA256 over "<timestamp>.<raw body>" using a per-endpoint secret "whsec_" plus 32 random characters, in a header Checkout-Signature: t=<unix seconds>,v1=<hex>. Retry a non-2xx response up to 5 times with backoff (10s, 30s, 2m, 10m, 30m). Log every attempt.
- Git: initialize a repository and commit once everything passes. Write a README with setup, a short architecture note, the public API reference, and the demo script.

## What to build

1. Landing: the name, one line on what it does ("Accept local bank payments from Latin America and the US. Get paid in dollars."), the list of rails with country flags, "Start accepting payments", "Sign in".

2. Merchant onboarding: sign up (business name, email), then terms of service, then business verification, then ready.
   - Terms: create a BlindPay terms link, send the browser to it, and receive the tos_id back on /onboarding/terms.
   - Verification: a KYB form prefilled with the sandbox business profile below (business name and email from sign-up, every field editable, one owner block, sample document images as thumbnails). Submitting creates the BlindPay customer, then shows "Verifying" until kyc_status is approved. A rejected status gets its own clear screen.
   - Ready: create the managed wallet and the test API key, show the key once with a copy button and a warning that it will not be shown again, then go to the dashboard.

3. Public merchant API (versioned under /api/v1, JSON in and out, Stripe-like error bodies {"error": {"type", "code", "message"}}):
   - POST /api/v1/checkout/sessions with {"amount": <USD cents, at least 1000>, "currency": "usd", "line_items": [{"name", "quantity", "unit_amount"}] (optional; if present they must sum to amount), "description" (optional), "customer_email" (optional), "success_url", "cancel_url", "metadata" (optional, string values), "expires_in_minutes" (optional, 30 to 1440, default 60)}. Accept an Idempotency-Key header and return the same session for a repeated key. Returns the session object: {"id": "cs_test_<random>", "object": "checkout.session", "url", "status": "open", "payment_status": "unpaid", "amount", "currency", "line_items", "metadata", "success_url", "cancel_url", "expires_at", "created_at", "payment": null}.
   - GET /api/v1/checkout/sessions/{id} returns the session, scoped to the key's merchant. Once the shopper pays, payment holds {"id": "pay_<random>", "rail", "local_currency", "local_amount", "status", "blindpay_payin_id"}.
   - GET /api/v1/checkout/sessions lists the merchant's sessions, newest first, with limit and starting_after.
   - Session status: open, then processing once a payin exists, then complete, failed, or expired. payment_status: unpaid, then paid on complete. A failed or refunded payin sets status failed. An open session past expires_at becomes expired; a processing session never expires, because a payin cannot be canceled.
   - Webhook events: checkout.session.completed, checkout.session.payment_failed, checkout.session.expired. Body: {"id": "evt_<random>", "type", "created", "data": {"object": <session>}}.

4. Hosted checkout at /pay/{session_id}. Desktop: order summary on the left (merchant name and wordmark, line items, total in USD), payment on the right. Mobile: one column, summary collapsed at the top.
   a. Country and rail picker: Brazil (Pix), Mexico (SPEI), Argentina (Bank transfer), Colombia (PSE), United States (ACH, Wire, RTP). Flags, plus a one-line hint per rail ("Pay from any Brazilian bank app", "Takes 1 to 3 business days" for ACH, and so on).
   b. Payer details, only what the rail needs: email and full name always; CPF or CNPJ for Pix (validate the check digits in the browser); CUIT or CUIL for Argentina; for PSE, document type (CC or NIT), document number, phone (+57 then 10 digits), and bank (a short list of Colombian banks with their PSE codes; 1007 is Bancolombia). Prefill email from customer_email.
   c. Price: create the payin quote and show "You pay" in the local currency, large, with "Includes fees and exchange" under it and "Merchant receives US$42.50" in small print. Show a countdown to the quote's expires_at. If it runs out, or the shopper comes back later, quote again on the next click without an error.
   d. Confirm: "Pay R$ 467,04" (formatted per locale). Creates the payin with an Idempotency-Key tied to the session, so a double click or a retry can never create two payins. From here the session is bound to this payin and the rail can no longer change.
   e. Instructions, per rail, each value with a copy button: Pix shows the Pix code as a QR code and copyable text; SPEI shows the CLABE, the exact MXN amount, and the beneficiary; Argentina shows the CBU or CVU and the exact ARS amount; PSE shows a "Continue to your bank" button with the payment link; ACH, wire, and RTP show the routing number, account number, beneficiary, bank, and the memo code with a bold note that the memo is required. On development, sandbox values replace real ones (see the facts below): show a "Test mode: this payment completes by itself" note instead of a QR code, and hide placeholder fields along with any warning about them, such as the memo note when the memo is a placeholder.
   f. Waiting: a live status line ("Waiting for your transfer", then "Payment received") while polling. Pix, SPEI, ACH, wire, and RTP settle within about 2 minutes on development; Argentina and PSE can take up to 3.
   g. Done: a success screen with the amount, merchant, and a reference, then redirect to success_url with ?session_id=<id> appended (use the URL API, so an existing query string survives) after 3 seconds. A failed or refunded payin shows "This payment did not go through" and a "Return to <merchant>" button to cancel_url. An expired session shows "This checkout has expired". "Cancel and return" goes to cancel_url while the session is open.

5. Merchant dashboard:
   - Home: today's and all-time gross volume (sum of completed payments from your store), the wallet balance from BlindPay ("Available"), and a chart of the last 14 days of volume. Volume always comes from completed payments, never from the wallet balance (see the facts below).
   - Payments: every session with amount, rail, local amount, payer, status chip (open, processing, complete, failed, expired), and date. A row opens details: the timeline, the BlindPay quote and payin ids, local amount, fees paid by the shopper, the platform fee, and metadata.
   - Payouts: add a US bank account (ACH) from a form built from the bank-details endpoint, enter a USD amount, show the quote (amount, fee, what lands, countdown), confirm, and follow the payout to completed. Saved accounts are reused.
   - Developers: the API key (masked, with Roll key, which invalidates the old key at once), webhook endpoints (add a URL, see the signing secret once, enable or disable, send a test event), and a delivery log with status code, attempt number, and the request body. A "Create test checkout" button that makes a $42.50 session and opens it.
   - Settings: the business profile as BlindPay returned it, read only.

6. Platform admin at /admin, protected by ADMIN_PASSWORD (generate it once into .env and print where to find it, never the value, in the final report): every merchant with verification status, volume, and payment count, plus platform fee revenue (sum of partner_fee_amount on completed payments) per merchant and in total. The copy says fees are collected by BlindPay during each payin and paid out monthly.

7. Demo store at /store: "<Merchant name> Coffee", three products (Single origin, 1 lb, US$18.00; Espresso blend, 2 lb, US$32.00; Gift box, US$54.00) with quantity pickers and a cart. Checkout is a server route that calls the platform's own public API over HTTP with the demo merchant's secret key (POST /api/v1/checkout/sessions, success_url /store/success, cancel_url /store/cart) and redirects to the session url. /store/success retrieves the session and shows "Order confirmed" once it is complete. A webhook receiver at /store/webhooks verifies Checkout-Signature (reject a bad signature or a timestamp older than 5 minutes) and marks the order paid; the store's orders page shows each order and whether the webhook arrived. On first start, if no demo merchant exists, the store page shows "Run the setup", and the end-to-end test below creates it by going through onboarding in the real UI, then saves its key and registers /store/webhooks as its endpoint.

Design: it must look like a checkout people trust with their money. Clean and quiet, lots of white space, one accent color, Inter or the system font, large tabular numerals for amounts, country flags, skeletons while loading, toasts after actions, inputs with inline validation, and buttons that show progress and never submit twice. The hosted checkout carries the merchant's name, not the platform's, except for a small "Powered by <name>" in the footer.

## BlindPay facts, verified on a development instance

Base URL https://api.blindpay.com/v1 with the header Authorization: Bearer <key>. Reference: https://blindpay.com/docs/llms.txt and the OpenAPI spec at https://api.blindpay.com/doc. These facts were checked against the live API. If a response disagrees, trust the response, adapt, and list the difference in the final report.

- Rate limit: a development instance takes about 40 requests per minute. Past that you get 429 development_instance_rate_limit_exceeded with a Retry-After header. Cache GET responses on the server for 10 seconds, keep the sync loop to the budget above, never fire more than 3 BlindPay calls for one page load, and on a 429 wait Retry-After and retry once while showing cached data.
- Terms: POST /e/instances/{instance_id}/tos with {"idempotency_key": "<new uuid>", "redirect_url": "<origin>/onboarding/terms"} returns {"url"}. The page behind it has one Accept button. After Accept, BlindPay sends the browser to redirect_url + "?tos_id=to_...". It appends "?tos_id=" without looking for an existing query string, so redirect_url must not have one: keep sign-up state in the session, and if the query parse fails, find to_[A-Za-z0-9]+ in the full URL. A tos_id works for one customer only; a second use fails with TOS_ALREADY_ACCEPTED.
- Business customer: POST /instances/{instance_id}/customers with the tos_id and this sandbox business profile (it passes; keep the image URLs as they are): {"type": "business", "kyc_type": "standard", "email": "<from sign-up>", "legal_name": "<from sign-up>", "formation_date": "2019-03-14T00:00:00Z", "tax_id": "123456789", "website": "https://example.com", "business_type": "llc", "business_industry": "454110", "business_description": "Online store", "account_purpose": "receive_payments_for_goods_and_services", "estimated_annual_revenue": "100000_999999", "source_of_wealth": "business_dividends_or_profits", "phone_number": "+13022006100", "address_line_1": "8 The Green", "address_line_2": "#12345", "city": "Dover", "state_province_region": "DE", "country": "US", "postal_code": "19901", "ip_address": "127.0.0.1", "incorporation_doc_file": "https://placehold.co/800x1000.jpg", "proof_of_ownership_doc_file": "https://placehold.co/800x1000.jpg", "proof_of_address_doc_type": "UTILITY_BILL", "proof_of_address_doc_file": "https://placehold.co/800x1000.jpg", "owners": [{"role": "beneficial_controlling", "first_name": "Ana", "last_name": "Ruiz", "title": "CEO", "ownership_percentage": 100, "date_of_birth": "1988-07-02T00:00:00Z", "tax_id": "536804398", "tax_type": "SSN", "address_line_1": "8 The Green", "city": "Dover", "state_province_region": "DE", "country": "US", "postal_code": "19901", "id_doc_country": "US", "id_doc_type": "PASSPORT", "id_doc_front_file": "https://placehold.co/800x1000.jpg", "proof_of_address_doc_type": "UTILITY_BILL", "proof_of_address_doc_file": "https://placehold.co/800x1000.jpg"}]}. business_industry is a NAICS code from a fixed list (454110, electronic shopping, is on it; an unknown code is a 400 that lists the valid ones). A US owner needs tax_type (SSN or ITIN) and their own proof of address. BlindPay downloads every document URL: data: URIs are rejected and dead links fail with FILES_UNREADABLE. The response is only {"id", "customer_id"}; GET /instances/{instance_id}/customers/{id} shows kyc_status, which reached approved within 5 seconds in testing.
- Wallet: POST /instances/{instance_id}/customers/{customer_id}/wallets with {"network": "base_sepolia", "name": "Checkout balance", "external_id": "<your merchant id>"} returns id (bl_...) and address. Store both. GET /instances/{instance_id}/customers/{customer_id}/wallets/{wallet_id}/balance returns one entry per token, like {"USDB": {"amount": 297.5}, "USDC": {...}}. amount is in whole tokens, so cents = Math.round(amount * 100).
- Partner fee: POST /instances/{instance_id}/partner-fees with {"name": "Platform fee", "payin_percentage_fee": 100, "payin_flat_fee": 30, "payout_percentage_fee": 0, "payout_flat_fee": 0} (basis points and cents) returns {"id": "pf_..."}. Create it once and reuse it.
- Payin quote: POST /instances/{instance_id}/payin-quotes with {"wallet_id": "<bl_>", "currency_type": "receiver", "cover_fees": true, "request_amount": <USD cents>, "payment_method": "<rail>", "token": "USDB", "partner_fee_id": "<pf_>", "payer_rules": {...}}. No network field: the wallet decides it. payment_method is pix, spei, transfers, pse, ach, wire, or rtp. The response has id (qu_...), sender_amount (what the shopper pays, in minor units of their currency, fees included), receiver_amount (equals request_amount), flat_fee, partner_fee_amount (in USD cents), and expires_at (epoch milliseconds, 5 minutes out). The quotation fields are scaled by 100, so if you show a rate, compute it from sender_amount / receiver_amount. Tested at US$42.50 with the 1% + $0.30 platform fee: SPEI MXN 823, Argentina ARS 72,237, PSE COP 143,113, ACH US$43.68, RTP US$44.29, wire US$58.44 (wire carries a $15.00 flat fee, so label wire "for larger orders"). US$86.00 by Pix came to R$ 467.04. MXN, ARS, and COP amounts are always whole units. A quote below a rail's minimum fails with an error that names the limit; show that message under the rail and let the shopper pick another.
- payer_rules per rail: Pix needs {"pix_allowed_tax_ids": ["<CPF or CNPJ, digits only>"]} and validates the check digits (52998224725 is a valid test CPF). Argentina needs {"transfers_allowed_tax_id": "20-12345678-6"} (a CUIT or CUIL). PSE needs {"pse_full_name", "pse_document_type": "CC" or "NIT", "pse_document_number", "pse_email", "pse_phone": "+57" plus 10 digits, "pse_bank_code"}; {"pse_full_name": "Camila Rojas", "pse_document_type": "CC", "pse_document_number": "1020304050", "pse_email": "camila@example.com", "pse_phone": "+573001234567", "pse_bank_code": "1007"} passes. SPEI, ACH, wire, and RTP need no payer_rules.
- Payin: within 5 minutes, POST /instances/{instance_id}/payins/evm with {"payin_quote_id": "<qu_>"} (this one endpoint serves every rail) and an Idempotency-Key header. It returns id (pi_...), status "processing", currency, sender_amount, receiver_amount, partner_fee, and the instructions: pix_code for Pix (the literal "<development>" on development); clabe for SPEI; tracking_transaction.transfers_instruction.account and .type ("CBU") for Argentina; tracking_transaction.pse_instruction.payment_link and .expires_at for PSE; memo_code (the literal "<development>" on development) plus blindpay_bank_details {routing_number, account_number, account_type, beneficiary {name, address_line_1, address_line_2}, receiving_bank {...}} for ACH, wire, and RTP. Read only the fields for the rail; the response also carries blindpay_bank_details for rails that do not use it.
- Settlement on development: every payin completes by itself. In testing, SPEI, ACH, wire, and RTP were completed within 30 seconds, PSE within 80, Argentina within 45, and Pix took about 2 minutes. A payin with request_amount 66600 (a $666.00 checkout) ends failed, and 77700 ($777.00) ends refunded, within about a minute.
- Balance quirk: on development, a failed or refunded payin still delivers its USDB to the wallet, and every payin shows up in GET .../wallets/{wallet_id}/deposits. So a payment's status comes only from the payin's status, never from a balance change or a deposit, and volume is computed from completed payins.
- Payin status: GET /instances/{instance_id}/payins/{id}, or the list GET /instances/{instance_id}/payins?limit=50 (add customer_id=<re_> to scope it), returns {"data": [...], "pagination": {...}}. limit must be 10, 50, 100, 200, 500, or 1000 (anything else is a 400). status is processing, on_hold (held for manual review, not final), completed, failed, or refunded. Keep an on_hold payin's session processing and show the shopper "Your payment is being reviewed".
- Merchant bank account: POST /instances/{instance_id}/customers/{customer_id}/bank-accounts with {"type": "ach", "name": "Operating account", "beneficiary_name": "<legal name>", "routing_number": "021000021", "account_number": "123456789", "account_type": "checking", "account_class": "business", "business_industry": "454110", "recipient_relationship": "first_party", "address_line_1", "city", "state_province_region", "country": "US", "postal_code"} returns id (ba_...). A business account without business_industry is a 400. GET /available/bank-details?rail=ach returns a plain array of fields, each with key, label, required, regex, and choices under items as [{label, value}] (the savings account type is "saving"); build the form from it and send business_industry from the merchant's profile.
- Payout: POST /instances/{instance_id}/quotes with {"bank_account_id": "<ba_>", "currency_type": "sender", "cover_fees": false, "request_amount": <USD cents, at least 500>, "network": "base_sepolia", "token": "USDB"} returns sender_amount, receiver_amount, flat_fee, and expires_at. Ignore the contract field: it is for wallets the customer signs for. Then POST /instances/{instance_id}/payouts/evm with {"quote_id": "<qu_>", "sender_wallet_address": "<managed wallet address>"}: a managed wallet needs no signature step. A $100.00 payout went from processing to completed in under 10 seconds and landed $99.50.
- Idempotency: send an Idempotency-Key header with a fresh UUID per user action on every POST that creates a customer, partner fee, quote, payin, or payout. Reuse it only to retry that same action.
- ID prefixes: customer re_, wallet bl_, bank account ba_, quote qu_, payin pi_, payout po_, partner fee pf_, terms to_.

## Verify before you report

1. npm run build passes with no type errors, and lint passes if the scaffold set it up.
2. Vitest unit tests cover the money helpers (minor units per currency, locale formatting), CPF and CUIT validation, the webhook signer and verifier (good signature, bad signature, stale timestamp), the session state machine, and the BlindPay client (error mapping, 429 retry), with fetch mocked.
3. Run the production build (npm start on the chosen port) in the background, logging to .data/server.log.
4. Install @playwright/test with Chromium and write tests/e2e/checkout.spec.ts. It drives the real UI against the development instance, one step at a time, and saves a full-page screenshot of every screen to screenshots/:
   a. Sign up the demo merchant through the real onboarding with a unique email (merchant+<timestamp>@example.com), wait for the BlindPay terms page (https://app.blindpay.com/e/terms-of-service), click the button named exactly "Accept" (the page also has a hidden "Accepted" button), wait until the URL is back on your own host at /onboarding/terms (BlindPay's own URL contains your redirect_url, so a path match alone fires too early), submit verification, wait for approval, and capture the API key. Save it as the demo store's key and register /store/webhooks as its endpoint.
   b. In the store, add an Espresso blend and a Gift box to the cart (US$86.00), check out, pay by Pix with CPF 52998224725, wait for complete, land on /store/success with "Order confirmed", and check that the store's orders page shows the webhook arrived with a valid signature.
   c. With curl against the public API, create one session per remaining rail (SPEI, Argentina, PSE, ACH, wire, RTP) at US$42.50, pay each in the hosted checkout with the payer details above, and wait for complete. Run them one after another to stay under the rate limit.
   d. Create a US$666.00 session and pay by ACH: it ends failed, the checkout shows "This payment did not go through", and checkout.session.payment_failed is delivered. Create a US$777.00 session and pay by ACH: the payin ends refunded, and the session ends failed with the same screen and event.
   e. Create a session with expires_in_minutes 30, then move its expires_at into the past in .data/db.json through a test-only helper, and check that the page shows "This checkout has expired" and checkout.session.expired is delivered.
   f. Call the API with a wrong key (401), a wrong amount sum (400), and a repeated Idempotency-Key (same session id back).
   g. In the dashboard, check the payments list and volume (the seven completed payments, US$341.00), add the test bank account, withdraw US$100.00, and wait for the payout to complete. On Developers, click "Send test event" and check it lands in the delivery log with a 200.
   h. Sign in to /admin and check the platform fee total matches the sum of partner_fee_amount for the completed payments.
   Settlement waits get up to 4 minutes each. Let the app's polling do the work and keep the pace under the rate limit. If you run the spec in the background, wait for it to finish: a status line while you wait is fine, a question is not.
5. Fix what fails and rerun until the whole spec passes. If one step still fails after three real attempts, leave the rest working and report that step as broken, with the error.
6. Leave the app running on its port and open the demo store in the default browser (open on macOS, xdg-open on Linux, start on Windows).

## Final report

This is your only message. Plain text, short, in this order:
1. The store URL, the dashboard URL, and the email to sign in with (the demo merchant from the test run, with payments in it).
2. A 60-second demo script: buy coffee in the store, pay by Pix, watch the checkout complete and the store confirm the order, then open the dashboard and the webhook log.
3. What was verified, with the BlindPay ids created, the payment per rail with its local amount, and the platform fee total.
4. Where the screenshots are, and where ADMIN_PASSWORD lives.
5. How to start it again, and a curl example that creates a checkout session.
6. What changes for production: a production instance and key, USDC or USDT on a mainnet network, real KYB with document upload (POST /v1/upload) and real owners, real authentication, BlindPay webhooks (payin.new, payin.update, payin.complete) with signature verification instead of polling, live API keys, and removing the test-mode values.
7. Every place the live API disagreed with this prompt.

How to use

  1. Sign up at app.blindpay.com, create a development instance, and create an API key in its API Keys tab.
  2. In an empty folder, create a .env file with BLINDPAY_API_KEY=... and BLINDPAY_INSTANCE_ID=in_....
  3. Start your agent with permission prompts off, or it will stop to ask before every command. In Claude Code: claude --dangerously-skip-permissions.
  4. Paste the prompt. To pick the name, add one line after it, like "Call it Quetzal".
  5. Come back in about 30 minutes. The demo store opens in your browser, with a verified merchant, a paid order on every rail, and a demo script.

No .env? The agent still builds everything and asks for the two values once, at the end.

Adding a checkout to an app you already run? Use Integrate a global checkout instead.

One API.
Multiple payment rails.

Blind Pay, Inc. (“BlindPay”) is a United States-based financial technology and payments infrastructure company, incorporated in the State of Delaware on May 2, 2024. BlindPay is duly registered as a Money Services Business (“MSB”) with the Financial Crimes Enforcement Network (“FinCEN”) under registration number 31000277566153. BlindPay is not a state-licensed money transmitter and does not, in its own capacity, engage in regulated money transmission services. All such regulated activities are conducted exclusively through, and under the licenses of, duly authorized financial institution partners. BlindPay's role is limited to providing financial technology, virtual account management, payment processing, and related payments infrastructure services that facilitate payment flows and account functionalities. These services are provided in coordination with its licensed partners and in compliance with applicable federal MSB requirements and relevant state regulatory frameworks.

Blind Pay, LLC (“BlindPay”) is a Delaware limited liability company formed on June 24, 2025. BlindPay is duly registered as a Money Services Business (“MSB”) with the Financial Crimes Enforcement Network (“FinCEN”) under registration number 31000318334247 and is listed in the Nationwide Multistate Licensing System (“NMLS”) under NMLS ID #2745309.

BLIND PAY SOCIEDADE PRESTADORA DE SERVIÇOS DE ATIVOS VIRTUAIS LTDA. (“BlindPay”) is a Virtual Asset Service Provider (“VASP”) operating in Brazil. The company is presently completing the regulatory adaptation process required of Virtual Asset Service Provider Companies (SPSAV) under Central Bank of Brazil Resolution No. 520/2025, and remains subject to the transitional regime set forth in Article 88 of that Resolution. Registered address: Avenida Princesa Isabel, nº 15, Sala 1405, Edifício Martinho de Freitas, Centro, Vitória/ES, 29010-361.

BlindPay is a stablecoin API that enables individuals and businesses to move money worldwide.