Class: Remitmd::X402Paywall

Inherits:
Object
  • Object
show all
Defined in:
lib/remitmd/x402_paywall.rb

Overview

x402 paywall for service providers - gate HTTP endpoints behind payments.

Providers use this class to:

  • Return HTTP 402 responses with properly formatted PAYMENT-REQUIRED headers
  • Verify incoming PAYMENT-SIGNATURE headers against the remit.md facilitator

Examples:

Rack middleware

paywall = Remitmd::X402Paywall.new(
  wallet_address: "0xYourProviderWallet",
  amount_usdc: 0.001,
  network: "eip155:84532",
  asset: "0x2d846325766921935f37d5b4478196d3ef93707c"
)
use paywall.rack_middleware

Instance Method Summary collapse

Constructor Details

#initialize(wallet_address:, amount_usdc:, network:, asset:, facilitator_url: "https://remit.md", facilitator_token: "", max_timeout_seconds: 60, resource: nil, description: nil, mime_type: nil) ⇒ X402Paywall

Returns a new instance of X402Paywall.

Parameters:

  • wallet_address (String)

    provider's checksummed Ethereum address (the payTo field)

  • amount_usdc (Float)

    price per request in USDC (e.g. 0.001)

  • network (String)

    CAIP-2 network string (e.g. "eip155:84532")

  • asset (String)

    USDC contract address on the target network

  • facilitator_url (String) (defaults to: "https://remit.md")

    base URL of the remit.md facilitator

  • facilitator_token (String) (defaults to: "")

    bearer JWT for authenticating calls to /api/v1/x402/verify

  • max_timeout_seconds (Integer) (defaults to: 60)

    how long the payment authorization remains valid

  • resource (String, nil) (defaults to: nil)

    V2 - URL or path of the resource being protected

  • description (String, nil) (defaults to: nil)

    V2 - human-readable description

  • mime_type (String, nil) (defaults to: nil)

    V2 - MIME type of the resource



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/remitmd/x402_paywall.rb', line 35

def initialize( # rubocop:disable Metrics/ParameterLists
  wallet_address:,
  amount_usdc:,
  network:,
  asset:,
  facilitator_url: "https://remit.md",
  facilitator_token: "",
  max_timeout_seconds: 60,
  resource: nil,
  description: nil,
  mime_type: nil
)
  @wallet_address      = wallet_address
  @amount_base_units   = (amount_usdc * 1_000_000).round.to_s
  @network             = network
  @asset               = asset
  @facilitator_url     = facilitator_url.chomp("/")
  @facilitator_token   = facilitator_token
  @max_timeout_seconds = max_timeout_seconds
  @resource            = resource
  @description         = description
  @mime_type           = mime_type
end

Instance Method Details

#check(payment_sig) ⇒ Hash

Check whether a PAYMENT-SIGNATURE header represents a valid payment. Calls the remit.md facilitator's /api/v1/x402/verify endpoint.

Parameters:

  • payment_sig (String, nil)

    the raw header value (base64 JSON), or nil if absent

Returns:

  • (Hash)

    { is_valid: true/false, invalid_reason: String or nil }



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/remitmd/x402_paywall.rb', line 81

def check(payment_sig)
  return { is_valid: false } unless payment_sig

  payment_payload = begin
    JSON.parse(Base64.decode64(payment_sig))
  rescue JSON::ParserError
    return { is_valid: false, invalid_reason: "INVALID_PAYLOAD" }
  end

  body = {
    paymentPayload:  payment_payload,
    paymentRequired: payment_required_object,
  }

  uri = URI("#{@facilitator_url}/api/v1/x402/verify")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == "https"
  http.read_timeout = 10

  req = Net::HTTP::Post.new(uri.path)
  req["Content-Type"] = "application/json"
  req["Authorization"] = "Bearer #{@facilitator_token}" unless @facilitator_token.empty?
  req.body = JSON.generate(body)

  begin
    resp = http.request(req)
    unless resp.is_a?(Net::HTTPSuccess)
      return { is_valid: false, invalid_reason: "FACILITATOR_ERROR" }
    end

    data = JSON.parse(resp.body)
  rescue StandardError
    return { is_valid: false, invalid_reason: "FACILITATOR_ERROR" }
  end

  {
    is_valid:       data["isValid"] == true,
    invalid_reason: data["invalidReason"],
  }
end

#payment_required_headerString

Return the base64-encoded JSON PAYMENT-REQUIRED header value.

Returns:

  • (String)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/remitmd/x402_paywall.rb', line 61

def payment_required_header
  payload = {
    scheme:            "exact",
    network:           @network,
    amount:            @amount_base_units,
    asset:             @asset,
    payTo:             @wallet_address,
    maxTimeoutSeconds: @max_timeout_seconds,
  }
  payload[:resource]    = @resource    if @resource
  payload[:description] = @description if @description
  payload[:mimeType]    = @mime_type   if @mime_type
  Base64.strict_encode64(JSON.generate(payload))
end

#rack_middlewareClass

Rack middleware adapter.

Examples:

use paywall.rack_middleware

Returns:

  • (Class)

    a Rack middleware class



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/remitmd/x402_paywall.rb', line 128

def rack_middleware
  paywall = self
  Class.new do
    define_method(:initialize) do |app|
      @app     = app
      @paywall = paywall
    end

    define_method(:call) do |env|
      payment_sig = env["HTTP_PAYMENT_SIGNATURE"]
      result = @paywall.check(payment_sig)

      unless result[:is_valid]
        headers = {
          "Content-Type"     => "application/json",
          "PAYMENT-REQUIRED" => @paywall.payment_required_header,
        }
        body = JSON.generate({
          error:         "Payment required",
          invalidReason: result[:invalid_reason],
        })
        return [402, headers, [body]]
      end

      @app.call(env)
    end
  end
end