Skip to main content

HTTP 402 Payments

JSS includes a built-in payment system. Resources under /pay/* cost satoshis to access. Users authenticate with a Nostr key (NIP-98), deposit sats from a Bitcoin UTXO, and spend them on API requests. Optionally, the pod can mint its own token for buying, selling, and trading.

Quick Start

jss start --pay --pay-cost 1

Put a resource behind the paywall:

curl -X PUT http://localhost:3000/pay/hello.json \
-H "Content-Type: application/json" \
-d '{"message": "Hello from the paid zone"}'

Now accessing GET /pay/hello.json requires NIP-98 authentication and 1 sat of balance.

Configuration

FlagDescriptionDefault
--payEnable HTTP 402 for /pay/* routesfalse
--pay-cost <n>Cost per request in satoshis1
--pay-mempool-url <url>Mempool API URL for deposit verificationtestnet4
--pay-address <addr>Address for receiving MRC20 token deposits-
--pay-token <ticker>Token to sell (enables buy/withdraw/sell/swap)-
--pay-rate <n>Sats per token for buy/withdraw1
--pay-chains <ids>Multi-chain deposits + AMM (e.g. "tbtc3,tbtc4")-

Environment Variables

export JSS_PAY=true
export JSS_PAY_COST=1
export JSS_PAY_MEMPOOL_URL=https://mempool.space/testnet4
export JSS_PAY_ADDRESS=tb1q...
export JSS_PAY_TOKEN=PODS
export JSS_PAY_RATE=10
export JSS_PAY_CHAINS=tbtc3,tbtc4

API Endpoints

Public (no auth)

MethodPathDescription
GET/pay/.infoPayment config: cost, token info, chains, pool
GET/pay/.offersOpen sell orders (secondary market)
GET/pay/.poolAMM pool state (requires --pay-chains)

Authenticated (NIP-98)

MethodPathDescription
GET/pay/.balanceCheck your balance (includes per-chain balances)
POST/pay/.depositDeposit sats (TXO URI) or MRC20 tokens
POST/pay/.buyBuy tokens with sat balance
POST/pay/.withdrawWithdraw balance as portable tokens
POST/pay/.sellCreate a sell order
POST/pay/.swapExecute a swap against a sell order
POST/pay/.poolAMM: swap, add-liquidity, remove-liquidity
GET/pay/*Access a paid resource (deducts balance)

How It Works

1. Discover

curl https://example.com/pay/.info
{
"cost": 1,
"unit": "sat",
"deposit": "/pay/.deposit",
"balance": "/pay/.balance",
"token": {
"ticker": "PODS",
"rate": 10,
"buy": "/pay/.buy",
"withdraw": "/pay/.withdraw"
}
}

2. Authenticate

All authenticated requests use NIP-98 — a Nostr event (kind 27235) signed with your private key:

Authorization: Nostr <base64-encoded-event>

The event must include tags ["u", "<request-url>"] and ["method", "<HTTP-METHOD>"].

3. Deposit

Post a confirmed Bitcoin transaction output to credit your balance:

curl -X POST -H "Authorization: Nostr <token>" \
https://example.com/pay/.deposit \
-d "txid:vout"

The server verifies the UTXO via the mempool API and credits the output value to your balance.

4. Access Resources

curl -H "Authorization: Nostr <token>" \
https://example.com/pay/my-resource.json

Each request deducts the configured cost. Response includes X-Balance and X-Cost headers. If your balance is too low, you get a 402 Payment Required response:

{
"error": "Payment Required",
"balance": 0,
"cost": 1,
"deposit": "/pay/.deposit"
}

Token Economy

When --pay-token is configured, the pod mints its own MRC20 token anchored to Bitcoin via blocktrails key chaining.

Mint a Token (CLI)

jss token mint --ticker PODS --supply 10000 \
--voucher "txo:btc:<txid>:<vout>?amount=<sats>&key=<hex>"

This creates a genesis MRC20 state, derives a taproot address via BIP-341 key chaining, and broadcasts a Bitcoin transaction anchoring the token.

Primary Market

Users buy tokens from the pod at the configured rate:

curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
https://example.com/pay/.buy \
-d '{"amount": 100}'

The pod deducts sats from the buyer's balance, advances the MRC20 trail on Bitcoin, and returns a portable proof:

{
"bought": 100,
"ticker": "PODS",
"cost": 1000,
"rate": 10,
"txid": "c3183f41...",
"proof": {
"state": { "..." },
"prevState": { "..." },
"anchor": {
"pubkey": "025e60b6...",
"stateStrings": ["..."],
"network": "testnet4"
}
}
}

The proof is independently verifiable — anyone can derive the expected taproot address from the pubkey + state chain and check the Bitcoin UTXO.

Withdrawal

Convert your balance back to portable tokens:

# Withdraw specific amount
curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
https://example.com/pay/.withdraw \
-d '{"tokens": 50}'

# Drain entire balance
curl -X POST ... -d '{"all": true}'

Secondary Market

Users can trade tokens with each other through the pod:

List tokens for sale:

curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
https://example.com/pay/.sell \
-d '{"amount": 100, "price": 1500}'

Browse offers:

curl https://example.com/pay/.offers

Execute a swap:

curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
https://example.com/pay/.swap \
-d '{"id": "<offer-uuid>"}'

The pod transfers tokens from seller to buyer on the Bitcoin trail, debits the buyer's sats, and credits the seller's sats.

Multi-Chain AMM

When --pay-chains is configured with two or more chain IDs, the pod enables multi-chain deposits and an automated market maker.

jss start --pay --pay-chains "tbtc3,tbtc4"

Supported Chains

Chain IDNetworkExplorer
btcBitcoin mainnetmempool.space
tbtc3Bitcoin Testnet3mempool.space/testnet
tbtc4Bitcoin Testnet4mempool.space/testnet4
ltcLitecoinlitecoinspace.org
signetBitcoin Signetmempool.space/signet

Multi-Chain Deposits

Deposits detect the chain from the TXO URI prefix:

# Deposit testnet3 sats
curl -X POST -H "Authorization: Nostr <token>" \
https://example.com/pay/.deposit \
-d "txo:tbtc3:<txid>:<vout>"

# Deposit testnet4 sats
curl -X POST -H "Authorization: Nostr <token>" \
https://example.com/pay/.deposit \
-d "txo:tbtc4:<txid>:<vout>"

Each chain's balance is tracked separately in the webledger using multi-currency format. The .balance endpoint returns per-chain balances:

{
"did": "did:nostr:<pubkey>",
"balance": 0,
"balances": { "tbtc3": 10000, "tbtc4": 50000 }
}

AMM Pool

The pool uses a constant-product formula (x × y = k) with a 0.3% fee on swaps.

Add liquidity:

curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
https://example.com/pay/.pool \
-d '{"action": "add-liquidity", "tbtc3": 1000, "tbtc4": 5000}'

Swap:

curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
https://example.com/pay/.pool \
-d '{"action": "swap", "sell": "tbtc3", "amount": 100, "minReceived": 400}'

Remove liquidity:

curl -X POST -H "Authorization: Nostr <token>" \
-H "Content-Type: application/json" \
https://example.com/pay/.pool \
-d '{"action": "remove-liquidity", "shares": 50}'

Pool state (public):

curl https://example.com/pay/.pool
{
"pair": ["tbtc3", "tbtc4"],
"reserves": { "tbtc3": 10000, "tbtc4": 50000 },
"k": 500000000,
"fee": 0.003,
"totalShares": 22360,
"lpShares": { "did:nostr:<pubkey>": 22360 }
}

Use Cases

  • Paid APIs — AI agents, data feeds, premium content, all metered per-request
  • Agent Economy — Agents self-provision access with a funded Bitcoin UTXO
  • Pod Monetization — Pod owners mint tokens and sell access
  • Portable Credits — Buy on one pod, withdraw, deposit on another
  • Micropayments — No Lightning channels, no payment processor, just Bitcoin UTXOs
  • Cross-chain trading — AMM pool between any two UTXO chains (e.g. testnet3 ↔ testnet4)
  • Liquidity provision — Earn 0.3% fees by providing liquidity to AMM pools

Balance Tracking

Balances are stored in a Web Ledger at /.well-known/webledgers/webledgers.json, mapping did:nostr:<pubkey> URIs to sat amounts.

Token Management (CLI)

# Mint
jss token mint --ticker PODS --supply 10000 --voucher <txo-uri>

# Transfer
jss token transfer --ticker PODS --to <pubkey> --amount 100

# Info
jss token info PODS