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

@afidos/ussd-engine

v2.0.2

Published

Librairie ussd NestJS

Readme

@afidos/ussd-engine

A powerful Node.js framework for creating, managing, and deploying USSD (Unstructured Supplementary Service Data) applications simply and effectively. Ideal for developing mobile services accessible without internet in emerging markets.

npm version License

IN FRENCH

🚀 Features

  • 🔄 Standardized API: Abstraction of differences between telecom operators
  • 📱 Advanced menu management: Intuitive API for creating interactive menus with static and dynamic options
  • 💾 Robust session management: Persistent storage of user data between requests
  • 🛠️ Flexible storage adapters: Support for memory, MongoDB, and PostgreSQL
  • 🧩 Native NestJS integration: Ready-to-use module and interceptors
  • 🌍 Multi-operator: Support for different operators (Moov, Celtis, etc.)
  • 📝 Input fields: Easy creation of USSD forms with built-in validation
  • 🔄 Contextual navigation: Navigation system with history and back functionality

📋 Table of Contents

📦 Installation

npm install @afidos/ussd-engine

Or with Yarn:

yarn add @afidos/ussd-engine

🏁 Quick Start

Here's a minimal example to create a USSD application:

import { USSDMenuBuilder, USSDService, InMemoryAdapter } from '@afidos/ussd-engine';

// Create the menu builder
const menuBuilder = new USSDMenuBuilder();

// Define menus
menuBuilder
  .createMenu('main', 'Main Menu')
  .addOption('main', 'balance', 'Check balance', async (session) => {
    // Simulate balance retrieval
    session.setData('balance', 5000);
    return USSDService.createNavigator().navigateTo('showBalance')(session);
  })
  .addOption('main', 'transfer', 'Transfer', async (session) => {
    return USSDService.createNavigator().navigateTo('transferAmount')(session);
  })
  .createMenu('showBalance', (session) => `Your balance is ${session.getData('balance')} FCFA`)
  .addOption('showBalance', 'back', 'Back', USSDService.createNavigator().goBack())
  .addOption('showBalance', 'exit', 'Exit', USSDService.createNavigator().endSession('Thank you for using our service!'))
  .createInputField('transferAmount', 'Enter the amount to transfer:', 
     USSDService.createNavigator().createInputField('amount', 'transferRecipient'))
  .createInputField('transferRecipient', 'Enter the recipient number:',
     USSDService.createNavigator().createInputField('recipient', 'confirmTransfer'))
  .createMenu('confirmTransfer', (session) => 
    `Transfer of ${session.getData('amount')} FCFA to ${session.getData('recipient')}. Confirm?`)
  .addOption('confirmTransfer', 'confirm', 'Confirm', async (session) => {
    // Transfer logic here
    return USSDService.createNavigator().endSession('Transfer completed successfully!')(session);
  })
  .addOption('confirmTransfer', 'cancel', 'Cancel', USSDService.createNavigator().endSession('Transfer cancelled.'));

// Create the USSD service with an in-memory storage adapter
const storageAdapter = new InMemoryAdapter();
const ussdService = new USSDService(menuBuilder.build(), storageAdapter);

// Example of request processing
async function handleRequest(request) {
  const response = await ussdService.processRequest({
    sessionId: request.sessionId,
    phoneNumber: request.phoneNumber,
    text: request.text,
    serviceCode: request.serviceCode
  });
  
  return response;
}

🏛️ Architecture

USSD Engine's architecture is designed to be modular and extensible:

@afidos/ussd-engine
├── core            - Core USSD service logic
├── menu            - Menu management and navigation
├── session         - User session management
├── storage         - Adapters for different storage systems
├── transformer     - Formatters for different telecom operators
├── types           - Common interfaces and types
└── ussd-engine     - NestJS module and integration

📚 Usage Guide

Creating Menus

Creating USSD menus is done with the USSDMenuBuilder class:

const menuBuilder = new USSDMenuBuilder();

// Simple menu with static title
menuBuilder.createMenu('main', 'Welcome to our service');

// Menu with dynamic title based on session data
menuBuilder.createMenu('profile', (session) => `Hello ${session.getData('username')}`);

// Adding options to a menu
menuBuilder.addOption('main', 'profile', 'My profile', (session) => {
  return USSDService.createNavigator().navigateTo('profile')(session);
});

