DEV Community

Cover image for x402 Protocol in FIFO Concert
Andrei Merlescu
Andrei Merlescu

Posted on

x402 Protocol in FIFO Concert

I'm telling you about a free open source reverse proxy that I built and why I added support for crypto's x402 protocol. This post isn't about crypto, but crypto is in the post. This is not financial advice and I'm not shilling anything. If you have been burned by crypto, you are like me then. If you are a builder of crypto things, maybe I have some choice words for you. But one thing that I know for absolutely certain, x402 protocol in the FIFO concert is a game changer and I want to explain to you why.

That being said, lets discuss what I built. It's called concert and it sits between my DNS A record for @ and www and my httpd process serving <VirtualHost *:3000></VirtualHost> for www.i18n.software. Concert is completely free and I wrote it in Go.

When configuring concert, I can specify $XRP, $HBAR, $XLM or $SOL fees for access to my website.

I can also configure concert to grant based on NFT ownership.

When using x402 on any concert instance, you'll may need a concert-client helper to assist you.

// Command concert-client is the reference automated wallet for Concert's
// fast lane. Payer keys stay in this process; Concert and its gateway never
// see them.
//
//  concert-client          pay for a fast-lane pass (CONCERT_ACCEPT_TERMS=yes)
//  concert-client --nft    prove NFT ownership for a short lease
//
// A signed authorization is saved to the state file before it is sent, and
// the client never signs a second one while the first is unresolved.
package main

import (
    "bytes"
    "context"
    "crypto/ed25519"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "log"
    "math/big"
    "net/http"
    "net/url"
    "os"
    "path/filepath"
    "runtime"
    "strings"
    "time"

    "github.com/andreimerlescu/concert/internal/chain/hedera"
    "github.com/andreimerlescu/concert/internal/chain/solana"
    "github.com/andreimerlescu/concert/internal/chain/stellar"
    "github.com/andreimerlescu/concert/internal/chain/xrpl"
    "github.com/andreimerlescu/concert/internal/gateway"
    "github.com/andreimerlescu/concert/internal/x402"
    hiero "github.com/hiero-ledger/hiero-sdk-go/v2/sdk"
    "github.com/stellar/go-stellar-sdk/keypair"
)

// wallet signs for one network.
type wallet struct {
    address string
    pay     func(context.Context, x402.Requirements) (json.RawMessage, error)
    sign    func(message string) (signature []byte, publicKey string)
}

func family(network string) string { f, _, _ := strings.Cut(network, ":"); return f }

func payload(key, value string) json.RawMessage {
    b, _ := json.Marshal(map[string]string{key: value})
    return b
}

