---
title: "Build a neobank"
seoTitle: "Build a neobank on BlindPay with one AI agent prompt"
description: "One prompt, no questions: your coding agent builds and tests a working neobank on a BlindPay development instance, with KYC, US account details, a Mexican CLABE, Pix deposits, payouts, and instant transfers between users."
date: "2026-10-29"
category: "integrations"
products: ["payins", "payouts", "virtual-accounts", "compliance", "quotes"]
standalone: true
---

Paste this prompt into a coding agent running in an empty folder, then walk away. It builds the app, tests every money flow against your development instance, and talks to you once, at the end, with the app open in your browser and a demo script.

## Prompt

```text
Build a working neobank on BlindPay in this folder, unattended, and prove it works.

You are a senior fintech 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 dollar account for people who live in the US and move money to and from Latin America. A new user opens an account in about a minute, gets a USD balance with US bank details and a Mexican CLABE, adds money, pays family in Brazil over Pix or in Mexico over SPEI, pays a friend in the app instantly, and watches every movement settle.

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

BlindPay is the money layer: terms of service, KYC, custody in BlindPay-managed wallets, virtual accounts, payins, payouts, and wallet-to-wallet transfers. It all runs on a BlindPay development instance, where the test stablecoin USDB stands in for USDC, KYC approves itself, and no real money moves. The app shows a "Sandbox" badge so nobody mistakes it for real money.

Build only what this prompt lists. Cards, savings, yield, investing, crypto trading, and loans 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 customers 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 and the API routes. 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 redirect URL from the incoming request's origin, 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.
- Auth: sign up with name and email, sign in with email, session in a signed httpOnly cookie. Generate SESSION_SECRET once into .env. Call it demo auth in the README.
- BlindPay access: plain fetch from one server-only module, with cache: "no-store" (you keep your own short cache, below). The key never reaches the browser; the browser only talks to the app's API routes.
- Network base_sepolia and token USDB for every wallet, quote, payin, payout, and transfer.
- Money: integer cents everywhere in the app, formatted with Intl.NumberFormat. No floating point math on amounts.
- Updates: polling, no webhooks. BlindPay cannot reach localhost, and a tunnel is one more thing to break.
- Git: initialize a repository and commit once everything passes. Write a README with setup, a short architecture note, and the demo script.

## What to build

1. Landing: the name, one line on what it does, "Open an account", "Sign in".
2. Onboarding: sign up (name, email), then terms of service, then identity, then account ready.
   - Terms: create a BlindPay terms link, send the browser to it, and receive the tos_id back on /onboarding/terms.
   - Identity: a KYC form prefilled with the sandbox profile below (name and email from sign-up, every field editable, 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.
   - Account ready: create the managed wallet, the US virtual account, and the Mexican CLABE, then show them with a short celebration.
3. Home: the USD balance, large. An account details card with the US routing and account numbers (ACH and wire) and the CLABE, each with a copy button. Buttons for Add money and Send. Recent activity.
4. Add money:
   - From Mexico: the CLABE, plus a sandbox button "Simulate a SPEI transfer" with an MXN amount (default 5,000.00) and a sender name. The money lands in seconds at the live rate. This is the best moment of the demo, so make it feel good: after the click, watch for the new payin and animate the balance when it lands.
   - By Pix: a BRL amount, a quote showing the USD that will land, then the payin with its Pix code and a copy button. On development it completes by itself in about 30 seconds.
   - From a US bank: the routing and account numbers, shown as deposit instructions with a copy button and no simulate button.
5. Send to a bank account: choose Brazil (Pix), Mexico (SPEI), or United States (ACH); fill a form generated from the bank-details endpoint; save the recipient; enter a USD amount; show the quote (what the recipient gets in their currency, fee, rate, a countdown to expiry); confirm; follow the payout to completed. Saved recipients can be reused.
6. Send to a friend: pick another user of the app by email, enter a USD amount, confirm. It settles in seconds and both balances change.
7. Activity: every payin, payout, and transfer with direction, counterparty, amount in USD and local currency, rail, and a status chip (processing, completed, failed, refunded). Tapping a row opens details with the BlindPay IDs and timestamps.
8. Failure paths: on development a $777.00 payout is refunded and a $666.00 payout fails. Refunded copy says the money is back in the balance. Failed copy says the payout did not go through and never claims the money came back, because on development it does not.

Design: it must look like a consumer fintech app you would show investors. Mobile-first single column, about 440px wide and centered on desktop, generous spacing, one accent color, Inter or the system font, large tabular numerals for money, country flags, skeletons while loading, toasts after actions, and buttons that show progress and never submit twice.

## 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 per user on the server for 10 seconds, poll only while something is pending (every 5 seconds against your own API, which serves from that cache), never fire more than 4 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.
- Customer: POST /instances/{instance_id}/customers with the tos_id and this sandbox profile (it passes; keep the image URLs as they are): {"type": "individual", "kyc_type": "standard", "first_name": "<from sign-up>", "last_name": "<from sign-up>", "email": "<from sign-up>", "date_of_birth": "1994-05-12T00:00:00Z", "tax_id": "12345678", "phone_number": "+13022006100", "address_line_1": "8 The Green", "address_line_2": "#12345", "city": "Dover", "state_province_region": "DE", "country": "US", "postal_code": "02050", "ip_address": "127.0.0.1", "id_doc_country": "US", "id_doc_type": "PASSPORT", "id_doc_front_file": "https://placehold.co/800x1000.jpg", "selfie_file": "https://placehold.co/800x1000.jpg", "proof_of_address_doc_type": "UTILITY_BILL", "proof_of_address_doc_file": "https://placehold.co/800x1000.jpg"}. 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. A first name of "Fail" forces a rejection.
- Wallet: POST /instances/{instance_id}/customers/{customer_id}/wallets with {"network": "base_sepolia", "name": "Main account", "external_id": "<your user 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": 106.64}, "USDC": {...}}. amount is in whole tokens, so cents = Math.round(amount * 100). This endpoint is the balance; never compute it from transactions.
- US account: POST /instances/{instance_id}/customers/{customer_id}/virtual-accounts with {"banking_partner": "jpmorgan", "token": "USDB", "blockchain_wallet_id": "<the bl_ wallet id>"}. The field is named blockchain_wallet_id but takes the managed wallet id. On development it comes back approved at once; us.ach and us.wire hold routing_number (always 110000000 on development) and account_number. Bank names and addresses come back as the literal "<development>" there, so hide any field with that value.
- Mexican CLABE: the same call with "banking_partner": "spei" returns mx.spei.clabe and mx.beneficiary.name. A customer gets one account per banking partner, so create each once, store it, and never recreate it on a page load.
- Simulated SPEI deposit (development only): POST /instances/{instance_id}/customers/{customer_id}/virtual-accounts/{virtual_account_id}/simulate-spei-deposit with {"amount": <MXN cents>, "sender_name": "Juan Perez"} returns {"success": true}. Within about 10 seconds a completed payin with payment_method "spei" shows up in GET /instances/{instance_id}/payins?customer_id=<id>&limit=10 and the balance rises. The sender's name is in the payin's tracking_transaction.sender_name. MXN 2,000.00 delivered 106.64 USDB in testing.
- Payin by Pix: POST /instances/{instance_id}/payin-quotes with {"wallet_id": "<bl_>", "currency_type": "sender", "cover_fees": true, "request_amount": <BRL cents, at least 1000>, "payment_method": "pix", "token": "USDB"}. No network field: the wallet decides it. With cover_fees true the fee is added to what the payer sends, so show the quote's sender_amount as the amount to pay and sender_amount minus request_amount as the fee (R$500.00 became R$501.00 in testing, while flat_fee said R$0.10). Within 5 minutes, POST /instances/{instance_id}/payins/evm with {"payin_quote_id": "<id>"} (this endpoint serves every rail). The payin has pix_code, which is the literal string "<development>" on development, so show a sandbox note instead of a QR code. It completes by itself about 30 seconds later.
- Recipients: POST /instances/{instance_id}/customers/{customer_id}/bank-accounts. The rails are pix, spei_bitso (Mexico: the payin payment_method is spei, but the bank account type is spei_bitso), and ach. GET /available/bank-details?rail=<rail> returns each field's key, label, required, regex, and choices under items (each with label, value, and sometimes is_active); build the forms from it. Pix needs only {"type": "pix", "name", "pix_key"}; bank-details lists only pix_key, so name is your own field. SPEI needs spei_protocol "clabe", spei_clabe (18 digits with a check digit; 012180001234567899 is valid for tests), and beneficiary_name. ACH needs beneficiary_name, routing_number (021000021 works for tests), account_number, account_type (checking or saving), account_class (individual), recipient_relationship (for example family), address_line_1, city, state_province_region, country, and postal_code.
- 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 (cents of the recipient's currency), flat_fee, and expires_at (epoch milliseconds, 5 minutes out). Ignore the contract field: it is for wallets the customer signs for. The quotation fields are scaled by 100 (an ACH quote returns 100 for a 1.00 rate), so show the rate as receiver_amount / sender_amount instead. Then POST /instances/{instance_id}/payouts/evm with {"quote_id": "<qu_>", "sender_wallet_address": "<managed wallet address>"}: a managed wallet needs no signature or approval step. It goes from processing to completed within seconds on development. A request_amount of 77700 ends refunded about 15 seconds later, with the USDB back in the wallet (it also appears as a wallet deposit). A request_amount of 66600 ends failed, and the USDB that left the wallet stays out on development.
- Friend transfer: POST /instances/{instance_id}/transfer-quotes with {"wallet_id": "<sender bl_>", "amount_reference": "sender", "request_amount": <USD cents>, "sender_token": "USDB", "customer_wallet_address": "<recipient wallet address>", "customer_token": "USDB", "customer_network": "base_sepolia", "cover_fees": true}, then right away POST /instances/{instance_id}/transfers with {"transfer_quote_id": "<id>"}. The quote id starts with qu_ (some docs say pq_). It completed in about 5 seconds in testing. Test transfers with ordinary amounts: 66600 and 77700 are for payins and payouts.
- Lists: GET /instances/{instance_id}/payins, /payouts, and /transfers filter by customer_id and return {"data": [...], "pagination": {...}} when you pass limit, which must be 10, 50, 100, 200, 500, or 1000 (anything else is a 400). A customer's wallets, virtual accounts, and bank accounts come back as plain arrays.
- Activity feed: GET .../wallets/{wallet_id}/deposits lists every inbound movement, payin settlements included, so adding it to payins double counts. A friend transfer shows up for the receiver only there. Record both sides of every friend transfer in your own store when you create it, refresh its status with GET /instances/{instance_id}/transfers/{id}, and build the feed from payins, payouts, and those records.
- Idempotency: send an Idempotency-Key header with a fresh UUID per user action on every POST that creates a customer, quote, payin, payout, or transfer. Reuse it only to retry that same action.
- ID prefixes: customer re_, wallet bl_, virtual account va_, bank account ba_, quote qu_, payin pi_, payout po_, transfer tr_, 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 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/demo.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 Maria Santos with a unique email (maria+<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 identity, and wait for the account to be ready.
   b. Simulate a SPEI transfer of MXN 20,000.00 and wait for the balance to rise. That funds the failure tests in step f.
   c. Deposit R$ 500.00 by Pix and wait for completed.
   d. Add a Brazil Pix recipient and send $50.00; wait for completed. Add a Mexico SPEI recipient and send $25.00; wait for completed.
   e. Sign up Lucas Almeida the same way. As Maria, send Lucas $20.00 and check both balances.
   f. As Maria, send $777.00 to the Pix recipient and check that it shows as refunded and the balance comes back. Then send $666.00 and check that it shows as failed.
   Settlement waits get up to 2 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 it 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 URL, and the email to sign in with (Maria from the test run, already funded and with history).
2. A 60-second demo script: what to click and what appears.
3. What was verified, with the BlindPay IDs created and the final balances.
4. Where the screenshots are.
5. How to start it again.
6. What changes for production: a production instance and key, USDC or USDT on a mainnet network, real document upload (POST /v1/upload), real authentication, webhooks with signature verification instead of polling, and removing the sandbox buttons.
7. Every place the live API disagreed with this prompt.
```

## How to use

1. Sign up at [app.blindpay.com](https://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 Pampa".
5. Come back in about 30 minutes. The app opens in your browser with a funded test account and a demo script.

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

## Related docs

- [Sandbox vs. production](https://blindpay.com/docs/learn/sandbox-vs-production)
- [Managed wallets](https://blindpay.com/docs/wallets)
- [Virtual accounts](https://blindpay.com/docs/virtual-accounts)
- [Payin quotes](https://blindpay.com/docs/payin-quotes)
- [Payout quotes](https://blindpay.com/docs/payout-quotes)
- [Transfers](https://blindpay.com/docs/transfers)
