npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@goonerlabs/ussd-router

v0.1.1

Published

A tiny, dependency-free state-machine router for Africa's Talking USSD apps.

Downloads

254

Readme

@goonerlabs/ussd-router

A tiny, dependency-free state-machine router for Africa's Talking USSD apps.

USSD is stateless: Africa's Talking POSTs { sessionId, serviceCode, phoneNumber, text } to your endpoint on every step, where text is the full *-joined trail of everything the user has entered so far (e.g. "1*2*500"). You reply with a plain-text body that starts with CON (keep the session open) or END (hang up).

This router lets you declare your app as a set of screens and replays that trail through them on each request — so navigation is deterministic and you don't need a session store.

npm install @goonerlabs/ussd-router

Quick start (Express)

const express = require('express');
const { createRouter, expressHandler } = require('@goonerlabs/ussd-router');

const router = createRouter({ start: 'main' });

router.screen('main', (ctx) =>
  ctx.menu('Welcome to MyApp', [
    { label: 'Check balance', next: 'balance' },
    { label: 'Buy airtime',   next: 'airtime' },
    { label: 'Help',          next: (c) => c.end('Call 100 for help.') },
  ])
);

router.screen('balance', async (ctx) => {
  const balance = await wallet.balanceOf(ctx.phoneNumber);
  return ctx.end(`Your balance is ${balance}.`);
});

router.screen('airtime', (ctx) => ctx.prompt('Enter amount:', { next: 'airtime:confirm' }));

router.screen('airtime:confirm', (ctx) =>
  ctx.end(`You're buying ${ctx.input} airtime. Thank you!`)
);

const app = express();
app.use(express.urlencoded({ extended: false })); // AT posts form-encoded
app.post('/ussd', expressHandler(router));
app.listen(3000);

Point your Africa's Talking USSD channel at POST /ussd and dial in.

How it works

On every request the router starts at the entry screen and walks the input trail:

  • a menu maps the user's numeric pick to the chosen item's next screen;
  • a prompt sends the typed value to the next screen as ctx.input;
  • a terminal (ctx.end) closes the session.

Because the trail is replayed from the start each time, your navigation screens (menus and prompts) should be side-effect-free; do the real work in terminal screens, which only run when reached.

API

createRouter({ start? })

Creates a router. start is the entry screen name (default "start").

router.screen(name, handler)

Registers a screen. handler(ctx) may be async and returns one of the ctx results below. Chainable.

router.handle(req) → Promise<string>

Takes an Africa's Talking request body and returns the raw CON /END string.

ctx

| Member | Description | |---|---| | ctx.sessionId / ctx.phoneNumber / ctx.serviceCode / ctx.text | Raw request fields | | ctx.input | The value entered to reach this screen (menu pick or typed text) | | ctx.history | Every input so far, in order | | ctx.menu(title, items, opts?) | Numbered menu; items are { label, next }; next is a screen name or inline handler. opts.invalidText customises the bad-pick message. | | ctx.prompt(text, { next }) | Ask for free input; the typed value becomes the next screen's ctx.input | | ctx.con(text) | Keep the session open with raw text | | ctx.end(text) | End the session |

expressHandler(router)

Returns an Express handler that reads req.body and replies text/plain.

Testing

Zero runtime dependencies; tests use the Node built-in runner:

npm test   # node --test

License

MIT © Owolabi Adeyemi (goonerlabs)