Janus Pay · developers Get startedWidgetSDK NoticesJanus app
Janus Pay

Take any coin.
Get paid in BSV.

Customers pay from the wallet they already use: BSV goes straight to you, and SOL, HYPE or BELLS are swapped through Janus's liquidity pool and paid to your address in BSV, usually within minutes. Or put Janus swaps inside your app. No exchange account, and nobody holds anyone's keys.

Kettle & Crow Coffeea pretend shop
Ethiopia Guji, 340 g0.45 BSV
Peach, bergamot, black tea. Roasted Monday, ships Tuesday.
Whole beanFilterEspresso
● Try it

Press Check out. This is the real widget with live prices. Pay in BSV straight to the shop with no fee, or pick SOL, HYPE or BELLS and see the exact amount a customer would send. The payment is simulated here, so nothing is sent.

All this shop added
<script async
  src="https://janusprotocol.xyz/embed/v1.js">
</script>

<janus-pay button
  to="1YourBsvAddress…"
  sat="45000000">
</janus-pay>

Your site doesn't need to handle BSV itself: Janus watches for the payment and tells your page when it's paid. More coins show up in the same button as Janus adds them, with no change to your code.

Get started: one line

This is all it takes to take BSV and every other coin Janus carries, without writing any payment code. Put the loader on your page once, and the tag wherever you take payment:

One line
<script src="https://janusprotocol.xyz/embed/v1.js" async></script>

<janus-pay to="1YourBsvAddress…" price="29.74" currency="USD" button></janus-pay>

Price it in dollars with price and currency (Janus turns it into BSV at its own price when the customer pays, and locks that rate), or in satoshis with sat="85000000". That shows a Pay $29.74 button. BSV is first: the customer pays your address directly from their wallet (one tap in Yours or a BRC-100 wallet, or scan the code), with no fee. Janus never holds it and only watches for it, so your page is told when it's paid. Or the customer picks SOL, HYPE or BELLS and pays from their own wallet; Janus swaps it through its pool and sends at least sat to your address, as an ordinary BSV payment. Your app sees it the way it sees any payment to that address. No account, no partner id, no server. Add item and name to say what's being bought, and accent for your colour.

When you can't put the widget on your page (a Shopify order email, a WooCommerce redirect, an invoice, a QR code), send the customer to Janus's checkout page instead. It takes the same options as the tag, plus where to send them back:

Link
https://janusprotocol.xyz/checkout.html?to=1YourBsvAddress…&price=29.74&currency=USD
  &name=Your Shop&item=Order 1001&ref=1001
  &return=https://yourshop.com/thanks&cancel=https://yourshop.com/cart

