x402 · USDC on Base + Solana

x402-notify

One message, one payment, one receipt — Telegram and SMTP email delivered per-message with the proof of delivery in the response.

git clone https://github.com/nirholas/x402-notify
cd x402-notify && npm install
npm run dev   # http://localhost:4025

Pricing

RoutePriceWhat you get back
POST /telegram$0.002`{messageId, chat, deliveredAt}` — the message id and the chat Telegram resolved, usable later to reply to or edit the message
POST /email$0.002the SMTP acceptance receipt — raw `250 …` response, reply code, Message-ID, host — plus accepted/rejected recipients and the envelope as sent
GET /healthfreeLiveness, data source, configured rails
GET /.well-known/x402freeMachine-readable discovery manifest

Every paid route returns the purchased artifact in the 200 body — nothing is deferred to a webhook or a second fetch.

How a paid call works

1 · RequestAgent calls POST /telegram
2 · 402Server quotes the price on both rails
3 · PayClient signs on its rail, retries with X-PAYMENT
4 · 200Artifact in the body + settlement receipt header

Two rails, one challenge

Pay in USDC on Base or Solana — your client picks. The 402 body always lists both.

EVM · Base

USDC on base-sepolia (or base). The wallet signs an EIP-3009 authorization locally — no server round-trip.

0x40252CFDF8B20Ed757D61ff157719F33Ec332402

Solana

USDC SPL transfer on solana. A facilitator sponsor pays the SOL network fee, so buyers need only USDC. Helpers at /api/x402-checkout build and wrap the transaction.

WwwuGbqHrwF5RG89KhUbmRWEvjnRH9k5kVM5p7T3WwW

Quickstart for agents

import { privateKeyToAccount } from "viem/accounts";
import { wrapFetchWithPayment } from "x402-fetch";

const payFetch = wrapFetchWithPayment(fetch, privateKeyToAccount(process.env.PRIVATE_KEY));

// Ping a chat — $0.002
const tg = await (await payFetch("http://localhost:4025/telegram", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ chatId: "123456789", text: "Deploy finished." }),
})).json();

// Or email, with the SMTP acceptance receipt — $0.002
const mail = await (await payFetch("http://localhost:4025/email", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ to: "dana@example.com", subject: "Deploy finished", text: "All green." }),
})).json();

console.log(mail.receipt.smtpResponse);   // "250 2.0.0 OK 1738012345 ..."

Telegram delivers for real when `TELEGRAM_BOT_TOKEN` is set (a free token from @BotFather). Email relays for real when `SMTP_HOST` is set (any server: Gmail app password, Postmark, SES, Mailtrap, a local relay). Each channel is independent — you can run one live and one dry. Unconfigured channels return `"source": "fixture"`, `"delivered": false` and a `dryRunReason` explaining exactly what was skipped. **Nothing ever reports a delivery that didn't happen.**

Links