// Option with custom action
menuBuilder.addOption('main', 'balance', 'Check balance', async (session) => {
  const balance = await fetchBalanceFromAPI(session.phoneNumber);
  session.setData('balance', balance);
  return USSDService.createNavigator().navigateTo('showBalance')(session);
});

Custom Option Indexes

You can define custom indexes for menu options instead of using automatic sequential numbering (1, 2, 3...):

const menuBuilder = new USSDMenuBuilder();

menuBuilder
  .createMenu('main', 'What would you like to do?')
  // Automatic index (will be 1)
  .addOption('main', 'balance', 'Check balance', balanceAction)
  // Automatic index (will be 2) 
  .addOption('main', 'transfer', 'Transfer money', transferAction)
  // Custom index 9 for help
  .addOption('main', 'help', {index: 9, text: 'Help'}, helpAction)
  // Custom index 0 for back/exit
  .addOption('main', 'exit', {index: 0, text: 'Exit'}, exitAction);

Display result:

What would you like to do?
0. Exit
1. Check balance  
2. Transfer money
9. Help

Supported text formats:

  • 'Simple text' - Automatic sequential index
  • {index: 5, text: 'Custom index 5'} - Custom index
  • (session) => 'Dynamic text' - Dynamic function (automatic index)

Benefits:

  • Consistent UX: Keep same indexes across different menus (0 = back, 9 = help)
  • User-friendly: Users can memorize shortcuts
  • Flexible: Mix automatic and custom indexes as needed
  • Backward compatible: Existing code continues to work

Session Management

The USSDSession class allows managing user data between interactions:

// In an action handler
async function handleProfileUpdate(session) {
  // Storing data
  session.setData('username', 'John Doe');
  session.setData('lastLogin', new Date().toISOString());
  
  // Retrieving data
  const username = session.getData('username');
  
  // Retrieving all data
  const allUserData = session.getAllData();
  
  // Saving changes
  await session.save();
  
  // Ending the session
  await session.end();
}

Navigation Between Menus

Navigation is managed by the Navigator via the createNavigator() method:

// Get a navigator
const navigator = USSDService.createNavigator();

// Navigate to another menu
menuBuilder.addOption('main', 'settings', 'Settings', navigator.navigateTo('settingsMenu'));

// Return to previous menu
menuBuilder.addOption('settings', 'back', 'Back', navigator.goBack());

// End the session with a message
menuBuilder.addOption('main', 'exit', 'Exit', navigator.endSession('Thank you for using our service!'));

Input Fields

Creating fields to collect information:

// Simple input field
menuBuilder.createInputField(
  'enterName',              // Field ID
  'Enter your name:',       // Prompt
  (input, session) => {     // Processing handler
    session.setData('name', input);
    return navigator.navigateTo('nextMenu')(session);
  }
);

// Field with validation
menuBuilder.createInputField(
  'enterPhone',
  'Enter your phone number:',
  navigator.createInputField('phone', 'confirmation', 'Invalid format', 
    (input) => /^\d{8}$/.test(input)) // Validation: 8 digits
);

Dynamic Options

Generating options based on data with custom indexes:

menuBuilder
  .createMenu('products', 'Our products')
  .addDynamicOptions('products', async (session) => {
    // Retrieve products from an API 
    const products = await fetchProductsFromAPI();
    
    // Generate options with custom indexes
    const options = products.map((product, index) => ({
      id: `product_${product.id}`,
      text: {index: index + 1, text: `${product.name} - ${product.price} FCFA`},
      action: async (session) => {
        session.setData('selectedProduct', product);
        return navigator.navigateTo('productDetails')(session);
      }
    }));

    // Add common navigation options
    options.push(
      {
        id: 'back',
        text: {index: 0, text: 'Back to main menu'},
        action: navigator.goBack()
      },
      {
        id: 'help',
        text: {index: 9, text: 'Help'},
        action: navigator.navigateTo('help')
      }
    );

    return options;
  });

Validation and Error Handling

// Add a validator to a menu
menuBuilder.setValidator('transferAmount', (input, session) => {
  const amount = parseInt(input);
  return !isNaN(amount) && amount > 0 && amount <= 1000000;
});

// Add a custom error handler
menuBuilder.setErrorHandler('transferAmount', (error, session) => {
  console.error('Transfer error:', error);
  return {
    text: 'An error occurred during the transfer. Please try again later.',
    type: 'end'
  };
});

🗄️ Storage Adapters

In-Memory Storage

Ideal for testing and development:

import { InMemoryAdapter } from '@afidos/ussd-engine';

// Simple storage
const adapter = new InMemoryAdapter();

