HTTP 402

The Missing Status Code
April 2026 · melvin.me · Part 3 of Solid Articles
402
Payment Required — reserved for future use since 1997

1. Twenty-Nine Years

When HTTP/1.1 was published in 1997, it included a status code for payments. 402 Payment Required. The spec said it was "reserved for future use." The web got 200, 301, 404, 500 — but the money code was left empty, waiting for someone to fill it in.

Meanwhile, the web built an entire payment infrastructure around HTTP. Stripe. PayPal. App stores taking 30%. Cookie-based subscriptions. Login walls. Every solution required a middleman, an account, and trust in a third party.

The status code was always there. The web just never had the pieces to use it. Now it does:

PieceWhat It DoesFrom
Solid podHosts content with fine-grained access controlPart 1
Nostr keyCryptographic identity + payment addressPart 2
PaymentConditionOne field in an ACL — "pay to read"This article
Testnet satsFree Bitcoin test tokens for developmentFaucets

2. One Line in an ACL

In Part 1 you learned that access control is just a JSON file. Public, private, per-user — all controlled by .acl files. A PaymentCondition is one more field in that same file:

{
  "@context": {"acl": "http://www.w3.org/ns/auth/acl#"},
  "@type": "acl:Authorization",
  "acl:agentClass": {"@id": "acl:AuthenticatedAgent"},
  "acl:accessTo": {"@id": "/premium/article.json"},
  "acl:mode": [{"@id": "acl:Read"}],
  "acl:condition": {
    "@type": "PaymentCondition",
    "amount": "0",
    "currency": "tbtc4"
  }
}

That's a membership gate. amount: "0" means: don't charge anything, just verify the reader has deposited sats at some point. Prove you're a paying customer. No charge per read.

When someone requests this resource:

ScenarioResponse
No authentication401 Unauthorized
Authenticated, never deposited402 Payment Required
Authenticated, has deposited200 OK + content

3. Set It Up

Start JSS with payments enabled (you need --pay):

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

Create Alice's pod if you haven't already:

curl -s -X POST http://localhost:4443/.pods \
  -H "Content-Type: application/json" \
  -d '{"name": "alice"}' | jq .
TOKEN="paste-your-token-here"

Write a premium article:

curl -X PUT http://localhost:4443/alice/public/premium.json \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/ld+json" \
  -d '{
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "The Future of Web Payments",
    "author": "Alice",
    "articleBody": "This article is behind a membership gate. If you can read this, you have deposited sats."
  }'

Now lock it with a PaymentCondition:

curl -X PUT http://localhost:4443/alice/public/premium.json.acl \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/ld+json" \
  -d '[
    {
      "@context": {"acl": "http://www.w3.org/ns/auth/acl#"},
      "@id": "#owner",
      "@type": "acl:Authorization",
      "acl:agent": {"@id": "http://localhost:4443/alice/profile/card#me"},
      "acl:accessTo": {"@id": "http://localhost:4443/alice/public/premium.json"},
      "acl:mode": [{"@id": "acl:Read"}, {"@id": "acl:Write"}, {"@id": "acl:Control"}]
    },
    {
      "@context": {"acl": "http://www.w3.org/ns/auth/acl#"},
      "@id": "#members",
      "@type": "acl:Authorization",
      "acl:agentClass": {"@id": "acl:AuthenticatedAgent"},
      "acl:accessTo": {"@id": "http://localhost:4443/alice/public/premium.json"},
      "acl:mode": [{"@id": "acl:Read"}],
      "acl:condition": {
        "@type": "PaymentCondition",
        "amount": "0",
        "currency": "tbtc4"
      }
    }
  ]'

4. The 402 Moment

Now try to read it. You need a Nostr key (from Part 2). Generate a NIP-98 auth header and request the article:

curl -H "Authorization: Nostr $NIP98_TOKEN" \
  http://localhost:4443/alice/public/premium.json
402 Payment Required
{
  "type": "PaymentRequired",
  "amount": "0",
  "currency": "tbtc4"
}

There it is. The status code that waited twenty-nine years. The server checked your identity, found you in the ACL, saw the PaymentCondition, checked the ledger — and you have no balance. 402.

5. One Key, Four Functions

Here's the fun part. Your Nostr key is a secp256k1 keypair. The same curve Bitcoin uses. That means your Nostr public key is also a valid taproot Bitcoin address.

FunctionSame Key
Sign Nostr events
Authenticate to a Solid pod (did:nostr)
Receive Bitcoin (taproot address)
Hold a balance in a webledger

To pass the membership gate, you need to deposit testnet sats. Testnet4 is Bitcoin's test network — the sats are free, the protocol is identical to mainnet. JSS uses it by default for development.

Get testnet4 sats from a faucet. The simplest way to deposit: get your personal deposit address and send sats directly from any wallet:

6. Deposit

curl http://localhost:4443/pay/.address?chain=tbtc4
# {"address": "tb1p...", "chain": "tbtc4"}

Send testnet sats to that address. Then check your balance — the pod detects the deposit automatically:

curl -H "Authorization: Nostr $NIP98_TOKEN" \
  http://localhost:4443/pay/.balance

Alternatively, deposit a TXO URI directly if you have one (the format is txo:chain:txid:vout):

curl -X POST http://localhost:4443/pay/.deposit \
  -H "Authorization: Nostr $NIP98_TOKEN" \
  -d "txo:tbtc4:abc123def456...789:0"

Either way, the server verifies the transaction on the testnet4 mempool API and credits your did:nostr in the webledger:

{
  "did": "did:nostr:5e7617...45af",
  "deposited": 5000,
  "balance": 5000,
  "unit": "sat",
  "chain": "tbtc4"
}

Your key now has a balance. The same key you used to authenticate.

7. Try Again

curl -H "Authorization: Nostr $NIP98_TOKEN" \
  http://localhost:4443/alice/public/premium.json
200 OK
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "The Future of Web Payments",
  "author": "Alice",
  "articleBody": "This article is behind a membership gate. If you can read this, you have deposited sats."
}

You're in. The server checked your identity, found the PaymentCondition, looked up your did:nostr in the webledger, confirmed you have an entry, and served the content. No sats were deducted — the gate is free. You just proved you're a member.

What just happened

A twenty-nine-year-old HTTP status code, a W3C access control spec, a Nostr cryptographic key, and a Bitcoin testnet transaction came together to create a membership gate. No Stripe. No PayPal. No app store. No email. No account. Just HTTP, cryptography, and a few bytes of JSON.

The same system works on mainnet with real sats. Change tbtc4 to btc and the membership gate costs real money. The code is identical.

8. What This Enables

Use CaseHow
Members-only blogamount: "0" — deposit once, read forever
Anti-spam APIRequire a balance to make requests — spammers won't deposit
Premium contentamount: "100" — charge per read (Part 4)
Tiered accessDifferent amounts for different resources
Anonymous membershipNostr key + sats = identity without email

The PaymentCondition is just another condition in the Web Access Control spec. The same ACL can combine it with specific agents, authenticated users, or any other condition. The server evaluates them all — and if it encounters a condition type it doesn't understand, it denies access. Fail-closed by design.