x402-rails 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 411abe38109c401d64e94078fd5b69bb3937b78e7bb6b1518921897ca28ad904
4
+ data.tar.gz: 2fe5f74f481df1a9b2e74cbd268ad02033d5250f9624eeb7a4912742409a53a5
5
+ SHA512:
6
+ metadata.gz: f1bbcaee8f5a30a99fb857f9983bc6ec638d9a6f10c2651e14ab65e3bb68bd4087fac81c5c2db25b1cabd1c92209cb6754a82b369e9f76dbf21998a43cf303c3
7
+ data.tar.gz: ac5aa6785d6cf1a9284cd4ac776200199d7c53696ae462c3775e226218727be30cbc2acabd39329aec6d8ec6918805fd52b609cddd185262d159b46856817da8
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Quicknode, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # x402-rails
2
+
3
+ ![Coverage](./coverage/coverage.svg)
4
+
5
+ Accept instant blockchain micropayments in your Rails applications using the [x402 payment protocol](https://www.x402.org/).
6
+
7
+ Supports Base, avalanche, and other blockchain networks.
8
+
9
+ ## Features
10
+
11
+ - **1 line of code** to accept digital dollars (USDC)
12
+ - **No fees** on supported networks (Base)
13
+ - **~1 second** response times (optimistic mode)
14
+ - **$0.001 minimum** payment amounts
15
+ - **Optimistic & non-optimistic** settlement modes
16
+ - **Automatic settlement** after successful responses
17
+ - **Browser paywall** and API support
18
+ - **Rails 7.0+** compatible
19
+
20
+ ## Installation
21
+
22
+ Add to your Gemfile:
23
+
24
+ ```ruby
25
+ gem 'x402-rails'
26
+ ```
27
+
28
+ Then run:
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Configure the gem
37
+
38
+ Create `config/initializers/x402.rb`:
39
+
40
+ ```ruby
41
+ X402.configure do |config|
42
+ config.wallet_address = ENV['X402_WALLET_ADDRESS'] # Your recipient wallet
43
+ config.facilitator = "https://x402.org/facilitator"
44
+ config.chain = "base-sepolia" # or "base" for base mainnet
45
+ config.currency = "USDC"
46
+ config.optimistic = false # Forces to check for settlement before giving response.
47
+ end
48
+ ```
49
+
50
+ ### 2. Protect your endpoints
51
+
52
+ Use `x402_paywall` in any controller action:
53
+
54
+ ```ruby
55
+ class ApiController < ApplicationController
56
+ def weather
57
+ x402_paywall(amount: 0.001) # $0.001 in USD
58
+
59
+ render json: {
60
+ temperature: 72,
61
+ paid_by: request.env['x402.payment'][:payer]
62
+ }
63
+ end
64
+ end
65
+ ```
66
+
67
+ That's it! Your endpoint now requires payment.
68
+
69
+ ## Usage Patterns
70
+
71
+ ### Direct Method Call
72
+
73
+ Call `x402_paywall` in any action:
74
+
75
+ ```ruby
76
+ def show
77
+ x402_paywall(amount: 0.01)
78
+ # Action continues after payment verified
79
+ render json: @data
80
+ end
81
+ ```
82
+
83
+ ### Before Action Hook
84
+
85
+ Protect multiple actions:
86
+
87
+ ```ruby
88
+ class PremiumController < ApplicationController
89
+ before_action :require_payment, only: [:show, :index]
90
+
91
+ def show
92
+ # Payment already verified
93
+ render json: @premium_content
94
+ end
95
+
96
+ private
97
+
98
+ def require_payment
99
+ x402_paywall(amount: 0.001, chain: "base")
100
+ end
101
+ end
102
+ ```
103
+
104
+ ### Per-Action Pricing
105
+
106
+ Different prices for different actions:
107
+
108
+ ```ruby
109
+ def basic_data
110
+ x402_paywall(amount: 0.001)
111
+ render json: basic_info
112
+ end
113
+
114
+ def premium_data
115
+ x402_paywall(amount: 0.01)
116
+ render json: premium_info
117
+ end
118
+ ```
119
+
120
+ ## Configuration Options
121
+
122
+ ### Global Configuration
123
+
124
+ Set defaults in `config/initializers/x402.rb`:
125
+
126
+ ```ruby
127
+ X402.configure do |config|
128
+ # Required: Your wallet address where payments will be received
129
+ config.wallet_address = ENV['X402_WALLET_ADDRESS']
130
+
131
+ # Facilitator service URL (default: "https://x402.org/facilitator")
132
+ config.facilitator = ENV.fetch("X402_FACILITATOR_URL", "https://x402.org/facilitator")
133
+
134
+ # Blockchain network (default: "base-sepolia")
135
+ # Options: "base-sepolia", "base", "avalanche-fuji", "avalanche"
136
+ config.chain = ENV.fetch("X402_CHAIN", "base-sepolia")
137
+
138
+ # Payment token (default: "USDC")
139
+ # Currently only USDC is supported
140
+ config.currency = ENV.fetch("X402_CURRENCY","USDC")
141
+
142
+ # Optimistic mode (default: true)
143
+ # true: Fast response, settle payment after response is sent
144
+ # false: Wait for blockchain settlement before sending response
145
+ config.optimistic = ENV.fetch("X402_OPTIMISTIC",false)
146
+ end
147
+ ```
148
+
149
+ ### Configuration Attributes
150
+
151
+ | Attribute | Required | Default | Description |
152
+ | ---------------- | -------- | -------------------------------- | --------------------------------------------------------------------------------- |
153
+ | `wallet_address` | **Yes** | - | Your Ethereum wallet address where payments will be received |
154
+ | `facilitator` | No | `"https://x402.org/facilitator"` | Facilitator service URL for payment verification and settlement |
155
+ | `chain` | No | `"base-sepolia"` | Blockchain network to use (`base-sepolia`, `base`, `avalanche-fuji`, `avalanche`) |
156
+ | `currency` | No | `"USDC"` | Payment token symbol (currently only USDC supported) |
157
+ | `optimistic` | No | `true` | Settlement mode (see Optimistic vs Non-Optimistic Mode below) |
158
+
159
+ ## Environment Variables
160
+
161
+ Configure via environment variables:
162
+
163
+ ```bash
164
+ # Required
165
+ X402_WALLET_ADDRESS=0xYourAddress
166
+
167
+ # Optional (with defaults)
168
+ X402_FACILITATOR_URL=https://x402.org/facilitator
169
+ X402_CHAIN=base-sepolia
170
+ X402_CURRENCY=USDC
171
+ X402_OPTIMISTIC=true # "true" or "false"
172
+ ```
173
+
174
+ ## Examples
175
+
176
+ ### Weather API
177
+
178
+ ```ruby
179
+ class WeatherController < ApplicationController
180
+ def current
181
+ x402_paywall(amount: 0.001)
182
+ render json: { temp: 72, condition: "sunny" }
183
+ end
184
+
185
+ def forecast
186
+ x402_paywall(amount: 0.01)
187
+ render json: { forecast: [...] }
188
+ end
189
+ end
190
+ ```
191
+
192
+ ## x402 Architecture
193
+
194
+ ```
195
+ ┌──────────┐ ┌──────────┐ ┌─────────────┐
196
+ │ Client │─────▶│ Rails │─────▶│ Facilitator │
197
+ │ │ │ x402 │ │ (x402.org) │
198
+ └──────────┘ └──────────┘ └─────────────┘
199
+ │ │ │
200
+ │ │ ▼
201
+ │ │ ┌──────────────┐
202
+ │ │ │ Blockchain │
203
+ │ │ │ (Base) │
204
+ └──────────────────┴─────────────┴──────────────┘
205
+ ```
206
+
207
+ ## Error Handling
208
+
209
+ The gem raises these errors:
210
+
211
+ - `X402::ConfigurationError` - Invalid configuration
212
+ - `X402::InvalidPaymentError` - Invalid payment payload
213
+ - `X402::FacilitatorError` - Facilitator communication issues
214
+
215
+ ## Security
216
+
217
+ - Payments validated via EIP-712 signatures
218
+ - Nonce prevents replay attacks
219
+ - Time windows limit authorization validity
220
+ - Facilitator verifies all parameters
221
+ - Settlement happens on-chain (immutable)
222
+
223
+ ## Requirements
224
+
225
+ - Ruby 3.0+
226
+ - Rails 7.0+
227
+
228
+ ## Resources
229
+
230
+ - [x402 Protocol Docs](https://docs.cdp.coinbase.com/x402)
231
+ - [GitHub Repository](https://github.com/coinbase/x402)
232
+ - [Facilitator API](https://x402.org/facilitator)
233
+
234
+ ## License
235
+
236
+ MIT License. See [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="94" height="20">
2
+ <linearGradient id="b" x2="0" y2="100%">
3
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
4
+ <stop offset="1" stop-opacity=".1"/>
5
+ </linearGradient>
6
+ <clipPath id="a">
7
+ <rect width="94" height="20" rx="3" fill="#fff"/>
8
+ </clipPath>
9
+ <g clip-path="url(#a)">
10
+ <path fill="#555" d="M0 0h59v20H0z"/>
11
+ <path fill="#4c1" d="M59 0h35v20H59z"/>
12
+ <path fill="url(#b)" d="M0 0h94v20H0z"/>
13
+ </g>
14
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
15
+ <text x="305" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">coverage</text>
16
+ <text x="305" y="140" transform="scale(.1)" textLength="490">coverage</text>
17
+ <text x="755" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="250">100%</text>
18
+ <text x="755" y="140" transform="scale(.1)" textLength="250">100%</text></g>
19
+ </svg>
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ # Chain configurations for supported networks
5
+ CHAINS = {
6
+ "base-sepolia" => {
7
+ chain_id: 84532,
8
+ rpc_url: "https://sepolia.base.org",
9
+ usdc_address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
10
+ explorer_url: "https://sepolia.basescan.org"
11
+ },
12
+ "base" => {
13
+ chain_id: 8453,
14
+ rpc_url: "https://mainnet.base.org",
15
+ usdc_address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
16
+ explorer_url: "https://basescan.org"
17
+ },
18
+ "avalanche-fuji" => {
19
+ chain_id: 43113,
20
+ rpc_url: "https://api.avax-test.network/ext/bc/C/rpc",
21
+ usdc_address: "0x5425890298aed601595a70AB815c96711a31Bc65",
22
+ explorer_url: "https://testnet.snowtrace.io"
23
+ },
24
+ "avalanche" => {
25
+ chain_id: 43114,
26
+ rpc_url: "https://api.avax.network/ext/bc/C/rpc",
27
+ usdc_address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
28
+ explorer_url: "https://snowtrace.io"
29
+ }
30
+ }.freeze
31
+
32
+ # Currency configurations by chain
33
+ CURRENCY_BY_CHAIN = {
34
+ "base-sepolia" => {
35
+ symbol: "USDC",
36
+ decimals: 6,
37
+ name: "USDC", # Testnet uses "USDC"
38
+ version: "2"
39
+ },
40
+ "base" => {
41
+ symbol: "USDC",
42
+ decimals: 6,
43
+ name: "USD Coin", # Mainnet uses "USD Coin"
44
+ version: "2"
45
+ },
46
+ "avalanche-fuji" => {
47
+ symbol: "USDC",
48
+ decimals: 6,
49
+ name: "USD Coin", # Testnet uses "USD Coin"
50
+ version: "2"
51
+ },
52
+ "avalanche" => {
53
+ symbol: "USDC",
54
+ decimals: 6,
55
+ name: "USDC", # Mainnet uses "USDC"
56
+ version: "2"
57
+ }
58
+ }.freeze
59
+
60
+ class << self
61
+ def chain_config(chain_name)
62
+ CHAINS[chain_name] || raise(ConfigurationError, "Unsupported chain: #{chain_name}")
63
+ end
64
+
65
+ def currency_config_for_chain(chain_name)
66
+ CURRENCY_BY_CHAIN[chain_name] || raise(ConfigurationError, "Unsupported chain for currency: #{chain_name}")
67
+ end
68
+
69
+ def supported_chains
70
+ CHAINS.keys
71
+ end
72
+
73
+ def usdc_address_for(chain_name)
74
+ chain_config(chain_name)[:usdc_address]
75
+ end
76
+
77
+ def currency_decimals_for_chain(chain_name)
78
+ currency_config_for_chain(chain_name)[:decimals]
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ class Configuration
5
+ attr_accessor :wallet_address, :facilitator, :chain, :currency, :optimistic
6
+
7
+ def initialize
8
+ @wallet_address = ENV.fetch("X402_WALLET_ADDRESS", nil)
9
+ @facilitator = ENV.fetch("X402_FACILITATOR_URL", "https://x402.org/facilitator")
10
+ @chain = ENV.fetch("X402_CHAIN", "base-sepolia")
11
+ @currency = ENV.fetch("X402_CURRENCY", "USDC")
12
+ @optimistic = ENV.fetch("X402_OPTIMISTIC", "false") == "true" # Default to optimistic mode (fast response, settle after)
13
+ end
14
+
15
+ def validate!
16
+ raise ConfigurationError, "wallet_address is required" if wallet_address.nil? || wallet_address.empty?
17
+ raise ConfigurationError, "facilitator URL is required" if facilitator.nil? || facilitator.empty?
18
+ raise ConfigurationError, "chain is required" if chain.nil? || chain.empty?
19
+ raise ConfigurationError, "currency is required" if currency.nil? || currency.empty?
20
+ end
21
+ end
22
+
23
+ class << self
24
+ attr_writer :configuration
25
+
26
+ def configuration
27
+ @configuration ||= Configuration.new
28
+ end
29
+
30
+ def configure
31
+ yield(configuration)
32
+ end
33
+
34
+ def reset_configuration!
35
+ @configuration = Configuration.new
36
+ end
37
+ end
38
+
39
+ class ConfigurationError < StandardError; end
40
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module X402
7
+ class FacilitatorClient
8
+ attr_reader :facilitator_url
9
+
10
+ def initialize(facilitator_url = nil)
11
+ @facilitator_url = facilitator_url || X402.configuration.facilitator
12
+ end
13
+
14
+ def verify(payment_payload, payment_requirements)
15
+ request_body = {
16
+ x402Version: payment_payload.x402_version,
17
+ paymentPayload: payment_payload.to_h,
18
+ paymentRequirements: payment_requirements
19
+ }
20
+
21
+ ::Rails.logger.info("=== X402 Verify Request ===")
22
+ ::Rails.logger.info("URL: #{facilitator_url}/verify")
23
+ ::Rails.logger.info("Request body: #{request_body.to_json}")
24
+
25
+ response = connection.post("verify") do |req|
26
+ req.headers["Content-Type"] = "application/json"
27
+ req.body = request_body.to_json
28
+ end
29
+
30
+ ::Rails.logger.info("Response status: #{response.status}")
31
+ ::Rails.logger.info("Response body: #{response.body}")
32
+
33
+ handle_response(response, "verify")
34
+ rescue Faraday::Error => e
35
+ raise FacilitatorError, "Failed to verify payment: #{e.message}"
36
+ end
37
+
38
+ def settle(payment_payload, payment_requirements)
39
+ request_body = {
40
+ x402Version: payment_payload.x402_version,
41
+ paymentPayload: payment_payload.to_h,
42
+ paymentRequirements: payment_requirements
43
+ }
44
+
45
+ ::Rails.logger.info("=== X402 Settlement Request ===")
46
+ ::Rails.logger.info("URL: #{facilitator_url}/settle")
47
+ ::Rails.logger.info("Request body: #{request_body.to_json}")
48
+
49
+ response = connection.post("settle") do |req|
50
+ req.headers["Content-Type"] = "application/json"
51
+ req.body = request_body.to_json
52
+ end
53
+
54
+ ::Rails.logger.info("Response status: #{response.status}")
55
+ ::Rails.logger.info("Response body: #{response.body}")
56
+
57
+ settlement_data = handle_response(response, "settle")
58
+ SettlementResponse.new(settlement_data)
59
+ rescue Faraday::Error => e
60
+ raise FacilitatorError, "Failed to settle payment: #{e.message}"
61
+ end
62
+
63
+ def supported_networks
64
+ response = connection.get("supported")
65
+ handle_response(response, "supported")
66
+ rescue Faraday::Error => e
67
+ raise FacilitatorError, "Failed to fetch supported networks: #{e.message}"
68
+ end
69
+
70
+ private
71
+
72
+ def connection
73
+ @connection ||= Faraday.new(url: facilitator_url) do |faraday|
74
+ faraday.adapter Faraday.default_adapter
75
+ end
76
+ end
77
+
78
+ def handle_response(response, action)
79
+ case response.status
80
+ when 200..299
81
+ JSON.parse(response.body)
82
+ when 400
83
+ error_body = JSON.parse(response.body) rescue {}
84
+ raise InvalidPaymentError, "Invalid payment: #{error_body['error'] || response.body}"
85
+ when 500..599
86
+ raise FacilitatorError, "Facilitator error (#{action}): #{response.status}"
87
+ else
88
+ raise FacilitatorError, "Unexpected response (#{action}): #{response.status}"
89
+ end
90
+ rescue JSON::ParserError => e
91
+ raise FacilitatorError, "Failed to parse facilitator response: #{e.message}"
92
+ end
93
+ end
94
+
95
+ class FacilitatorError < StandardError; end
96
+ class InvalidPaymentError < StandardError; end
97
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ class PaymentPayload
5
+ attr_accessor :x402_version, :scheme, :network, :payload
6
+
7
+ def initialize(attributes = {})
8
+ attrs = attributes.with_indifferent_access
9
+
10
+ @x402_version = attrs[:x402Version] || attrs[:x402_version] || 1
11
+ @scheme = attrs[:scheme]
12
+ @network = attrs[:network]
13
+ @payload = attrs[:payload]
14
+ end
15
+
16
+ def self.from_header(header_value)
17
+ return nil if header_value.nil? || header_value.empty?
18
+
19
+ begin
20
+ decoded = Base64.strict_decode64(header_value)
21
+ json = JSON.parse(decoded)
22
+ new(json)
23
+ rescue StandardError => e
24
+ raise InvalidPaymentError, "Failed to decode payment payload: #{e.message}"
25
+ end
26
+ end
27
+
28
+ def authorization
29
+ @authorization ||= payload&.with_indifferent_access&.[](:authorization)
30
+ end
31
+
32
+ def signature
33
+ payload&.with_indifferent_access&.[](:signature)
34
+ end
35
+
36
+ def from_address
37
+ authorization&.with_indifferent_access&.[](:from)
38
+ end
39
+
40
+ def to_address
41
+ authorization&.with_indifferent_access&.[](:to)
42
+ end
43
+
44
+ def value
45
+ authorization&.with_indifferent_access&.[](:value)
46
+ end
47
+
48
+ def valid_after
49
+ authorization&.with_indifferent_access&.[](:validAfter) || authorization&.with_indifferent_access&.[](:valid_after)
50
+ end
51
+
52
+ def valid_before
53
+ authorization&.with_indifferent_access&.[](:validBefore) || authorization&.with_indifferent_access&.[](:valid_before)
54
+ end
55
+
56
+ def nonce
57
+ authorization&.with_indifferent_access&.[](:nonce)
58
+ end
59
+
60
+ def to_h
61
+ {
62
+ x402Version: x402_version,
63
+ scheme: scheme,
64
+ network: network,
65
+ payload: payload
66
+ }
67
+ end
68
+
69
+ def to_json(*args)
70
+ to_h.to_json(*args)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ class PaymentRequirement
5
+ attr_accessor :scheme, :network, :max_amount_required, :asset, :pay_to, :resource, :description, :max_timeout_seconds, :mime_type, :output_schema, :extra
6
+
7
+ def initialize(attributes = {})
8
+ attrs = attributes.with_indifferent_access
9
+
10
+ @scheme = attrs[:scheme] || "exact"
11
+ @network = attrs[:network]
12
+ @max_amount_required = attrs[:maxAmountRequired] || attrs[:max_amount_required]
13
+ @asset = attrs[:asset]
14
+ @pay_to = attrs[:payTo] || attrs[:pay_to]
15
+ @resource = attrs[:resource]
16
+ @description = attrs[:description]
17
+ @max_timeout_seconds = attrs[:maxTimeoutSeconds] || attrs[:max_timeout_seconds] || 600
18
+ @mime_type = attrs[:mimeType] || attrs[:mime_type] || "application/json"
19
+ @output_schema = attrs[:outputSchema] || attrs[:output_schema]
20
+ @extra = attrs[:extra]
21
+ end
22
+
23
+ def to_h
24
+ h = {
25
+ scheme: scheme,
26
+ network: network,
27
+ maxAmountRequired: max_amount_required.to_s,
28
+ asset: asset,
29
+ payTo: pay_to,
30
+ resource: resource,
31
+ description: description,
32
+ maxTimeoutSeconds: max_timeout_seconds,
33
+ mimeType: mime_type
34
+ }
35
+ h[:outputSchema] = output_schema if output_schema
36
+ h[:extra] = extra if extra
37
+ h
38
+ end
39
+
40
+ def to_json(*args)
41
+ to_h.to_json(*args)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ class PaymentValidator
5
+ attr_reader :facilitator_client
6
+
7
+ def initialize(facilitator_client = nil)
8
+ @facilitator_client = facilitator_client || FacilitatorClient.new
9
+ end
10
+
11
+ def validate(payment_payload, requirement)
12
+ # Validate scheme
13
+ unless payment_payload.scheme == requirement.scheme
14
+ return validation_error("Scheme mismatch: expected #{requirement.scheme}, got #{payment_payload.scheme}")
15
+ end
16
+
17
+ # Validate network
18
+ unless payment_payload.network == requirement.network
19
+ return validation_error("Network mismatch: expected #{requirement.network}, got #{payment_payload.network}")
20
+ end
21
+
22
+ # Validate recipient address
23
+ unless payment_payload.to_address&.downcase == requirement.pay_to&.downcase
24
+ return validation_error("Recipient mismatch: expected #{requirement.pay_to}, got #{payment_payload.to_address}")
25
+ end
26
+
27
+ # Validate amount
28
+ payment_value = payment_payload.value.to_i
29
+ required_value = requirement.max_amount_required.to_i
30
+
31
+ if payment_value < required_value
32
+ return validation_error("Insufficient amount: expected at least #{required_value}, got #{payment_value}")
33
+ end
34
+
35
+ # Call facilitator to verify payment (does NOT settle on blockchain yet)
36
+ begin
37
+ verify_result = facilitator_client.verify(payment_payload, requirement.to_h)
38
+
39
+ unless verify_result["isValid"]
40
+ return validation_error("Facilitator validation failed: #{verify_result['invalidReason']}")
41
+ end
42
+
43
+ if verify_result["payer"].nil?
44
+ return validation_error("Verification failed: no payer address returned")
45
+ end
46
+
47
+ validation_success(verify_result["payer"], verify_result)
48
+ rescue InvalidPaymentError => e
49
+ validation_error("Facilitator validation failed: #{e.message}")
50
+ rescue FacilitatorError => e
51
+ validation_error("Facilitator error: #{e.message}")
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def validation_success(payer_address, data = {})
58
+ {
59
+ valid: true,
60
+ payer: payer_address,
61
+ data: data
62
+ }
63
+ end
64
+
65
+ def validation_error(message)
66
+ {
67
+ valid: false,
68
+ error: message
69
+ }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Rails
5
+ module ControllerExtensions
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ after_action :settle_x402_payment_if_needed
10
+ end
11
+
12
+ def x402_paywall(options = {})
13
+ amount = options[:amount] or raise ArgumentError, "amount is required"
14
+ chain = options[:chain] || X402.configuration.chain
15
+ currency = options[:currency] || X402.configuration.currency
16
+
17
+ # Check if payment header is present
18
+ payment_header = request.headers["X-PAYMENT"]
19
+
20
+ if payment_header.nil? || payment_header.empty?
21
+ # No payment provided, return 402 with requirements
22
+ return render_payment_required(amount, chain, currency)
23
+ end
24
+
25
+ # Payment provided, validate it
26
+ process_payment(payment_header, amount, chain, currency)
27
+ end
28
+
29
+ private
30
+
31
+ def generate_payment_required_response(amount, chain, currency, error_message = nil)
32
+ requirement_response = X402::RequirementGenerator.generate(
33
+ amount: amount,
34
+ resource: request.original_url,
35
+ description: "Payment required for #{request.path}",
36
+ chain: chain,
37
+ currency: currency
38
+ )
39
+ requirement_response[:error] = error_message if error_message
40
+ requirement_response
41
+ end
42
+
43
+ def render_payment_required(amount, chain, currency)
44
+ requirement_response = generate_payment_required_response(amount, chain, currency)
45
+
46
+ # Detect if request is from browser or API client
47
+ # if browser_request?
48
+ # render_html_paywall(requirement_response)
49
+ # else
50
+ render json: requirement_response, status: :payment_required
51
+ # end
52
+ end
53
+
54
+ def process_payment(payment_header, amount, chain, currency)
55
+ # Parse payment payload
56
+ payment_payload = X402::PaymentPayload.from_header(payment_header)
57
+
58
+ # Generate requirement for validation (must match the 402 response exactly!)
59
+ requirement_data = X402::RequirementGenerator.generate(
60
+ amount: amount,
61
+ resource: request.original_url,
62
+ description: "Payment required for #{request.path}",
63
+ chain: chain,
64
+ currency: currency
65
+ )
66
+
67
+ requirement = X402::PaymentRequirement.new(requirement_data[:accepts].first)
68
+
69
+ # Validate payment (verify signature, but don't settle on blockchain yet)
70
+ validator = X402::PaymentValidator.new
71
+ validation_result = validator.validate(payment_payload, requirement)
72
+
73
+ unless validation_result[:valid]
74
+ requirement_response = generate_payment_required_response(amount, chain, currency, validation_result[:error])
75
+ return render json: requirement_response, status: :payment_required
76
+ end
77
+
78
+ # Store payment info and requirement in request environment
79
+ request.env["x402.payment"] = {
80
+ payer: validation_result[:payer],
81
+ amount: payment_payload.value,
82
+ network: payment_payload.network,
83
+ payload: payment_payload,
84
+ requirement: requirement
85
+ }
86
+
87
+ # If non-optimistic mode, settle payment synchronously before continuing
88
+ unless X402.configuration.optimistic
89
+ settlement_result = settle_payment_now
90
+
91
+ # If settlement failed, abort and return 402 with payment requirements
92
+ if settlement_result.nil? || !settlement_result.success?
93
+ error_message = settlement_result&.error_reason || "Settlement failed"
94
+ requirement_response = generate_payment_required_response(amount, chain, currency, "failed to settle payment: #{error_message}")
95
+ return render json: requirement_response, status: :payment_required
96
+ end
97
+ end
98
+
99
+ # Payment verified, continue with action
100
+ # In optimistic mode, settlement will happen automatically via after_action callback
101
+ rescue X402::InvalidPaymentError => e
102
+ requirement_response = generate_payment_required_response(amount, chain, currency, "Invalid payment: #{e.message}")
103
+ render json: requirement_response, status: :payment_required
104
+ rescue X402::FacilitatorError => e
105
+ requirement_response = generate_payment_required_response(amount, chain, currency, "Verification error: #{e.message}")
106
+ render json: requirement_response, status: :payment_required
107
+ end
108
+
109
+ def settle_x402_payment_if_needed
110
+ # Only run in optimistic mode (non-optimistic settles synchronously)
111
+ return unless X402.configuration.optimistic
112
+
113
+ # Only settle if payment was verified
114
+ payment_info = request.env["x402.payment"]
115
+ return unless payment_info
116
+
117
+ # Only settle if response is 2xx (success)
118
+ return unless response.status >= 200 && response.status < 300
119
+
120
+ perform_settlement(payment_info)
121
+ end
122
+
123
+ def settle_payment_now
124
+ payment_info = request.env["x402.payment"]
125
+ return unless payment_info
126
+
127
+ ::Rails.logger.info("=== X402 Non-Optimistic Settlement (before response) ===")
128
+ settlement_result = perform_settlement(payment_info)
129
+
130
+ # Store settlement result for later use (e.g., adding to response body)
131
+ request.env["x402.settlement_result"] = settlement_result
132
+ settlement_result
133
+ end
134
+
135
+ def perform_settlement(payment_info)
136
+ begin
137
+ ::Rails.logger.info("=== X402 Settlement Attempt ===")
138
+ ::Rails.logger.info("Optimistic mode: #{X402.configuration.optimistic}")
139
+ ::Rails.logger.info("Payment payload class: #{payment_info[:payload].class}")
140
+ ::Rails.logger.info("Payment payload: #{payment_info[:payload].inspect}")
141
+ ::Rails.logger.info("Requirement class: #{payment_info[:requirement].class}")
142
+ ::Rails.logger.info("Requirement hash: #{payment_info[:requirement].to_h.inspect}")
143
+
144
+ facilitator_client = X402::FacilitatorClient.new
145
+ settlement_result = facilitator_client.settle(
146
+ payment_info[:payload],
147
+ payment_info[:requirement].to_h
148
+ )
149
+
150
+ if settlement_result.success?
151
+ # Add settlement response header
152
+ response.headers["X-PAYMENT-RESPONSE"] = settlement_result.to_base64
153
+ ::Rails.logger.info("x402 settlement successful: #{settlement_result.transaction}")
154
+ else
155
+ # Settlement failed - in optimistic mode, user already got the service
156
+ # In non-optimistic mode, this will be caught before the response is sent
157
+ ::Rails.logger.error("x402 settlement failed: #{settlement_result.error_reason}")
158
+ end
159
+
160
+ settlement_result
161
+ rescue X402::FacilitatorError => e
162
+ ::Rails.logger.error("x402 settlement error: #{e.message}")
163
+ nil
164
+ end
165
+ end
166
+
167
+ def browser_request?
168
+ # If Accept header explicitly requests JSON, return JSON even from browsers
169
+ accept_header = request.headers["Accept"].to_s
170
+ return false if accept_header.include?("application/json")
171
+
172
+ # Otherwise, check User-Agent for browser indicators
173
+ user_agent = request.headers["User-Agent"].to_s
174
+ user_agent.match?(/(Mozilla|Chrome|Safari|Firefox|Edge|Opera)/i)
175
+ end
176
+
177
+ def render_html_paywall(requirement_response)
178
+ html = <<~HTML
179
+ <!DOCTYPE html>
180
+ <html>
181
+ <head>
182
+ <title>Payment Required</title>
183
+ <style>
184
+ body {
185
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
186
+ display: flex;
187
+ justify-content: center;
188
+ align-items: center;
189
+ min-height: 100vh;
190
+ margin: 0;
191
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
192
+ }
193
+ .paywall-container {
194
+ background: white;
195
+ padding: 3rem;
196
+ border-radius: 1rem;
197
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
198
+ max-width: 500px;
199
+ text-align: center;
200
+ }
201
+ h1 {
202
+ color: #333;
203
+ margin-bottom: 1rem;
204
+ font-size: 2rem;
205
+ }
206
+ p {
207
+ color: #666;
208
+ line-height: 1.6;
209
+ margin-bottom: 2rem;
210
+ }
211
+ .amount {
212
+ font-size: 2.5rem;
213
+ font-weight: bold;
214
+ color: #667eea;
215
+ margin: 1.5rem 0;
216
+ }
217
+ .info {
218
+ background: #f7f7f7;
219
+ padding: 1rem;
220
+ border-radius: 0.5rem;
221
+ margin: 1.5rem 0;
222
+ font-size: 0.9rem;
223
+ }
224
+ code {
225
+ background: #e0e0e0;
226
+ padding: 0.2rem 0.5rem;
227
+ border-radius: 0.25rem;
228
+ font-family: monospace;
229
+ }
230
+ </style>
231
+ </head>
232
+ <body>
233
+ <div class="paywall-container">
234
+ <h1>💳 Payment Required</h1>
235
+ <p>This resource requires payment to access.</p>
236
+ <div class="amount">$#{format('%.3f', requirement_response[:accepts].first[:maxAmountRequired].to_i / 1_000_000.0)}</div>
237
+ <div class="info">
238
+ <p><strong>Network:</strong> #{requirement_response[:accepts].first[:network]}</p>
239
+ <p><strong>Asset:</strong> USDC</p>
240
+ <p><strong>Resource:</strong> #{requirement_response[:accepts].first[:resource]}</p>
241
+ </div>
242
+ <p style="font-size: 0.85rem; color: #999;">
243
+ This resource uses the x402 payment protocol.
244
+ API clients can make payments programmatically by including the X-PAYMENT header.
245
+ </p>
246
+ </div>
247
+ </body>
248
+ </html>
249
+ HTML
250
+
251
+ render html: html.html_safe, status: :payment_required
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Rails
5
+ module Generators
6
+ class InstallGenerator < ::Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ desc "Creates X402 initializer for your application"
10
+
11
+ def copy_initializer
12
+ template "x402_initializer.rb", "config/initializers/x402.rb"
13
+
14
+ puts ""
15
+ puts "✅ X402 initializer created at config/initializers/x402.rb"
16
+ puts ""
17
+ puts "Next steps:"
18
+ puts "1. Set your X402_WALLET_ADDRESS environment variable"
19
+ puts "2. Configure your preferred chain (base-sepolia for testing, base for production)"
20
+ puts "3. Add x402_paywall(amount: 0.001) to any controller action"
21
+ puts ""
22
+ puts "Example usage:"
23
+ puts " def show"
24
+ puts " x402_paywall(amount: 0.001) # $0.001 payment required"
25
+ puts " render json: @data"
26
+ puts " end"
27
+ puts ""
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # X402 Payment Protocol Configuration
4
+ # For more information, see: https://docs.cdp.coinbase.com/x402
5
+
6
+ X402.configure do |config|
7
+ # Your wallet address (where payments will be received)
8
+ # Set this via environment variable: X402_WALLET_ADDRESS
9
+ config.wallet_address = ENV.fetch("X402_WALLET_ADDRESS", nil)
10
+
11
+ # Facilitator URL (default: https://x402.org/facilitator)
12
+ # The facilitator handles payment verification and settlement
13
+ config.facilitator = ENV.fetch("X402_FACILITATOR_URL", "https://x402.org/facilitator")
14
+
15
+ # Blockchain network to use
16
+ # Options: "base-sepolia" (testnet), "base" (mainnet), "avalanche-fuji" (testnet), "avalanche" (mainnet)
17
+ # For testing, use "base-sepolia". For production, use "base" (no fees!)
18
+ config.chain = ENV.fetch("X402_CHAIN", "base-sepolia")
19
+
20
+ # Currency symbol (currently only USDC is supported)
21
+ config.currency = ENV.fetch("X402_CURRENCY", "USDC")
22
+ end
23
+
24
+ # Validate configuration on initialization
25
+ X402.configuration.validate!
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ initializer "x402.controller_extensions" do
7
+ ActiveSupport.on_load(:action_controller) do
8
+ include X402::Rails::ControllerExtensions
9
+ end
10
+ end
11
+
12
+ initializer "x402.configuration" do
13
+ # Configuration will be loaded from initializers/x402.rb if present
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Rails
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/x402/rails.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_job"
5
+ require "action_controller"
6
+ require "json"
7
+ require "base64"
8
+
9
+ require_relative "rails/version"
10
+ require_relative "../x402/configuration"
11
+ require_relative "../x402/chains"
12
+ require_relative "../x402/payment_requirement"
13
+ require_relative "../x402/payment_payload"
14
+ require_relative "../x402/settlement_response"
15
+ require_relative "../x402/facilitator_client"
16
+ require_relative "../x402/payment_validator"
17
+ require_relative "../x402/requirement_generator"
18
+ require_relative "../x402/rails/controller_extensions"
19
+ require_relative "../x402/rails/railtie" if defined?(::Rails::Railtie)
20
+
21
+ module X402
22
+ module Rails
23
+ class Error < StandardError; end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ class RequirementGenerator
5
+ def self.generate(amount:, resource:, description: nil, chain: nil, currency: nil)
6
+ config = X402.configuration
7
+ chain_name = chain || config.chain
8
+
9
+ # Get chain and currency configuration for this specific chain
10
+ chain_config = X402.chain_config(chain_name)
11
+ currency_config = X402.currency_config_for_chain(chain_name)
12
+
13
+ # Convert amount to atomic units
14
+ atomic_amount = convert_to_atomic(amount, currency_config[:decimals])
15
+
16
+ # Get asset address (USDC contract address for the chain)
17
+ asset_address = X402.usdc_address_for(chain_name)
18
+
19
+ # Build EIP-712 domain info (required for signature verification)
20
+ # IMPORTANT: The name must match what the USDC contract returns for name()
21
+ # Testnets use "USDC", mainnets use "USD Coin" or "USDC" depending on chain
22
+ eip712_domain = {
23
+ name: currency_config[:name],
24
+ version: currency_config[:version]
25
+ }
26
+
27
+ # Create payment requirement
28
+ requirement = PaymentRequirement.new(
29
+ scheme: "exact",
30
+ network: chain_name,
31
+ max_amount_required: atomic_amount,
32
+ asset: asset_address,
33
+ pay_to: config.wallet_address,
34
+ resource: resource,
35
+ description: description || "Payment required for #{resource}",
36
+ max_timeout_seconds: 600,
37
+ mime_type: "application/json",
38
+ extra: eip712_domain
39
+ )
40
+
41
+ # Build full response
42
+ {
43
+ x402Version: 1,
44
+ error: "Payment required to access this resource",
45
+ accepts: [requirement.to_h]
46
+ }
47
+ end
48
+
49
+ def self.convert_to_atomic(amount, decimals)
50
+ # Convert USD amount to atomic units
51
+ # For USDC with 6 decimals: $0.001 = 1000 atomic units
52
+ (amount.to_f * (10**decimals)).to_i
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ class SettlementResponse
5
+ attr_accessor :success, :transaction, :network, :payer, :error_reason
6
+
7
+ def initialize(attributes = {})
8
+ @success = attributes[:success] || attributes["success"]
9
+ @transaction = attributes[:transaction] || attributes["transaction"]
10
+ @network = attributes[:network] || attributes["network"]
11
+ @payer = attributes[:payer] || attributes["payer"]
12
+ @error_reason = attributes[:error_reason] || attributes["errorReason"]
13
+ end
14
+
15
+ def success?
16
+ success == true
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ success: success,
22
+ transaction: transaction,
23
+ network: network,
24
+ payer: payer,
25
+ errorReason: error_reason
26
+ }
27
+ end
28
+
29
+ def to_json(*args)
30
+ to_h.to_json(*args)
31
+ end
32
+
33
+ def to_base64
34
+ Base64.strict_encode64(to_json)
35
+ end
36
+ end
37
+ end
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: x402-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - QuickNode
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-10-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday-follow_redirects
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '6.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '6.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: vcr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '6.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '6.0'
125
+ description: Accept instant blockchain micropayments in Rails applications using the
126
+ x402 protocol. Enable HTTP 402 Payment Required with USDC payments on Base and other
127
+ networks.
128
+ email:
129
+ - zach@quiknode.io
130
+ executables: []
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - ".rspec"
135
+ - LICENSE.txt
136
+ - README.md
137
+ - Rakefile
138
+ - coverage/coverage.svg
139
+ - lib/x402/chains.rb
140
+ - lib/x402/configuration.rb
141
+ - lib/x402/facilitator_client.rb
142
+ - lib/x402/payment_payload.rb
143
+ - lib/x402/payment_requirement.rb
144
+ - lib/x402/payment_validator.rb
145
+ - lib/x402/rails.rb
146
+ - lib/x402/rails/controller_extensions.rb
147
+ - lib/x402/rails/generators/install_generator.rb
148
+ - lib/x402/rails/generators/templates/x402_initializer.rb
149
+ - lib/x402/rails/railtie.rb
150
+ - lib/x402/rails/version.rb
151
+ - lib/x402/requirement_generator.rb
152
+ - lib/x402/settlement_response.rb
153
+ homepage: https://github.com/quiknode-labs/x402-rails
154
+ licenses:
155
+ - MIT
156
+ metadata:
157
+ homepage_uri: https://github.com/quiknode-labs/x402-rails
158
+ source_code_uri: https://github.com/quiknode-labs/x402-rails
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: 3.0.0
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubygems_version: 3.5.16
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Rails integration for x402 payment protocol
178
+ test_files: []