What you'll build

In the final part of this series you'll add SMS confirmation messages so users receive a receipt on their phone after every successful transaction. Then you'll deploy the service to Railway with environment variables set correctly, and switch to production Africa's Talking credentials.

By the end of this part:

Build the SMS service

BlocWeave prompt

Create src/services/sms.ts for the GH Utilities USSD service. Reuse the Africa's Talking client initialised in airtime.ts (export it from airtime.ts and import here, or initialise separately using the same env vars). Export sendSms(to: string, message: string): Promise. Call AT.SMS.send({ to: [to], message, from: undefined }). Log success and any errors. Do not throw — SMS is best-effort; USSD flow should not fail if SMS fails. Export these helper functions: - airtimeSmsReceipt(phone: string, amount: string): Promise — sends "GH Utilities: GHS [amount] airtime credited to your line. Thank you for using our service." - paymentSmsReceipt(phone: string, meterNo: string, amount: string, reference: string): Promise — sends "GH Utilities: Payment of GHS [amount] for meter [meterNo] received. Ref: [reference]. Your balance will update within 1 hour."

Wire SMS into the USSD handler

BlocWeave prompt

Update src/ussd/handler.ts to call the SMS service after completed transactions. In the airtime confirm branch (where sendAirtime succeeds), after returning the END response, call airtimeSmsReceipt(phoneNumber, steps[1]) — fire and forget (do not await in the response path; use .catch(console.error) to avoid unhandled rejections). In the payment confirm branch (where recordPayment resolves), call paymentSmsReceipt(phoneNumber, meterNo, amount, reference) in the same fire-and-forget pattern. Important: the USSD response must be sent back to AT before the SMS call completes. Make sure res.send() is called before the async SMS functions.

Handle session cleanup

Sessions can be abandoned — users dial and then close the app without pressing END. Add a cleanup timer:

BlocWeave prompt

In src/ussd/handler.ts, after storing data in the session Map, also store a cleanup timer: setTimeout(() => sessionMap.delete(sessionId), 5 * 60 * 1000). If a new request arrives for an existing sessionId, clear the old timer and set a new one. This prevents unbounded memory growth from abandoned sessions.

Prepare for Railway deployment

Create a Procfile:

web: node dist/index.js

Add a build script to package.json:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "nodemon --exec ts-node src/index.ts"
  }
}

Make sure the server binds to process.env.PORT — Railway assigns the port dynamically. Verify your src/index.ts uses process.env.PORT ?? 3000.

Deploy to Railway

  1. Push your project to GitHub (create a repo at github.com/new)
  2. Go to railway.app and sign in with GitHub
  3. Click New Project → Deploy from GitHub repo and select your repo
  4. Railway will detect Node.js and run npm run build then npm start
  5. Once deployed, go to Settings → Domains and click Generate Domain to get a public URL

Add your environment variables in Railway's Variables tab — copy everything from your .env file except PORT (Railway sets that automatically):

AT_API_KEY=...
AT_USERNAME=sandbox
AT_SHORTCODE=*384*12345#
SUPABASE_URL=...
SUPABASE_SERVICE_KEY=...

Update the Africa's Talking callback URL

Go to the AT sandbox USSD channel settings and update the Callback URL to your Railway domain:

https://your-app.railway.app/ussd

Test the full flow in the AT simulator one more time using the Railway URL — no ngrok needed.

Switch to production (when you're ready)

When you're ready to go live on a real shortcode:

  1. In the AT dashboard, go to Production and apply for a shortcode
  2. Update AT_USERNAME in Railway variables from sandbox to your AT username
  3. Update AT_API_KEY to your production API key
  4. Update AT_SHORTCODE to your registered production shortcode

Africa's Talking production shortcodes are country-specific and require regulatory approval. For Ghana, contact the Ghana Communications Authority (GCA) through AT's partnership team. The sandbox is fully functional for testing — there is no rush to go live until you're ready.

What you've built

Over this four-part series you've built a production-ready USSD service that:

The complete source is on GitHub. You can extend it with more service types, a web admin dashboard, or automated bill reminders via scheduled SMS.