func walletFromEnvironment(network string, n gateway.Network, getenv func(string) string) (*wallet, error) {
    switch family(network) {
    case "xrpl":
        key, err := xrpl.KeyFromSeed(getenv("CONCERT_CLIENT_XRP_SEED"))
        if err != nil {
            return nil, err
        }
        l := xrpl.RPC{URL: n.RPC}
        return &wallet{
            address: key.Address,
            pay: func(ctx context.Context, r x402.Requirements) (json.RawMessage, error) {
                blob, err := xrpl.NewPaymentBlob(ctx, l, key, r)
                return payload("signedTxBlob", blob), err
            },
            sign: func(m string) ([]byte, string) {
                return key.Sign([]byte(m)), strings.ToUpper(hex.EncodeToString(key.Public))
            },
        }, nil
    case "stellar":
        kp, err := keypair.ParseFull(getenv("CONCERT_CLIENT_XLM_SECRET"))
        if err != nil {
            return nil, errors.New("invalid CONCERT_CLIENT_XLM_SECRET")
        }
        rpc, horizon := stellar.RPC{URL: n.RPC}, stellar.Horizon{URL: n.Horizon}
        return &wallet{
            address: kp.Address(),
            pay: func(ctx context.Context, r x402.Requirements) (json.RawMessage, error) {
                want, err := stellar.Passphrase(network)
                if err != nil {
                    return nil, err
                }
                if got, err := rpc.Passphrase(ctx); err != nil || got != want {
                    return nil, errors.New("RPC network mismatch")
                }
                tx, err := stellar.NewPayment(ctx, rpc, horizon, network, kp, r)
                return payload("transaction", tx), err
            },
            sign: func(m string) ([]byte, string) {
                sig, _ := kp.Sign(stellar.SEP53Hash(m))
                return sig, ""
            },
        }, nil
    case "hedera":
        key, err := hiero.PrivateKeyFromStringDer(getenv("CONCERT_CLIENT_HBAR_KEY"))
        if err != nil {
            return nil, errors.New("invalid CONCERT_CLIENT_HBAR_KEY")
        }
        account := getenv("CONCERT_CLIENT_HBAR_ACCOUNT")
        if _, err := hiero.AccountIDFromString(account); err != nil {
            return nil, errors.New("invalid CONCERT_CLIENT_HBAR_ACCOUNT")
        }
        mirror := hedera.Mirror{URL: n.Mirror}
        return &wallet{
            address: account,
            pay: func(ctx context.Context, r x402.Requirements) (json.RawMessage, error) {
                tx, err := hedera.NewPayment(ctx, mirror, account, key, r)
                return payload("transaction", tx), err
            },
            sign: func(m string) ([]byte, string) { return key.Sign([]byte(m)), "" },
        }, nil
    case "solana":
        raw, err := base64.StdEncoding.DecodeString(getenv("CONCERT_CLIENT_SOL_KEY"))
        if err != nil || len(raw) != ed25519.PrivateKeySize {
            return nil, errors.New("CONCERT_CLIENT_SOL_KEY must be a base64 64-byte secret key")
        }
        priv := ed25519.PrivateKey(raw)
        var from solana.PublicKey
        copy(from[:], priv.Public().(ed25519.PublicKey))
        rpc := solana.RPC{URL: n.RPC}
        return &wallet{
            address: from.String(),
            pay: func(ctx context.Context, r x402.Requirements) (json.RawMessage, error) {
                if got, err := rpc.Network(ctx); err != nil || got != network {
                    return nil, errors.New("RPC network mismatch")
                }
                to, err := solana.ParsePublicKey(r.PayTo)
                if err != nil {
                    return nil, err
                }
                lamports, ok := new(big.Int).SetString(r.Amount, 10)
                if !ok || !lamports.IsUint64() || lamports.Sign() <= 0 {
                    return nil, errors.New("invalid amount")
                }
                bh, err := rpc.LatestBlockhash(ctx)
                if err != nil {
                    return nil, err
                }
                tx := solana.Sign(solana.NewTransfer(from, to, lamports.Uint64(), bh), priv)
                return payload("transaction", base64.StdEncoding.EncodeToString(tx)), nil
            },
            sign: func(m string) ([]byte, string) { return ed25519.Sign(priv, []byte(m)), "" },
        }, nil
    }
    return nil, errors.New("unsupported network")
}

// client talks to one Concert origin.
type client struct {
    origin string
    http   *http.Client
}

func newClient(origin string) *client {
    return &client{origin: origin, http: &http.Client{
        Timeout:       55 * time.Second,
        CheckRedirect: func(*http.Request, []*http.Request) error { return errors.New("redirects are refused") },
    }}
}