// With session expiration (TTL)
const adapter = new InMemoryAdapter({ ttlMs: 5 * 60 * 1000 }); // 5 minutes

MongoDB Storage

For production environments with MongoDB:

import { MongoAdapter } from '@afidos/ussd-engine';

// Creating the adapter
const adapter = await MongoAdapter.create(
  'mongodb://localhost:27017',   // Connection URL
  'ussd_database',              // Database name
  'ussd_sessions',             // Collection name
  { ttlSeconds: 3600 }        // Expiration after 1 hour
);

PostgreSQL Storage

For production environments with PostgreSQL:

import { PgAdapter } from '@afidos/ussd-engine';

// Creating the adapter
const adapter = await PgAdapter.create(
  'postgresql://user:password@localhost:5432/ussd_db',
  'ussd_sessions' // Table name
);

🔄 Operator Formatters

Moov Formatter

For Moov operator requests:

import { MoovFormatter, USSDInterceptor } from '@afidos/ussd-engine';

// Creating a Moov interceptor
const moovInterceptor = new USSDInterceptor({
  formatter: new MoovFormatter()
});

Celtis Formatter

For Celtis operator requests:

import { CeltisFormatter, USSDInterceptor } from '@afidos/ussd-engine';

// Creating a Celtis interceptor
const celtisInterceptor = new USSDInterceptor({
  formatter: new CeltisFormatter()
});

🦁 NestJS Integration

USSD Engine Module

import { Module } from '@nestjs/common';
import { UssdEngineModule, InMemoryAdapter, USSDMenuBuilder } from '@afidos/ussd-engine';

@Module({
  imports: [
    UssdEngineModule.register({
      storageAdapter: new InMemoryAdapter(),
      menuBuilderFactory: () => {
        // Menu configuration
        return new USSDMenuBuilder()
          .createMenu('main', 'Main Menu')
          // ...other menu configurations
          .build();
      }
    })
  ]
})
export class AppModule {}

Interceptors

import { Controller, Post, UseInterceptors } from '@nestjs/common';
import { 
  USSDInterceptor, 
  MoovFormatter, 
  CeltisFormatter,
  UssdParam,
  USSDRequest,
  USSDResponse
} from '@afidos/ussd-engine';

@Controller('ussd')
export class UssdController {
  constructor(private readonly ussdService: USSDService) {}

  @Post('moov')
  @UseInterceptors(new USSDInterceptor({ formatter: new MoovFormatter() }))
  async handleMoovRequest(@UssdParam() request: USSDRequest): Promise<USSDResponse> {
    return this.ussdService.processRequest(request);
  }

  @Post('celtis')
  @UseInterceptors(new USSDInterceptor({ formatter: new CeltisFormatter() }))
  async handleCeltisRequest(@UssdParam() request: USSDRequest): Promise<USSDResponse> {
    return this.ussdService.processRequest(request);
  }
}

Decorators

The @UssdParam() decorator makes it easy to extract standardized USSD data:

// Extract the entire request
@UssdParam() request: USSDRequest

// Extract specific properties
@UssdParam('phoneNumber') phoneNumber: string
@UssdParam('text') userInput: string
@UssdParam('sessionId') sessionId: string

📝 Complete Examples

USSD Banking Service

const menuBuilder = new USSDMenuBuilder();

