Documentation
Tutorials
Donation

Building a Simple Donation System with Bitcoin

In this tutorial, we will demonstrate how to create a basic donation system using Bitcoin. The system allows users to pay to a specific address, and the status of each payment can be tracked in real time.

Prerequisites

Before starting this tutorial, make sure you have the following:

Docker Compose installed. It is used to run the multi-container Docker applications.

Node.js installed for running the server and installing packages.

If you don't have a Next.js application ready, you can create a new one by using the following command:

npx create-next-app@latest

Ensure the experimental server action feature is enabled in your Next.js app. Add the following configuration to next.config.js:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
  },
}
 
module.exports = nextConfig

Server Action is not required for bamotf to work, but it is required for this tutorial.

Setting up the Application

Start up bamotf

Let's start by setting up the Docker services required for bamotf. Create a docker-compose.yml file in the root directory of your project and paste the following:

docker-compose.yml
version: '3.8'
services:
  bamotf:
    image: bamotf/server:latest
    environment:
      - REDIS_URL=redis://redis:6379
      - POSTGRES_URL=postgresql://johndoe:randompassword@postgres:5432
      - DEV_MODE_ENABLED=true
      - DEV_API_KEY=my-key
      - DEV_WEBHOOK_URL=http://host.docker.internal:3000/webhook/bamotf
      - DEV_WEBHOOK_SECRET=my-secret
    ports:
      - 21000:21000
    depends_on:
      - postgres
      - redis
 
  postgres:
    image: postgres:latest
    environment:
      - POSTGRES_USER=johndoe
      - POSTGRES_PASSWORD=randompassword
    volumes:
      - ./docker-data/postgres:/var/lib/postgresql/data
 
  redis:
    image: redis:latest

This configuration will run docker services: bamotf, postgres, and redis. The bamotf service is our main service and it will interact with the other services.

You can start the Docker services by running:

docker-compose up -d

Remember to add Docker data directories to your .gitignore file to avoid pushing them to the repository:

.gitignore
# docker image files
docker-data

Install required packages

We will use three packages from the bamotf library: @bamotf/react, @bamotf/node, and @bamotf/utils. Install these packages using the following command:

npm install --save @bamotf/react @bamotf/node

Configuring bamotf

Create a new file utils/bamotf.ts in your project directory:

utils/bamotf.ts
import {Bamotf} from '@bamotf/node'
 
// TODO: Replace with your API key
const bamotf = new Bamotf('my-key')
 
export {bamotf}

Creating Payment Flow

Replace your app/page.tsx with this code to create simple payment form:

app/page.tsx
import {bamotf} from '@/utils/bamotf'
import {redirect} from 'next/navigation'
 
const donate = async () => {
  'use server'
 
  // Addresses are unique to each payment. You normally want this index to
  // come from your database, but for this example we'll just use 0 for
  // creating one payment intent.
  const index = 0
  // TODO: Replace with your XPUB
  const xpub =
    'tpubD6NzVbkrYhZ4XkJSgrMFnNTX9yThvRVCaBTeXhXMC54PfJ9Y8uwzLNQcT53fCW2ADemefH1ADEX7CeyjnkBNws7NoP7nfjKo93wBNQuRVMw'
 
  const address = await bamotf.address.derive(xpub, index, 'development')
  const pi = await bamotf.paymentIntents.create({
    amount: 1000,
    currency: 'USD',
    address,
  })
 
  return redirect(`/${pi.id}`)
}
 
export default async function Home() {
  return (
    <form action={donate}>
      <button type="submit">Donate $10</button>
    </form>
  )
}

Creating the Payment Status Page Create a new page app/[id]/page.tsx to display the status of a specific payment:

app/[id]/page.tsx
import React from 'react'
import {bamotf} from '@/utils/bamotf'
 
import {PaymentDetails} from './payment-details'
 
import '@bamotf/react/components.css'
 
export default async function DonationStatusPage({
  params,
}: {
  params: {id: string}
}) {
  const pi = await bamotf.paymentIntents.retrieve(params.id)
  const amountInBtc = await bamotf.currency.toBitcoin(pi)
 
  if (pi.status === 'succeeded') {
    return <>Thank you of your donation</>
  }
 
  return <PaymentDetails amount={amountInBtc} address={pi.address} />
}

Since Next.js 13 follows the "use client" patern, we need to create a separate file for the client-side components. Next, create the app/[id]/payment-details.tsx:

'use client'
 
// Make the PaymentDetails component available only to the client
// This is a Next.js workaround for exporting client-side components
// See: https://nextjs.org/docs/getting-started/react-essentials#third-party-packages
export {PaymentDetails} from '@bamotf/react'

With these steps, your payment system is now set up. Users can make payments in Bitcoin and check their status in real time.

Creating a Webhook

To receive payment status updates, we need to create a webhook. Create a new file app/webhook/bamotf/route.ts:

app/webhook/bamotf/route.ts
import {bamotf} from '@/utils/bamotf'
import {NextResponse} from 'next/server'
 
export async function POST(request: Request) {
  // TODO: add your secret here
  const secret = 'my-secret'
 
  const rawBody = await request.text()
  const signatureHeader = request.headers.get('x-webhook-signature') || ''
 
  const {success, parsed} = bamotf.webhooks.constructEvent(
    rawBody,
    signatureHeader,
    secret,
  )
 
  if (!success) {
    return NextResponse.json(
      {success: false, error: 'Invalid signature'},
      {status: 401, statusText: 'Unauthorized'},
    )
  }
 
  const {
    event,
    data: {paymentIntent},
  } = parsed
 
  switch (event) {
    case 'payment_intent.succeeded':
      // here is where you update the database with the payment status
      // and send a confirmation email to the user or push a web socket event to
      // the client to update the UI
      // ...
 
      console.log(`✅ Payment intent succeeded: ${paymentIntent.id}`)
      return NextResponse.json({success: true})
 
    default:
      console.error(`Unknown event: ${event}`)
      return NextResponse.json({success: false, error: 'Unknown event'})
  }
}

Running the Application

To run the application, use the following command:

npm run dev

This will start the Next.js development server and expose it on http://localhost:3000 (opens in a new tab). You can now open the application in your browser and start the payment. You can visualise the payment status in real time by opening the application in a different tab on http://localhost:21000 (opens in a new tab). There's also a simulate function where you can simulate a payment during development.