func (c *client) request(ctx context.Context, path, session string, body any, headers map[string]string, out any) (int, error) {
    var rd *bytes.Reader
    method := http.MethodGet
    if body != nil {
        b, err := json.Marshal(body)
        if err != nil {
            return 0, err
        }
        rd, method = bytes.NewReader(b), http.MethodPost
    } else {
        rd = bytes.NewReader(nil)
    }
    req, err := http.NewRequestWithContext(ctx, method, c.origin+"/_concert"+path, rd)
    if err != nil {
        return 0, err
    }
    req.Header.Set("Content-Type", "application/json")
    if session != "" {
        req.Header.Set("Concert-Session", session)
    }
    for k, v := range headers {
        req.Header.Set(k, v)
    }
    res, err := c.http.Do(req)
    if err != nil {
        return 0, err
    }
    defer res.Body.Close()
    d := json.NewDecoder(io.LimitReader(res.Body, 1<<20))
    d.UseNumber()
    if err := d.Decode(out); err != nil {
        return res.StatusCode, fmt.Errorf("unexpected response (%d)", res.StatusCode)
    }
    return res.StatusCode, nil
}

type failure struct {
    Error string `json:"error"`
}

func (f failure) err(fallback string) error {
    if f.Error != "" {
        return errors.New(f.Error)
    }
    return errors.New(fallback)
}

var nativeAssets = map[string]string{"xrpl": "XRP", "hedera": "0.0.0", "solana": "SOL"}

// preparePayment signs one authorization for the offer on network, only if
// it pays expectedPayTo in the native currency within the budget. It never
// picks another offer or signs a replacement.
func (c *client) preparePayment(ctx context.Context, session, network string, w *wallet, maxAtomic, expectedPayTo string) (string, error) {
    var disc struct {
        failure
        Resource *x402.Resource      `json:"resource"`
        Accepts  []x402.Requirements `json:"accepts"`
    }
    status, err := c.request(ctx, "/payment", session, map[string]any{}, nil, &disc)
    if err != nil {
        return "", err
    }
    if status != http.StatusPaymentRequired {
        if status < 300 {
            return "", errors.New("session already has access")
        }
        return "", disc.err("payment discovery failed")
    }
    var r *x402.Requirements
    for i := range disc.Accepts {
        if disc.Accepts[i].Network == network {
            r = &disc.Accepts[i]
            break
        }
    }
    budget, okBudget := new(big.Int).SetString(maxAtomic, 10)
    var amount *big.Int
    okAmount := false
    if r != nil {
        amount, okAmount = new(big.Int).SetString(r.Amount, 10)
    }
    if r == nil || !okBudget || !okAmount || expectedPayTo == "" || r.PayTo != expectedPayTo || amount.Cmp(budget) > 0 || amount.Sign() <= 0 {
        return "", errors.New("network, recipient or spending budget mismatch")
    }
    asset, scheme := nativeAssets[family(network)], "exact"
    if family(network) == "stellar" {
        pass, err := stellar.Passphrase(network)
        if err != nil {
            return "", err
        }
        if asset, err = stellar.NativeAsset(pass); err != nil {
            return "", err
        }
    }
    if family(network) == "solana" {
        scheme = solana.SchemeName
    }
    if r.Asset != asset || r.Scheme != scheme {
        return "", errors.New("only the configured native currency scheme is supported")
    }
    proof, err := w.pay(ctx, *r)
    if err != nil {
        return "", err
    }
    b, err := json.Marshal(x402.Payload{Version: 2, Resource: disc.Resource, Accepted: *r, Payload: proof})
    if err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(b), nil
}

type state struct {
    Origin  string `json:"origin"`
    Network string `json:"network"`
    Session string `json:"session,omitempty"`
    Payment string `json:"payment,omitempty"`
}

// saveState atomically replaces file: a crash leaves the previous state or
// the complete new one, never a torn file.
func saveState(file string, s state) error {
    b, err := json.MarshalIndent(s, "", "  ")
    if err != nil {
        return err
    }
    dir := filepath.Dir(file)
    if err := os.MkdirAll(dir, 0o700); err != nil {
        return err
    }
    tmp := file + ".tmp"
    f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
    if err != nil {
        return err
    }
    if _, err := f.Write(b); err != nil {
        f.Close()
        return err
    }
    if err := f.Sync(); err != nil {
        f.Close()
        return err
    }
    if err := f.Close(); err != nil {
        return err
    }
    if err := os.Rename(tmp, file); err != nil {
        return err
    }
    if runtime.GOOS == "windows" {
        return nil
    }
    d, err := os.Open(dir)
    if err != nil {
        return err
    }
    defer d.Close()
    return d.Sync()
}

