What you'll build

This is Part 1 of a series where you build a live USSD service called GH Utilities — a fictional utility company that lets customers check their account balance, buy airtime, and pay bills by dialling a shortcode on any phone, including basic feature phones with no internet.

By the end of the series you'll have:

By the end of Part 1 you'll have a running Express server that responds to USSD requests and serves a welcome menu in the Africa's Talking sandbox.

Prerequisites

Create the project

Create a new folder and initialise the project:

mkdir gh-utilities-ussd && cd gh-utilities-ussd
npm init -y
npm install express dotenv
npm install -D typescript ts-node @types/node @types/express nodemon

Create a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true
  }
}

Open the project in VS Code:

code .

Tell BlocWeave about the project

Create BLOCWEAVE_PLAN.md in the project root:

# GH Utilities USSD Service

## Stack
- Node.js 20 + Express + TypeScript
- Africa's Talking USSD and SMS APIs
- Hosted on Railway

## Conventions
- All source files in src/
- Entry point: src/index.ts
- USSD handler logic in src/ussd/handler.ts
- Session state tracked in memory (Map<sessionId, SessionState>)
- Env vars loaded with dotenv from .env
- Africa's Talking credentials: AT_API_KEY, AT_USERNAME, AT_SHORTCODE in .env

Set up Africa's Talking sandbox

  1. Log in to your Africa's Talking dashboard at account.africastalking.com
  2. Go to Sandbox in the left menu
  3. In the sandbox dashboard, go to USSD and click Create Channel
  4. Enter any shortcode (e.g. *384*12345#) — this is your test dial code
  5. Set Callback URL to a placeholder for now (http://localhost:3000/ussd) — we'll update it after ngrok is running
  6. Click Save

Go to Settings → API Keys and copy your API Key. The username for sandbox is always sandbox.

Create a .env file:

AT_API_KEY=your_api_key_here
AT_USERNAME=sandbox
AT_SHORTCODE=*384*12345#
PORT=3000

Add .env to .gitignore:

echo ".env" >> .gitignore

Build the USSD server

BlocWeave prompt

Create an Express USSD server for the GH Utilities project. 1. src/index.ts — Express app on PORT from env. Register POST /ussd route. Register GET /health that returns { status: "ok" }. Start the server. 2. src/ussd/handler.ts — export a ussdHandler(req, res) Express handler. Parse Africa's Talking USSD POST body fields: sessionId, serviceCode, phoneNumber, text. If text is empty (first request), respond with a CON welcome menu: "Welcome to GH Utilities\n1. Check balance\n2. Buy airtime\n3. Pay utility bill\n0. Exit" If text is "0", respond with END "Thank you for using GH Utilities. Goodbye." For all other inputs, respond with CON "Invalid option. Press 0 to go back." All responses must be plain text — CON to continue the session, END to close it.

CON vs END: Africa's Talking uses CON (with a space) to continue the session and prompt more input. Use END to close the session and dismiss the USSD dialog on the user's phone. Never add extra headers — AT reads the raw text body.

Start ngrok

Open a second terminal and start ngrok:

ngrok http 3000

Copy the https:// forwarding URL (e.g. https://abc123.ngrok-free.app).

Go back to the Africa's Talking sandbox USSD channel settings and update the Callback URL to:

https://abc123.ngrok-free.app/ussd

Test in the AT simulator

  1. In your AT sandbox dashboard, go to USSD → Simulate
  2. Enter your test phone number (e.g. +233200000001) and your shortcode
  3. Click Initiate

You should see the welcome menu appear. Type 1, 2, 3, or 0 and click Send to test each option.

Start the dev server in VS Code's terminal:

npx ts-node src/index.ts

Watch the terminal — each request from the simulator appears as a POST to /ussd with the session fields logged.

Project structure so far

gh-utilities-ussd/
  src/
    index.ts         ← Express server entry point
    ussd/
      handler.ts     ← USSD route handler
  .env               ← AT credentials (not committed)
  BLOCWEAVE_PLAN.md
  package.json
  tsconfig.json

What's next

In Part 2 we build the full session state machine — level-by-level menus for balance check, airtime purchase, and utility payment. Each option will branch into its own sub-menu and remember where the user is in the flow using the sessionId from AT.