Rails
TypeScript

abapay-sdk

Two functions, matching the two protocol-driven paths on this site. Signs with whatever viem account your agent already has — nothing hidden, the source is the same wire format documented on /agents/x402 and /agents/a2a.

Live on npm — npm install abapay-sdk viem
npmjs.com →

Zero setup: payBillViaX402

Fetches the 402 challenge, signs it, retries, returns the settlement.

import { privateKeyToAccount } from "viem/accounts";
import { payBillViaX402 } from "abapay-sdk";

const account = privateKeyToAccount(process.env.PRIVATE_KEY);

const result = await payBillViaX402({
  signer: account,
  bill: {
    serviceID: "mtn", serviceCategory: "AIRTIME",
    network: "MTN", billersCode: "08012345678",
    nairaAmount: 1000, token: "USDC",
  },
});

console.log(result.status, result.tx_hash);

Linked wallet: AbaPayAgent

Links once, sets the PIN in that same call, then reuses the api_key for the full tool catalog.

import { privateKeyToAccount } from "viem/accounts";
import { AbaPayAgent } from "abapay-sdk";

const account = privateKeyToAccount(process.env.PRIVATE_KEY);

// One-time -- no browser, PIN chosen right here.
const agent = await AbaPayAgent.link({ signer: account, pin: "123456" });

console.log(await agent.checkBalance());
await agent.payBill({
  pin: "123456", service: "AIRTIME", provider: "mtn",
  account_number: "08012345678", amount_ngn: 1000,
});

Full function reference

ExportSignatureDescription
payBillViaX402(params: { signer, bill, baseUrl? }) => Promise<X402PayResult>Zero-setup path. Fetches the 402 challenge, signs it, retries, returns the settlement.
AbaPayAgent.linkstatic (params: LinkParams) => Promise<AbaPayAgent>Links a wallet with a plain signed message and mints an api_key. Does NOT grant any on-chain allowance itself — approve() + setSpendingAllowance() stay a separate, explicit step.
AbaPayAgent.fromApiKeystatic (apiKey, walletAddress, baseUrl?) => AbaPayAgentReattach to an api_key minted earlier — no new signature needed.
agent.callTool(name: string, args?: object) => Promise<string>Low-level: call any of the 11 MCP tools by name. Every typed method below is a thin wrapper over this.
agent.checkBalance(chain?: 'CELO') => Promise<string>Wraps check_balance.
agent.payBill(args: { pin, service, provider, account_number, amount_ngn, chain?, token?, variation_code? }) => Promise<string>Wraps pay_bill.
agent.scheduleBill(args: object) => Promise<string>Wraps schedule_bill — same fields as the MCP tool, see the Tools reference.
agent.transactionHistory(args?: object) => Promise<string>Wraps transaction_history.

Types

TypeShape
BillDetails{ serviceID, serviceCategory, network, billersCode, nairaAmount, token: 'USDC'|'USD₮'|'USA₮', wallet_address }
X402PayResult{ success, status: 'SUCCESS'|'FAILED_VENDING'|'TIMEOUT'|string, purchased_code?, units?, request_id?, tx_hash?, message? }
LinkParams{ signer, pin, approvedChain?: 'CELO', approvedToken?, label?, baseUrl? }
AbaPayErrorextends Error — carries .cause and .response for the original failure

Correctness, not just types

A test in the package asserts the signed typed-data's domain and message match a real 402 challenge byte-for-byte — the exact contract AbaPay's own server-side verification reconstructs to check the signature against. A silent drift there would be a signature that fails to verify; the test exists so that drift fails loudly instead.

Source, tests, and the full README →