menuBuilder
  // Main menu
  .createMenu('main', 'My Bank - Menu')
  .addOption('main', 'account', 'My account', navigator.navigateTo('accountMenu'))
  .addOption('main', 'transfer', 'Transfer', navigator.navigateTo('transferAmount'))
  .addOption('main', 'payment', 'Bill payment', navigator.navigateTo('paymentMenu'))
  .addOption('main', 'help', 'Help', navigator.navigateTo('helpMenu'))

  // Account menu
  .createMenu('accountMenu', 'Account Menu')
  .addOption('accountMenu', 'balance', 'Balance', async (session) => {
    // Get balance from API
    const balance = await bankAPI.getBalance(session.phoneNumber);
    session.setData('balance', balance);
    return navigator.navigateTo('showBalance')(session);
  })
  .addOption('accountMenu', 'statement', 'Mini statement', navigator.navigateTo('statementMenu'))
  .addOption('accountMenu', 'back', 'Back', navigator.goBack())

  // Balance display
  .createMenu('showBalance', (session) => 
    `Your balance is: ${session.getData('balance')} FCFA`)
  .addOption('showBalance', 'back', 'Back', navigator.goBack())
  .addOption('showBalance', 'exit', 'Exit', 
    navigator.endSession('Thank you for using My Bank!'))

  // Transfer: amount entry
  .createInputField('transferAmount', 'Enter amount to transfer:',
    navigator.createInputField('amount', 'transferRecipient', 'Invalid amount',
      (input) => !isNaN(input) && parseInt(input) > 0))

  // Transfer: recipient entry
  .createInputField('transferRecipient', 'Enter recipient number:',
    navigator.createInputField('recipient', 'confirmTransfer', 'Invalid number',
      (input) => /^\d{8}$/.test(input)))

  // Transfer confirmation
  .createMenu('confirmTransfer', (session) => 
    `Confirm transfer of ${session.getData('amount')} FCFA to ${session.getData('recipient')}?`)
  .addOption('confirmTransfer', 'confirm', 'Confirm', async (session) => {
    try {
      await bankAPI.transfer(
        session.phoneNumber,
        session.getData('recipient'),
        session.getData('amount')
      );
      return navigator.endSession('Transfer completed successfully!')(session);
    } catch (error) {
      return navigator.endSession('Transfer failed: ' + error.message)(session);
    }
  })
  .addOption('confirmTransfer', 'cancel', 'Cancel', 
    navigator.endSession('Transfer cancelled.'));

📘 API Reference

Main Classes

  • USSDMenuBuilder: USSD menu builder
  • USSDService: Main service for processing USSD requests
  • USSDSession: User session management
  • USSDInterceptor: NestJS interceptor for transforming requests/responses

Key Interfaces

  • USSDRequest: Standardized format for USSD requests
  • USSDResponse: Standardized format for USSD responses with title and options
  • USSDMenu: USSD menu structure
  • MenuOption: USSD menu option with flexible text format
  • MenuOptionConfig: Configuration object for custom option indexes
  • StorageAdapter: Interface for storage adapters
  • USSDFormatter: Interface for operator formatters

Updated Interface Definitions

export interface USSDResponse {
  title: string;
  options?: Record<number, string>;
  type: 'continue' | 'end';
}

export interface MenuOptionConfig {
  index: number;
  text: string;
}

export interface MenuOption {
  id: string;
  text: string | MenuOptionConfig | ((session: USSDSessionInterface) => string | Promise<string>);
  action: (session: USSDSessionInterface) => Promise<USSDResponse> | USSDResponse;
}

Important Methods

USSDMenuBuilder:

  • createMenu(id, title): Creates a new menu
  • createInputField(id, title, processInput): Creates an input field
  • addOption(menuId, optionId, text, action): Adds an option to a menu
  • addDynamicOptions(menuId, generator): Adds dynamic options
  • setValidator(menuId, validator): Sets a validator for a menu
  • setErrorHandler(menuId, handler): Sets an error handler
  • build(): Builds and returns the menu map

USSDService:

  • processRequest(request): Processes a USSD request
  • createNavigator(): Creates a navigation object

USSDSession:

  • setData(key, value): Stores data
  • getData(key): Retrieves data
  • getAllData(): Retrieves all data
  • save(): Saves the session
  • end(): Ends the session

💡 Best Practices

  1. Menu Structure: Limit navigation depth to 3-4 levels maximum for a better user experience.

  2. Short Texts: USSD screens have limitations (often 160-182 characters), keep your texts concise.

  3. State Management: Use session.setData() to store important data between requests.

  4. Validation: Always validate user inputs with appropriate validators.

  5. Intuitive Navigation: Always provide back and exit options.

  6. Performance: For menus with many options, use pagination or group them by categories.

  7. Error Handling: Implement custom error handlers for each critical menu.

  8. Testing: Test your USSD menus on different devices and with different operators.

❓ FAQ

Q: Can I use USSD Engine without NestJS?
A: Yes, USSD Engine works independently. NestJS integration is optional.

Q: How to handle expired sessions?
A: Use TTL options in storage adapters to automatically clean up inactive sessions.

Q: Does the application support Unicode/accented characters?
A: Yes, but be careful as some USSD terminals may have limited support.

Q: How to debug my USSD application?
A: Use detailed logs and the InMemoryAdapter during development to inspect session states.

Q: How to test my USSD service without real equipment?
A: You can simulate USSD requests with tools like Postman by following your telecom operator's format.

🤝 Contribution

Contributions are welcome! Feel free to submit pull requests.

  1. Fork the project
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.