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.
npm@janusprotocol/pay
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.
<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:
<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.
Checkout link: no code on your site
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:
https://janusprotocol.xyz/checkout.html?to=1YourBsvAddress…&price=29.74¤cy=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.
<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>
<janus-swap partner="your-id" name="Your Wallet" theme="light" accent="#7c5cff"></janus-swap>
| Attribute | Used by | What it does |
|---|---|---|
button | all | Show 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". |
outline | all | With button: the gradient (or your accent) as a border only, on a clear background in your page's text colour. |
coins | checkout | With button: list the coins it takes (with their logos) under the label, instead of "with Janus · any coin". |
demo | checkout | Try it without taking money: live prices, but the payment is simulated and no request reaches Janus. Take it off before you go live. |
partner | both | Optional. Your partner id: adds signed notices and limits payouts to your registered addresses. |
to | checkout | Your BSV address. No sign-up needed; if you use a partner id, it must be one of that partner's registered payout addresses. |
sat | checkout | What you're owed, in satoshis. You receive at least this. Minimum 10,000. |
price, currency | checkout | Or 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. |
logo | both | Your logo (an https:// image), shown at the top in place of your initial. |
radius | both | Corner radius in px, 0–24 (default 12), to match your site. |
name | both | Your name, shown at the top of the box. |
item | checkout | What's being bought, shown to the customer. |
ref | checkout | Your order reference. Comes back in every notice. Up to 64 of A-Z a-z 0-9 . _ : # / - |
assets | both | Limit the coins offered, e.g. SOL,HYPE. Default: every coin that's open. |
theme | both | dark or light. |
accent | both | Your brand colour, #rrggbb. Button text turns dark or white to stay readable on it. |
width | both | Width 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.
| Event | When |
|---|---|
janus:ready | The widget has loaded. |
janus:quote | A price was shown or refreshed. |
janus:payment.created | The customer started a payment. |
janus:payment.status | Its status changed (detail carries the payment). |
janus:payment.completed | Paid out. |
janus:payment.cancelled | The customer cancelled before paying. |
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+.
npm install @janusprotocol/pay // or, with no build step: import { JanusPay } from "https://janusprotocol.xyz/sdk/v1.js";
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));// 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 });
| Method | Returns |
|---|---|
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.
<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.
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.
| Call | What it does |
|---|---|
GET /pay/assets | Coins open right now. |
GET /pay/quote?to_sat=&pay_with=GET /pay/quote?price=¤cy=USD&pay_with= | Checkout price, for an amount in sat or a price in dollars. |
GET /pay/rate?currency=USD | What 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}/refund | With 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/create | Start 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-sent | A BSV checkout's wallet just paid. Body {"txid": "…", "raw": "…"} (raw optional). Checks it now instead of at the next scan; optional. |
POST /pay/{id}/cancel | Body {"secret": "…"}. Cancels it before payment. |
POST /pay/keys | Get a partner id, notice secret and API key. Body: name, payout_addresses, webhook_url (optional), site. Secrets shown once. |
GET /pay/me · POST /pay/me | With Authorization: Bearer jpk_…: your settings; change the notice URL, payout addresses or name, or rotate_secret. |
POST /pay/me/test-notice | Sends 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/buy | Start 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.
{ "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": "…" } }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); });
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", ""))- The signature checks out, with
twithin five minutes. typeispayment.completedandmodeischeckout.to_addressis yours.order_refis an order of yours, and the amount matches it in your own records:received_satcovers the order total in sat, or, if you price in dollars,priceandcurrencyare 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
idbefore. A notice can arrive more than once.
Payment statuses
| Status | Notice | Meaning |
|---|---|---|
| waiting | none | Created; nothing has arrived yet. |
| seen | payment.seen | The customer's coin has arrived; payout is under way. |
| completed | payment.completed | BSV paid out. received_sat and payout_txid are set. |
| underpaid | payment.underpaid | Paid out, but less than asked (the customer sent too little). |
| held | payment.held | Arrived but needs a person, e.g. a coin paused for maintenance. We sort it out. |
| failed | payment.failed | Couldn't be filled; the customer is refunded. |
| expired | payment.expired | Nothing arrived within 30 minutes. In BSV, a payment that arrives later the same day still counts: a payment.completed follows. |
How it stays safe
- Only your addresses. A checkout under your partner id can only pay the BSV addresses you registered, so nobody can open a "checkout" for your shop that pays themselves.
- The widget is sealed. It runs on our domain in an iframe. Payment secrets never leave it, and it can't read your page.
- Signed notices. Fulfil only from notices whose signature you've checked. Browser events are for your UI.
- Exact quotes. Into BSV, the amount shown is what's guaranteed. If the market moves too far before payment, the customer is asked to accept the new price or is refunded.
- No custody on your side. You never hold a customer's coins or keys, and neither does the SDK.
Coins & limits
| What | Value |
|---|---|
| Customers pay with | BSV (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 receive | BSV, at your address. |
| Prices | In satoshis, or in USD (converted at Janus's BSV price and locked for the payment). |
| Smallest checkout | 10,000 sat. |
| Largest checkout | In 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 lasts | 30 minutes. |
| Rate limits | Per 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. |
| Fees | Paid 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:
{
"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.
| code | Means |
|---|---|
amount_too_small · amount_too_large | Under 10,000 sat, or more than the pool can take in that coin right now. |
unsupported_coin · unsupported_currency · coin_paused | Not a coin or currency Janus takes, or paused for maintenance. |
invalid_address · address_not_registered | Not a valid address; or, with a partner id, not one of its payout addresses. |
price_impact_not_accepted | A large payment moves the price; the customer has to accept it first. |
invalid_request · invalid_metadata · invalid_order_ref | Something in the request is wrong; detail says what. |
not_found · forbidden · unauthorized | No such payment; not your secret; no or bad API key. |
rate_limited · unavailable | Slow down (see Retry-After); or a price or pool isn't available for a moment: try again. |
Versions & npm
https://janusprotocol.xyz/sdk/v1.jsis always the newest 1.x (no breaking changes within v1). To pin a version, use/sdk/v1.3.0.js: a published version never changes, so you can add an integrity hash.- Types:
/sdk/v1.d.ts, also in the npm package. - npm:
npm install @janusprotocol/pay. Every version published to npm is the same file as the matching/sdk/vX.Y.Z.js. - New coins and bigger pools don't need a new version: the widget and SDK read coins, limits and prices from the API.
Go-live checklist
- The
toaddress is yours, and your app marks an order paid when at least its total arrives there (check the amount, not the browser event). - With a partner id: your notice secret is on your server only, your notice URL is
https://, answers 2xx quickly and checks the signature, and orders are marked paid only frompayment.completednotices, each payment id once. - You've made one small real payment end to end and seen the BSV arrive.
- You've tried a cancelled payment and an expired one, and your shop handles both.
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.