func loadState(file, origin, network string) (state, error) {
    b, err := os.ReadFile(file)
    if os.IsNotExist(err) {
        return state{Origin: origin, Network: network}, nil
    }
    if err != nil {
        return state{}, err
    }
    var s state
    if err := json.Unmarshal(b, &s); err != nil {
        return state{}, err
    }
    if s.Origin != origin || s.Network != network {
        return state{}, errors.New("state file belongs to another origin/network")
    }
    return s, nil
}

func sessionID(token string) string { id, _, _ := strings.Cut(token, "."); return id }

func checkOrigin(origin string) error {
    u, err := url.Parse(origin)
    if err != nil || u.Host == "" {
        return errors.New("invalid CONCERT_ORIGIN")
    }
    if u.Scheme == "https" {
        return nil
    }
    switch u.Hostname() {
    case "localhost", "127.0.0.1", "::1":
        if u.Scheme == "http" {
            return nil
        }
    }
    return errors.New("HTTPS is required")
}

func env(name, fallback string) string {
    if v := os.Getenv(name); v != "" {
        return v
    }
    return fallback
}

func main() {
    log.SetFlags(0)
    if err := run(context.Background(), os.Args[1:], os.Getenv); err != nil {
        log.Fatal(err)
    }
}

func run(ctx context.Context, args []string, getenv func(string) string) error {
    nft := false
    for _, a := range args {
        if a != "--nft" {
            return errors.New("usage: concert-client [--nft]")
        }
        nft = true
    }
    origin := strings.TrimRight(env("CONCERT_ORIGIN", "http://127.0.0.1:8080"), "/")
    if err := checkOrigin(origin); err != nil {
        return err
    }
    network := getenv("CONCERT_CLIENT_NETWORK")
    if network == "" {
        return errors.New("set CONCERT_CLIENT_NETWORK")
    }
    networks, err := gateway.LoadNetworks(env("CONCERT_NETWORKS_CONFIG", "examples/networks.json"))
    if err != nil {
        return err
    }
    n, ok := networks[network]
    if !ok {
        return errors.New("network configuration is missing")
    }
    file, err := filepath.Abs(env("CONCERT_CLIENT_STATE", "client-state.json"))
    if err != nil {
        return err
    }
    if err := os.MkdirAll(filepath.Dir(file), 0o700); err != nil {
        return err
    }
    lock, err := os.OpenFile(file+".lock", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
    if err != nil {
        return errors.New("client state is locked; ensure no client is running before removing a stale .lock file")
    }
    defer func() { lock.Close(); os.Remove(file + ".lock") }()

    st, err := loadState(file, origin, network)
    if err != nil {
        return err
    }
    c := newClient(origin)
    w, err := walletFromEnvironment(network, n, getenv)
    if err != nil {
        return err
    }
    if err := c.renew(ctx, file, &st); err != nil {
        return err
    }
    var out any
    if nft {
        out, err = c.proveNFT(ctx, st.Session, w, getenv("CONCERT_NFT_RULE"), getenv("CONCERT_NFT_TOKEN"))
    } else {
        out, err = c.pay(ctx, file, &st, network, w, getenv)
    }
    if err != nil {
        return err
    }
    b, _ := json.MarshalIndent(out, "", "  ")
    fmt.Println(string(b))
    fmt.Println("Session saved. Send it as the Concert-Session header on protected requests.")
    return nil
}

// renew keeps the session ID, and with it any unresolved payment, while
// refreshing the expiry. An expired session yields a new ID instead.
func (c *client) renew(ctx context.Context, file string, st *state) error {
    var s struct {
        failure
        Session string `json:"session"`
    }
    status, err := c.request(ctx, "/session", st.Session, map[string]any{}, nil, &s)
    if err != nil {
        return err
    }
    if status != http.StatusOK || s.Session == "" {
        return s.err("session failed")
    }
    if st.Session != "" && st.Payment != "" && sessionID(s.Session) != sessionID(st.Session) {
        return errors.New("the saved session expired before its payment was resolved; keep this state file and contact the merchant; do not authorize a new payment")
    }
    st.Session = s.Session
    return saveState(file, *st)
}

func (c *client) pay(ctx context.Context, file string, st *state, network string, w *wallet, getenv func(string) string) (any, error) {
    if st.Payment == "" {
        if getenv("CONCERT_ACCEPT_TERMS") != "yes" {
            return nil, errors.New("review merchant terms and set CONCERT_ACCEPT_TERMS=yes")
        }
        p, err := c.preparePayment(ctx, st.Session, network, w, getenv("CONCERT_MAX_ATOMIC_AMOUNT"), getenv("CONCERT_EXPECT_PAY_TO"))
        if err != nil {
            return nil, err
        }
        st.Payment = p
        if err := saveState(file, *st); err != nil {
            return nil, err
        }
    }
    var out map[string]any
    status, err := c.request(ctx, "/payment", st.Session, map[string]any{}, map[string]string{"PAYMENT-SIGNATURE": st.Payment}, &out)
    if err != nil {
        return nil, fmt.Errorf("%w; retain this state file and do not authorize a new payment", err)
    }
    if status != http.StatusOK {
        msg, _ := out["error"].(string)
        return nil, fmt.Errorf("%s; retain this state file and do not authorize a new payment", or(msg, "payment failed"))
    }
    return out, nil
}

func (c *client) proveNFT(ctx context.Context, session string, w *wallet, rule, token string) (any, error) {
    var ch struct {
        failure
        Nonce   string `json:"nonce"`
        Message string `json:"message"`
    }
    status, err := c.request(ctx, "/nft/challenge", session, map[string]string{"rule": rule, "address": w.address, "token_id": token}, nil, &ch)
    if err != nil {
        return nil, err
    }
    if status != http.StatusOK {
        return nil, ch.err("challenge failed")
    }
    sig, pub := w.sign(ch.Message)
    var out map[string]any
    status, err = c.request(ctx, "/nft/verify", session, map[string]string{"nonce": ch.Nonce, "signature": base64.StdEncoding.EncodeToString(sig), "public_key": pub}, nil, &out)
    if err != nil {
        return nil, err
    }
    if status != http.StatusOK {
        msg, _ := out["error"].(string)
        return nil, errors.New(or(msg, "NFT proof failed"))
    }
    return out, nil
}

func or(a, b string) string {
    if a != "" {
        return a
    }
    return b
}
Enter fullscreen mode Exit fullscreen mode

I felt like sharing this with you. The code is up on Github if you want to check it out.

https://github.com/andreimerlescu/concert

implements a reverse proxy of: https://github.com/andreimerlescu/room
concurrency package: https://github.com/andreimerlescu/sema
load testing app: https://github.com/andreimerlescu/lemmings

When using concert, a fast lane will be created and visitors waiting for admission into your website will be able to see their crypto related information needed.

Why

I opened this piece with, this isn't financial advice and yes that applies still. But I also mentioned getting burned by crypto. This product, concert is free because x402 has the potential to help website owners earn extra income using crypto, but also, the support for NFT protected endpoints means that perhaps some of those NFT collections can be put to good use.

Nowhere in the concert does it require YOU be the issuer of the NFT collection that your website requires admission into. For example, let's say you wanted to use a very prestigious NFT collection to gate access to your website by granting them priority lane access, means that the NFT collection has some real world value to it. Far beyond its digits. With concert, you can actually do something with your NFTs now.

The best is yet to come. I truly believe that what technology innovates in the next 10 years will make our parents' expectations of science fiction seem closer to reality than ever.

Top comments (0)