(One line, no spaces.) The SDK builds it for you: checkoutUrl({ to, price, orderRef, returnUrl, cancelUrl }). When the customer has paid, the page offers a button back to return with ?janus_payment=<id>&janus_status=completed&ref=<ref> added. Confirm it on your server with GET /pay/{id} (or a signed notice) before you ship: the link itself is not proof of payment. Check that its status is completed, its order_ref is this order, its to_address is yours, and the amount covers the order; and use each payment id for one order only. "Back to the shop" adds janus_status=cancelled only if the payment really was cancelled (otherwise abandoned: it could still be paid, so don't cancel the order on that alone).

Shopify: add a manual payment method ("Pay with crypto"), and put the link in the order confirmation email with the order's total as price and its number as ref. Mark the order paid when the payment completes. WooCommerce: a payment gateway that redirects to the link and marks the order paid from the notice.

Get your keys (optional)

Nothing above needs an account. Keys add signed notices to your server as each payment moves (so orders fulfil themselves), a lock so checkouts under your partner id only pay your addresses, an API key for your server (20× the rate limits, idempotent creates, your payments list, a test notice, and changing your settings, payout addresses included, so treat it like a password), and they're yours in a minute:

The notice secret and API key are shown once; keep both on your server only. Lost one, or think it leaked? POST /pay/me with {"rotate_secret": true} or {"rotate_api_key": true} gives you a new one and the old one stops working. Then fulfil from a payment.completed notice whose signature checks out, never from the browser. Test your handler any time with POST /pay/me/test-notice.

Widget

The widget runs in an iframe on janusprotocol.xyz. Your page can't read it and it can't read your page. Add the loader once, then a tag wherever you want a checkout or a swap.

Checkout
<script src="https://janusprotocol.xyz/embed/v1.js" async></script>

<janus-pay
  partner="your-id"
  to="1YourBsvAddress…"
  sat="85000000"
  name="Your Shop"
  item="Glass Owl #4412"
  ref="order-1001"
  theme="dark"
  accent="#e2b04a"></janus-pay>
Swap
<janus-swap partner="your-id" name="Your Wallet" theme="light" accent="#7c5cff"></janus-swap>
AttributeUsed byWhat it does
buttonallShow a button that opens the box in a sheet, instead of the box itself. Give it text to change the label, e.g. button="Pay with SOL".
outlineallWith button: the gradient (or your accent) as a border only, on a clear background in your page's text colour.
coinscheckoutWith button: list the coins it takes (with their logos) under the label, instead of "with Janus · any coin".
democheckoutTry it without taking money: live prices, but the payment is simulated and no request reaches Janus. Take it off before you go live.
partnerbothOptional. Your partner id: adds signed notices and limits payouts to your registered addresses.
tocheckoutYour BSV address. No sign-up needed; if you use a partner id, it must be one of that partner's registered payout addresses.
satcheckoutWhat you're owed, in satoshis. You receive at least this. Minimum 10,000.
price, currencycheckoutOr price it in a currency (USD): the customer sees the price, and Janus turns it into BSV at its own price when they pay, locking that rate.
logobothYour logo (an https:// image), shown at the top in place of your initial.
radiusbothCorner radius in px, 0–24 (default 12), to match your site.
namebothYour name, shown at the top of the box.
itemcheckoutWhat's being bought, shown to the customer.
refcheckoutYour order reference. Comes back in every notice. Up to 64 of A-Z a-z 0-9 . _ : # / -
assetsbothLimit the coins offered, e.g. SOL,HYPE. Default: every coin that's open.
themebothdark or light.
accentbothYour brand colour, #rrggbb. Button text turns dark or white to stay readable on it.
widthbothWidth in px (default 420). Shrinks to fit a phone.

Browser events

Events bubble from the tag. Use them for your UI, e.g. a thank-you screen. They're not proof of payment.

EventWhen
janus:readyThe widget has loaded.
janus:quoteA price was shown or refreshed.
janus:payment.createdThe customer started a payment.
janus:payment.statusIts status changed (detail carries the payment).
janus:payment.completedPaid out.
janus:payment.cancelledThe customer cancelled before paying.
JavaScript
document.addEventListener("janus:payment.completed", (e) => {
  showThankYou(e.detail.order_ref);   // UI only. Fulfil from the signed notice.
});

SDK

Load it from npm or straight from our site. It works in browsers and in Node 18+.

Install
npm install @janusprotocol/pay
// or, with no build step:
import { JanusPay } from "https://janusprotocol.xyz/sdk/v1.js";
Checkout
import { JanusPay } from "@janusprotocol/pay";
const janus = new JanusPay({ partner: "your-id" });

// How much SOL pays exactly 0.85 BSV to you?
const q = await janus.quote({ toSat: 85_000_000, payWith: "SOL" });

// Start it. Pay from fromAddress (failed swaps are refunded there). SOL is also matched by
// its exact amount, all nine places, so a payment from an exchange counts too.
const p = await janus.createCheckout({
  toAddress: "1YourBsvAddress…", toSat: 85_000_000, payWith: "SOL",
  fromAddress: customerSolanaAddress, orderRef: "order-1001",
});
// Show p.pay_to and p.amount exactly. Keep p.secret with the customer (it cancels).

const stop = janus.watch(p.payment_id, (s) => render(s.status));
Swap
// Into BSV: receive_min is guaranteed.
await janus.swapQuote({ from: "SOL", to: "BSV", amount: 0.5 });
await janus.createSwap({ from: "SOL", to: "BSV", amount: 0.5,
  fromAddress: theirSolanaAddress, toAddress: theirBsvAddress });

// Out of BSV: receive_about is an estimate, priced when the BSV arrives.
await janus.createSwap({ from: "BSV", to: "SOL", amount: 2,
  fromAddress: theirBsvAddress /* refunds go here */, toAddress: theirSolanaAddress });
MethodReturns
assets()Coins open right now, with names, colours, logos (logo, an https link) and the most each can pay.
quote({ toSat, payWith })Checkout price: amount, guaranteed_bsv_sat, price_impact_pct, needs_acceptance.
swapQuote({ from, to, amount })Swap price: receive_min (into BSV) or receive_about (out of BSV).
createCheckout({ … })payment_id, pay_to, amount, secret, expires_at.
createSwap({ … })Same shape as a checkout.
getPayment(id)The payment now (see statuses).
watch(id, fn)Calls fn on every change until it's final. Returns a stop function.
cancel(id, secret)Cancels it, only before anything is paid.
mount(el, opts)The ready-made widget, from code.
verifyNotice(secret, header, rawBody)Checks a notice's signature on your server.
rate({ currency })Janus's BSV price now (bsv in USD).
checkoutUrl({ to, price, orderRef, returnUrl, cancelUrl })A hosted checkout link (no network call).
me(), updateMe({ … })Your partner settings; change them, or rotate your notice secret / API key. Needs apiKey.
payments({ ref, status, limit, before })Your payments, newest first. Needs apiKey.
refund(id, { amountSat })Refund a completed SOL/HYPE/BELLS checkout of yours: returns the BSV amount and address you pay; Janus then pays the customer in their coin, to the address they paid from. Needs apiKey.
testNotice()Sends a signed payment.test to your URL now. Needs apiKey.

If the price moves by more than the usual amount, needs_acceptance is true. Show the customer price_impact_pct and pass acceptedImpactPct when they agree.

Sell Twetch NFTs for any coin

List Twetch NFTs on your site (a finder, a gallery, a collection page) and let anyone buy them with SOL, HYPE or BELLS. The buyer pays Janus; Janus buys the listing on Twetch with BSV and sends it to the buyer's Twetch account. Your site never holds the NFT or the money, and you don't need a partner id. The price shown includes the NFT, Twetch's fees and ours.

Buy button
<script src="https://janusprotocol.xyz/embed/v1.js" async></script>

<janus-buy
  collection="20d0f7e0f64cd5a9e6a12687a29ee896d5f487fe4e8f458798fcacceb4dca3c5"
  number="3931"
  recipient="65502"
  theme="dark"></janus-buy>

collection is the Twetch collection's 64-character id and number the NFT's number; both come with every Twetch listing. recipient (the buyer's Twetch user number) is optional: leave it out and the buyer types it. assets, theme, accent, name and width work as on the other tags. The tag fires the same browser events; janus:payment.completed means delivered.

SDK
const q = await janus.nftQuote({ collection, number, payWith: "SOL" });
// q.item {name, image, priceSats, rarity}, q.amount, q.buyable, q.needs_acceptance

const o = await janus.buyNft({ collection, number, listingId: q.item.id, payWith: "SOL",
                               fromAddress: buyerSolanaAddress, recipient: "65502" });
// show o.pay_to and o.amount EXACTLY (SOL to all nine places: it's how the payment is matched)

janus.watchNft(o.order_id, (s) => render(s.status));
// waiting → buying → delivering → delivered   (or held / expired / failed)

HTTP API

Base URL https://api.janusprotocol.xyz. JSON in, JSON out. Callable from any origin.

CallWhat it does
GET /pay/assetsCoins open right now.
GET /pay/quote?to_sat=&pay_with=
GET /pay/quote?price=&currency=USD&pay_with=
Checkout price, for an amount in sat or a price in dollars.
GET /pay/rate?currency=USDWhat 1 BSV is worth now, by the price Janus converts at.
GET /pay/swap-quote?from_asset=&to_asset=&amount=Swap price.
POST /pay/{id}/refundWith your API key. Body: amount_sat (default: all that's left), refund_from (your BSV address; default your first payout address). Returns pay_to and amount in BSV: pay it, and Janus pays the customer back in the coin they used. The refund is its own payment (mode: "swap", metadata.refund_of) with its own notices. BSV payments went straight to you: refund those from your wallet.
POST /pay/createStart a checkout or swap. Body: partner (optional), mode, pay_with, from_address, to_address, to_sat (or price + currency) or amount, order_ref, metadata (up to 20 of your own fields, returned on every read and notice), idempotency_key (or the Idempotency-Key header, sent with your API key from your server: retrying with the same key returns the same payment; the same key with a different request is a 409), accepted_slippage_pct.
GET /pay/{id}A payment's status, with order_ref, to_address, to_sat and received_sat. Without a partner id, call this from your server to confirm a payment before you ship: status is completed, the address is yours and received_sat covers the order. A cancelled payment reads expired with the message "cancelled before payment".
POST /pay/{id}/bsv-sentA BSV checkout's wallet just paid. Body {"txid": "…", "raw": "…"} (raw optional). Checks it now instead of at the next scan; optional.
POST /pay/{id}/cancelBody {"secret": "…"}. Cancels it before payment.
POST /pay/keysGet a partner id, notice secret and API key. Body: name, payout_addresses, webhook_url (optional), site. Secrets shown once.
GET /pay/me · POST /pay/meWith Authorization: Bearer jpk_…: your settings; change the notice URL, payout addresses or name, or rotate_secret.
POST /pay/me/test-noticeSends a signed payment.test notice to your URL now and shows what your server answered.
GET /pay/me/payments?ref=&status=&limit=&before=Your payments, newest first.
GET /pay/nft/quote?collection=&number=&pay_with=What a Twetch listing costs in a coin, fees included.
POST /pay/nft/buyStart buying it. Body: collection, number, listing_id (optional), pay_with, from_address, recipient (Twetch user number), accepted_slippage_pct, order_ref.
GET /pay/nft/{order_id}An NFT order: waiting, buying, delivering, delivered, held (a person is finishing it), expired, failed.

Signed notices

Each time a payment moves, Janus sends a POST to your notice URL. The header is Janus-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is computed over "<t>.<raw body>", keyed with your notice secret. Notices are retried with backoff for about a day until you answer with a 2xx.

Body
{ "type": "payment.completed",
  "data": { "id": "…", "status": "completed", "mode": "checkout", "order_ref": "order-1001",
            "pay_with": "SOL", "pay_amount": "0.1426", "to_address": "1You…",
            "to_sat": 85000000, "received_sat": 85012000, "payout_txid": "…" } }
Node (Express)
import express from "express";
import { verifyNotice } from "@janusprotocol/pay";

app.post("/janus", express.raw({ type: "application/json" }), async (req, res) => {
  if (!(await verifyNotice(SECRET, req.get("janus-signature"), req.body))) return res.sendStatus(400);
  const { type, data } = JSON.parse(req.body);
  if (type === "payment.completed" && data.mode === "checkout") {
    const order = await db.orders.get(data.order_ref);             // YOUR record
    if (order && !order.paid && data.to_address === MY_BSV_ADDRESS
        && data.received_sat >= order.total_sat) {
      await db.orders.markPaid(order.id, data.id, data.payout_txid);   // once only
    }
  }
  res.sendStatus(200);
});
Python
import hashlib, hmac, time

def verify_notice(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        ts = int(parts["t"])
    except Exception:
        return False
    if abs(time.time() - ts) > tolerance:
        return False
    want = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, parts.get("v1", ""))
Before you fulfil an order
  • The signature checks out, with t within five minutes.
  • type is payment.completed and mode is checkout.
  • to_address is yours.
  • order_ref is an order of yours, and the amount matches it in your own records: received_sat covers the order total in sat, or, if you price in dollars, price and currency are the order's (the rate was locked when the customer paid). The amount on a page can be changed by whoever loads it, and anyone can start a checkout that names your partner id.
  • You haven't handled this payment id before. A notice can arrive more than once.

Payment statuses

StatusNoticeMeaning
waitingnoneCreated; nothing has arrived yet.
seenpayment.seenThe customer's coin has arrived; payout is under way.
completedpayment.completedBSV paid out. received_sat and payout_txid are set.
underpaidpayment.underpaidPaid out, but less than asked (the customer sent too little).
heldpayment.heldArrived but needs a person, e.g. a coin paused for maintenance. We sort it out.
failedpayment.failedCouldn't be filled; the customer is refunded.
expiredpayment.expiredNothing arrived within 30 minutes. In BSV, a payment that arrives later the same day still counts: a payment.completed follows.

How it stays safe

Coins & limits

WhatValue
Customers pay withBSV (straight to you), SOL (Solana), HYPE (HyperEVM), BELLS (Bellscoin). New coins appear in the widget and /pay/assets as Janus opens them, with no change on your side. A coin paused for maintenance shows as unavailable.
You receiveBSV, at your address.
PricesIn satoshis, or in USD (converted at Janus's BSV price and locked for the payment).
Smallest checkout10,000 sat.
Largest checkoutIn BSV: no limit. In another coin: what the pool can take for it right now, limits.max_bsv_by_coin in /pay/assets. It grows as the pools do; a bigger order can be offered in BSV or split.
A payment request lasts30 minutes.
Rate limitsPer IP address: 30 payments created and 120 quotes a minute. With your API key (server side), 20× that, counted per key. A 429 carries Retry-After.
FeesPaid in BSV: none, it goes straight to you. Paid in another coin: the swap fee (2–5%, lower when the payment helps balance the pool) and a 1% price cushion are in the amount the customer sees, and anything the cushion doesn't use goes to you. You always get at least your price. There is no partner fee and no revenue share.

AI agents (MCP)

An AI agent (Claude, Cursor, any MCP client) can price, create, check and refund Janus Pay payments with @janusprotocol/mcp. It holds no keys or money: a payment returns an address and an exact amount, and whoever pays sends it from their own wallet. Add it to the client's MCP settings:

MCP settings
{
  "mcpServers": {
    "janus-pay": {
      "command": "npx",
      "args": ["-y", "@janusprotocol/mcp"],
      "env": { "JANUS_API_KEY": "jpk_…", "JANUS_PARTNER": "your-id" }
    }
  }
}

Both values are optional; the key adds your payments list and refunds. Tools: janus_coins, janus_rate, janus_quote, janus_swap_quote, janus_checkout_link, janus_create_checkout, janus_payment_status, janus_my_payments, janus_refund.

Errors

Every error from /pay/* is JSON with a human detail and a stable code to branch on (the SDK throws a JanusError with both, plus retryAfter on a 429). A missing or malformed field is a 422 with code: "invalid_request" and the field named in errors.

codeMeans
amount_too_small · amount_too_largeUnder 10,000 sat, or more than the pool can take in that coin right now.
unsupported_coin · unsupported_currency · coin_pausedNot a coin or currency Janus takes, or paused for maintenance.
invalid_address · address_not_registeredNot a valid address; or, with a partner id, not one of its payout addresses.
price_impact_not_acceptedA large payment moves the price; the customer has to accept it first.
invalid_request · invalid_metadata · invalid_order_refSomething in the request is wrong; detail says what.
not_found · forbidden · unauthorizedNo such payment; not your secret; no or bad API key.
rate_limited · unavailableSlow down (see Retry-After); or a price or pool isn't available for a moment: try again.

Versions & npm

Go-live checklist

Support

For a question, or anything that looks wrong, message @swapnowbish on X. Include your partner id and, for a payment, its id. Service status: janusprotocol.xyz/status.html.