Observed since 20 Oct, 21:36 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
9bridge
Aggregates native Nigerian payment gateways, handles local currency webhooks, and outputs Stellar x402 payment challenge layouts
Nigerian payment gateway aggregator with Stellar x402 payment challenge output.
Aggregates Paystack, Monnify, and Flutterwave behind a single interface, normalizes webhooks into a unified event schema, and converts verified fiat payments into Stellar USDC via Soroban smart contracts following the x402 protocol.
npm install 9bridge
import { create9bridge } from '9bridge';
import express from 'express';
const app = express();
// Capture raw body for signature verification
app.use(express.json({
verify: (req, _res, buf) => { (req as any).rawBody = buf.toString(); }
}));
const { gatewayFactory, webhookRouter } = create9bridge(
{
paystack: { apiKey: 'sk_live_...' },
monnify: { apiKey: '...', secretKey: '...', contractCode: '...' },
flutterwave: { apiKey: 'FLWSECK-...' },
},
{
paystackSecret: 'whsec_...',
flutterwaveSecret: 'FLWSECK-...',
stellarPayTo: 'G...YOUR_STELLAR_PUBLIC_KEY',
sorobanContractId: 'C...YOUR_CONTRACT_ID',
stellarNetwork: 'testnet',
}
);
// Mount the webhook endpoint
app.use(webhookRouter);
// Use adapters directly
const paystack = gatewayFactory.resolve('card');
const { data } = await paystack.initializePayment({
amount: 5000,
currency: 'NGN',
email: 'buyer@example.com',
});
app.listen(3000);
| Channel | Gateway | Trigger Methods |
|---|---|---|
card | Paystack | card, cards, debit_card, checkout |
transfer | Monnify | transfer, bank_transfer, virtual_account |
ussd_wallet | Flutterwave | ussd, wallet, opay, palmpay, mobile_money |
create9bridge(gatewayConfig, webhookConfig)Entry point. Returns { gatewayFactory, webhookRouter }.
GatewayFactoryimport { GatewayFactory } from '9bridge';
const factory = new GatewayFactory(config);
factory.initialize();
// Resolve by channel name
const adapter = factory.resolve('card');
// Resolve by any supported alias
const adapter = factory.resolveByMethod('checkout');
// List configured channels
factory.getAvailableChannels(); // ['card', 'transfer', 'ussd_wallet']
PaystackAdapterconst paystack = factory.resolve('card');
// Initialize
const init = await paystack.initializePayment({
amount: 5000,
currency: 'NGN',
email: 'buyer@example.com',
reference: 'custom-ref-123',
metadata: { orderId: '42' },
});
// init.data.authorization_url → redirect user here
// Verify
const verification = await paystack.verifyPayment('custom-ref-123');
// verification.data.status === 'success'
// List banks (optional)
const banks = await paystack.listBanks();
MonnifyAdapterconst monnify = factory.resolve('transfer');
// Initialize bank transfer
const init = await monnify.initializePayment({
amount: 10000,
currency: 'NGN',
email: 'buyer@example.com',
});
// Create virtual account for a customer
const account = await monnify.createVirtualAccount('customer-123');
// account.data.accounts[0].account_number → share with customer
// Initiate outbound transfer
const transfer = await monnify.initiateTransfer({
amount: 5000,
currency: 'NGN',
destination_bank_code: '044',
destination_account_number: '1234567890',
reference: 'disb-001',
});
FlutterwaveAdapterconst flutterwave = factory.resolve('ussd_wallet');
// Initialize payment
const init = await flutterwave.initializePayment({
amount: 2000,
currency: 'NGN',
email: 'buyer@example.com',
});
// Initiate USSD
const ussd = await flutterwave.initiateUSSD({
amount: 2000,
currency: 'NGN',
email: 'buyer@example.com',
phone_number: '+2348012345678',
ussd_type: 'generic',
});
// Verify
const verification = await flutterwave.verifyPayment('FW-txref-123');
POST /api/v1/webhooks/payment-listener
The router handles four steps:
x-paystack-signature, flutterwave-signature, monnify-signature)IVerifiedPaymentEventReturns HTTP 402 with the x402 payment challenge:
{
"requiresPayment": true,
"paymentRequirements": {
"network": "testnet",
"x402Version": 1,
"accepts": ["stellar:usdc@testnet", "soroban:C..."],
"maxAmountRequired": "3.125000",
"resource": "/payments/ref-123",
"description": "9bridge payment: NGN 5000.00 from paystack via card",
"mimeType": "application/json",
"payTo": "G...",
"extraHeaders": {
"X-9bridge-Tx-Reference": "ref-123",
"X-9bridge-Gateway": "paystack",
"X-9bridge-Channel": "card",
"X-9bridge-Original-Amount": "NGN 5000.00"
}
},
"paymentReceipt": {
"success": true,
"transactionHash": "a1b2c3...",
"payer": "buyer@example.com",
"paymentId": "12345",
"network": "testnet",
"amount": "3.125000"
}
}
The transactionHash in the receipt is a simulated payment ID derived from the verified payment event and contract parameters. It is deterministic and does not represent an actual on-chain Stellar transaction. The real on-chain hash is produced when the Soroban contract is invoked by the recipient wallet.
The simulated payment ID is set in the X-PAYMENT-RESPONSE header.
IVerifiedPaymentEventAll gateway payloads are normalized to this shape:
interface IVerifiedPaymentEvent {
transactionId: string;
reference: string;
amount: number;
currency: string;
status: 'success' | 'failed' | 'pending' | 'reversed';
channel: 'card' | 'transfer' | 'ussd_wallet';
gateway: 'paystack' | 'monnify' | 'flutterwave';
paidAt: string;
createdAt: string;
customer: { id?: string; email: string; name?: string; phone?: string };
fees: { gateway_fee: number; processing_fee: number; total_fee: number; currency: string };
metadata: Record<string, unknown>;
rawPayload: Record<string, unknown>;
}
The x402 pipeline converts NGN to USDC using a pluggable exchange rate provider. By default it uses a static rate of 1600 NGN = 1 USD.
import {
StaticExchangeRateProvider,
CoinGeckoExchangeRateProvider,
CompositeExchangeRateProvider,
} from '9bridge';
| Provider | Source | Use case |
|---|---|---|
StaticExchangeRateProvider | Hardcoded rate | Default fallback, testing |
CoinGeckoExchangeRateProvider | CoinGecko API | Live market rates |
CompositeExchangeRateProvider | Tries providers in order | Resilience, production |
import { create9bridge, CompositeExchangeRateProvider, CoinGeckoExchangeRateProvider } from '9bridge';
const { gatewayFactory, webhookRouter } = create9bridge(
gatewayConfig,
{
...webhookConfig,
exchangeRateProvider: new CompositeExchangeRateProvider([
new CoinGeckoExchangeRateProvider(),
]),
}
);
Implement the IExchangeRateProvider interface:
import { IExchangeRateProvider, IExchangeRateQuote } from '9bridge';
class MyProvider implements IExchangeRateProvider {
async getRate(from: string, to: string): Promise<IExchangeRateQuote> {
const rate = await fetchMyRate(from, to);
return {
rate,
from,
to,
timestamp: new Date().toISOString(),
source: 'my-source',
quoteId: 'optional-id',
expiresAt: 'optional-expiry',
};
}
}
import { buildX402PaymentChallenge, simulateSorobanSwap } from '9bridge';
// Swap simulation (now async — fetches live exchange rate)
const result = await simulateSorobanSwap(verifiedEvent, {
payTo: 'G...',
sorobanContractId: 'C...',
network: 'testnet',
});
// result.simulatedPaymentId — deterministic ID, NOT an on-chain tx hash
// result.toAmount — USDC amount after conversion
// result.exchangeRate — rate source, timestamp, quote ID
// Full x402 challenge
const challenge = await buildX402PaymentChallenge(verifiedEvent, {
payTo: 'G...',
sorobanContractId: 'C...',
network: 'testnet',
});
import { PaystackAdapter, parseWebhookEvent } from '9bridge';
const paystack = new PaystackAdapter('sk_live_...');
const event = parseWebhookEvent('paystack', rawPayload);
if (event.status === 'success') {
// handle confirmed payment
}
MIT
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
axiosEnvironment{
"local_name": "axios",
"alias": false,
"peer_metadata": null
} | ^1.7.0 | runtime |
cryptoEnvironment{
"local_name": "crypto",
"alias": false,
"peer_metadata": null
} | ^1.0.1 | runtime |
expressEnvironment{
"local_name": "express",
"alias": false,
"peer_metadata": null
} | ^4.21.0 | runtime |
stellar-sdkEnvironment{
"local_name": "stellar-sdk",
"alias": false,
"peer_metadata": null
} | ^12.3.0 | runtime |
@types/expressEnvironment{
"local_name": "@types/express",
"alias": false,
"peer_metadata": null
} | ^4.17.21 | development |
@types/nodeEnvironment{
"local_name": "@types/node",
"alias": false,
"peer_metadata": null
} | ^20.14.0 | development |
typescriptEnvironment{
"local_name": "typescript",
"alias": false,
"peer_metadata": null
} | ^5.5.0 | development |
Source distribution
{
"integrity": "sha512-Vu82DeD99Z5qEfwR6L5EfXQodwsW8+/fgLQkxcInx5gcIkGZv2WoaX0eVg80tkz7hdqOpRYm0W6vN01kzeVN2Q==",
"shasum": "b63c93b19dbf265e2c47cdff067d302bd6f255fc",
"tarball": "https://registry.npmjs.org/9bridge/-/9bridge-1.0.5.tgz",
"fileCount": 46,
"unpackedSize": 94955,
"signatures": [
{
"keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U",
"sig": "MEYCIQD9/qzr1jo2bzwB5riSPbY3K16fGNmhpTv1mnWiB/4eCAIhAMGCE1MGkfdkjYI5gew8+nJ84076LuAAhhBImbFcInsq"
}
]
}Observed since 20 Oct, 21:36 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:722768e53ee433bf5ab8ad78377fd7682e4b6a9efb7b67c921ff3d2416987ee3