x402-payments 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: 42c2b33d7692d31886f45dde66a23fc750502aa7fe44db52d2c11ebd6d59b069
4
+ data.tar.gz: '081a125a98b562d5202396f96ceaaf950980f575d112b7c20dc2c3e96e2a235c'
5
+ SHA512:
6
+ metadata.gz: 6d79fed8b305baa86cc5378229bc039715cd70a4cfbce99424e84501d5890301698e80efc82b9d61d55ccd907aea2ec71eeaf1a7056dfe788b8ce8c99fffc683
7
+ data.tar.gz: 1fdc0978f6443a9d34de4e9596410fddf000461de27a2573a2e3adece08aeaae979d2029a1c569637541179a6902acb39cf0fda6e1c3b6f8af913339bbace821
@@ -0,0 +1,14 @@
1
+ name: "Dependency Review"
2
+ on: [pull_request]
3
+
4
+ permissions:
5
+ contents: read
6
+
7
+ jobs:
8
+ dependency-review:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: "Checkout Repository"
12
+ uses: actions/checkout@v4
13
+ - name: "Dependency Review"
14
+ uses: actions/dependency-review-action@v4
@@ -0,0 +1,40 @@
1
+ name: Pull Request Tests
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ push:
8
+ branches:
9
+ - main
10
+
11
+ permissions:
12
+ contents: write
13
+
14
+ jobs:
15
+ test:
16
+ runs-on: ubuntu-latest
17
+
18
+ steps:
19
+ - name: Checkout code
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Set up Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: "3.3"
26
+
27
+ - name: Install dependencies
28
+ run: bundle install --jobs 4 --retry 3
29
+
30
+ - name: Run tests
31
+ run: bundle exec rake
32
+
33
+ - name: Commit coverage badge
34
+ if: github.ref == 'refs/heads/main'
35
+ run: |
36
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
37
+ git config --local user.name "github-actions[bot]"
38
+ git add coverage/coverage.svg
39
+ git diff --staged --quiet || git commit -m "Update coverage badge [skip ci]"
40
+ git push
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,215 @@
1
+ # X402::Payments
2
+
3
+ ![Coverage](./coverage/coverage.svg)
4
+
5
+ Ruby gem for generating signed payment HTTP headers and links using the [x402 protocol](https://www.x402.org/).
6
+
7
+ Supports USDC payments on Base, Avalanche, and other EVM networks with EIP-712 signing.
8
+
9
+ ## Installation
10
+
11
+ ### System Requirements
12
+
13
+ This gem depends on the `eth` gem which requires native extensions for cryptographic operations. You'll need to install system dependencies first:
14
+
15
+ #### macOS
16
+
17
+ ```bash
18
+ brew install automake openssl libtool pkg-config gmp libffi
19
+ ```
20
+
21
+ #### Ubuntu/Debian
22
+
23
+ ```bash
24
+ sudo apt-get install build-essential libgmp-dev libssl-dev
25
+ ```
26
+
27
+ #### Alpine Linux
28
+
29
+ ```bash
30
+ apk add build-base gmp-dev openssl-dev autoconf automake libtool
31
+ ```
32
+
33
+ ### Installing the Gem
34
+
35
+ Add this line to your application's Gemfile:
36
+
37
+ ```ruby
38
+ gem 'x402-payments'
39
+ ```
40
+
41
+ And then execute:
42
+
43
+ ```bash
44
+ bundle install
45
+ ```
46
+
47
+ Or install it yourself as:
48
+
49
+ ```bash
50
+ gem install x402-payments
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ The gem uses environment variables for configuration with sensible defaults:
56
+
57
+ ```bash
58
+ # Required
59
+ export X402_PAY_TO="0xYourWalletAddress" # Default address to receive payments
60
+ export X402_PRIVATE_KEY="0xYourPrivateKey" # Private key for signing
61
+
62
+ # Optional (with defaults shown)
63
+ export X402_CHAIN="base-sepolia" # Network to use
64
+ export X402_MAX_TIMEOUT_SECONDS="600" # Payment validity timeout
65
+
66
+ # Optional: Custom RPC URLs (override default RPC endpoints)
67
+ export X402_BASE_RPC_URL="https://your-custom-rpc.com"
68
+ export X402_BASE_SEPOLIA_RPC_URL="https://your-sepolia-rpc.com"
69
+ export X402_AVALANCHE_RPC_URL="https://your-avax-rpc.com"
70
+ export X402_AVALANCHE_FUJI_RPC_URL="https://your-avax-testnet-rpc.com"
71
+ ```
72
+
73
+ ### Supported Networks
74
+
75
+ - `base-sepolia` (testnet) - Default
76
+ - `base` (mainnet)
77
+ - `avalanche-fuji` (testnet)
78
+ - `avalanche` (mainnet)
79
+
80
+ ## Usage
81
+
82
+ ### Basic Usage
83
+
84
+ ```ruby
85
+ require 'x402/payments'
86
+
87
+ # Generate a signed payment header
88
+ header = X402::Payments.generate_header(
89
+ amount: 0.001, # Amount in USD
90
+ resource: "http://localhost:3000/api/weather", # Protected resource URL
91
+ description: "Payment for weather API access", # Optional description
92
+ pay_to: "0xRecipientAddress" # Optional: override recipient (defaults to config)
93
+ )
94
+
95
+ # Use the header in an HTTP request
96
+ # curl -H "X-PAYMENT: #{header}" http://localhost:3000/api/weather
97
+ ```
98
+
99
+ **Note**: The `pay_to` parameter allows you to specify a different recipient wallet address per payment. If not provided, it uses the configured `default_pay_to`.
100
+
101
+ ### Using in Rails
102
+
103
+ The gem works seamlessly in Rails applications:
104
+
105
+ ```ruby
106
+ # config/initializers/x402.rb
107
+ X402::Payments.configure do |config|
108
+ config.default_pay_to = ENV['X402_PAY_TO']
109
+ config.private_key = ENV['X402_PRIVATE_KEY']
110
+ config.chain = Rails.env.production? ? 'base' : 'base-sepolia'
111
+
112
+ # Optional: Override RPC URLs programmatically
113
+ # config.rpc_urls = {
114
+ # 'base' => 'https://your-custom-base-rpc.com',
115
+ # 'base-sepolia' => 'https://your-sepolia-rpc.com'
116
+ # }
117
+ end
118
+
119
+ # In your controller or service
120
+ class PaymentService
121
+ def self.generate_payment_for(resource_url, amount)
122
+ X402::Payments.generate_header(
123
+ amount: amount,
124
+ resource: resource_url,
125
+ description: "Payment for #{resource_url}"
126
+ )
127
+ end
128
+ end
129
+ ```
130
+
131
+ ### Using Standalone (Non-Rails)
132
+
133
+ ```ruby
134
+ #!/usr/bin/env ruby
135
+ require 'x402/payments'
136
+
137
+ # Set environment variables or configure directly
138
+ X402::Payments.configure do |config|
139
+ config.default_pay_to = "0xYourDefaultRecipient"
140
+ config.private_key = "0xYourPrivateKeyHere"
141
+ config.chain = "base-sepolia"
142
+ # config.rpc_urls = { 'base-sepolia' => 'https://your-custom-rpc.com' }
143
+ end
144
+
145
+ # Generate payment
146
+ header = X402::Payments.generate_header(
147
+ amount: 0.001,
148
+ resource: "http://localhost:3000/api/data",
149
+ # network: "avalanche", # Override default network
150
+ # private_key: "0xDifferentKey", # Override default key
151
+ # pay_to: "0xRecipientWalletAddress", # Override recipient address
152
+ # extra: { # Override EIP-712 domain
153
+ # name: "Custom Token",
154
+ # version: "1"
155
+ # }
156
+ )
157
+
158
+ puts "Payment Header:"
159
+ puts header
160
+
161
+ HTTParty.get("http://localhost:3000/api/data", headers: { "X-PAYMENT" => header })
162
+ ```
163
+
164
+ ## How It Works
165
+
166
+ 1. **Payment Requirements**: The gem creates a payment requirement specifying the amount (in USDC atomic units), network, and resource
167
+ 2. **EIP-712 Signing**: Uses EIP-3009 `TransferWithAuthorization` to create a signature that authorizes the payment
168
+ 3. **Header Encoding**: Encodes the signed payment data as a base64 string for the `X-PAYMENT` HTTP header
169
+ 4. **Server Validation**: The server validates the signature and settles the payment on-chain
170
+
171
+ ## Example Script
172
+
173
+ A complete example script is provided in `examples/generate_payment.rb`:
174
+
175
+ ```bash
176
+ # Create your .env file in examples directory
177
+ cd examples
178
+ cp .env.example .env
179
+ # Edit .env with your credentials
180
+
181
+ # Run the example
182
+ cd ..
183
+ export $(cat examples/.env | xargs)
184
+ ruby examples/generate_payment.rb
185
+ ```
186
+
187
+ This will generate a signed payment header and provide a ready-to-use curl command for testing. See `examples/README.md` for more details.
188
+
189
+ ## Development
190
+
191
+ After checking out the repo, run:
192
+
193
+ ```bash
194
+ bin/setup # Install dependencies
195
+ bundle exec rake spec # Run tests
196
+ bin/console # Interactive prompt for experimentation
197
+ ```
198
+
199
+ ## Contributing
200
+
201
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/x402-payments.
202
+
203
+ ## Requirements
204
+
205
+ - Ruby 3.0+
206
+
207
+ ## Resources
208
+
209
+ - [x402 Protocol Docs](https://docs.cdp.coinbase.com/x402)
210
+ - [GitHub Repository](https://github.com/coinbase/x402)
211
+ - [Facilitator API](https://x402.org/facilitator)
212
+
213
+ ## License
214
+
215
+ 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,14 @@
1
+ # X402 Payments Configuration
2
+ # Copy this file to .env and fill in your actual values
3
+
4
+ # Required: Private key for signing payments
5
+ X402_PRIVATE_KEY=0xYourPrivateKeyHere
6
+
7
+ # Required: Default recipient wallet address for payments
8
+ X402_PAY_TO=0xYourRecipientAddressHere
9
+
10
+ # Optional: Network/chain to use (default: base-sepolia)
11
+ X402_CHAIN=base-sepolia
12
+
13
+ # Optional: Payment timeout in seconds (default: 600)
14
+ X402_MAX_TIMEOUT_SECONDS=600
@@ -0,0 +1,2 @@
1
+ # Environment variables with sensitive credentials
2
+ .env
@@ -0,0 +1,88 @@
1
+ # X402 Payments Examples
2
+
3
+ This directory contains example scripts demonstrating how to use the x402-payments gem.
4
+
5
+ ## generate_payment.rb
6
+
7
+ Generates a signed payment header and curl command for testing x402 payments.
8
+
9
+ ### Setup
10
+
11
+ First, create a `.env` file in the `examples/` directory with your credentials:
12
+
13
+ ```bash
14
+ cd examples
15
+ cp .env.example .env
16
+ # Edit .env with your actual values
17
+ ```
18
+
19
+ ### Usage
20
+
21
+ ```bash
22
+ # From the gem root directory
23
+ cd /Users/zachp/Dev/gems/x402-payments
24
+
25
+ # Load environment variables and run
26
+ export $(cat examples/.env | xargs)
27
+ ruby examples/generate_payment.rb
28
+
29
+ # Or set variables inline
30
+ X402_PRIVATE_KEY="0xYourPrivateKey" \
31
+ X402_PAY_TO="0xYourRecipientAddress" \
32
+ X402_CHAIN="base-sepolia" \
33
+ ruby examples/generate_payment.rb
34
+ ```
35
+
36
+ ### Environment Variables
37
+
38
+ - `X402_PRIVATE_KEY` - Private key for signing (required)
39
+ - `X402_PAY_TO` - Default recipient wallet address for payments (required)
40
+ - `X402_CHAIN` - Network to use: `base-sepolia`, `base`, `avalanche-fuji`, or `avalanche` (default: `base-sepolia`)
41
+
42
+ ### Example Output
43
+
44
+ ```
45
+ === X402 Payment Generator ===
46
+ Default Pay To: 0xYourRecipientAddress
47
+ Chain: base-sepolia
48
+
49
+ Generating payment for:
50
+ Resource: http://localhost:3000/api/weather/current
51
+ Amount: $0.001 USD
52
+
53
+ Payment Header (X-PAYMENT):
54
+ eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoi...
55
+
56
+ Curl Command:
57
+ curl -i -H "X-PAYMENT: eyJ4NDAyVmVyc2lvbiI6..." http://localhost:3000/api/weather/current
58
+
59
+ To test the payment, run:
60
+ curl -i -H "X-PAYMENT: eyJ4NDAyVmVyc2lvbiI6..." http://localhost:3000/api/weather/current
61
+ ```
62
+
63
+ You can copy and paste the curl command to test your x402-enabled API endpoint.
64
+
65
+ ## Customizing the Script
66
+
67
+ You can modify `generate_payment.rb` to:
68
+
69
+ - Change the amount (e.g., `amount = 0.005` for $0.005)
70
+ - Target different resources (e.g., `resource_url = "https://api.example.com/data"`)
71
+ - Use different networks (set `X402_CHAIN` environment variable)
72
+ - Add custom descriptions
73
+
74
+ ### Example: Generate Payment for Different Resource
75
+
76
+ ```ruby
77
+ # At the end of generate_payment.rb, add:
78
+
79
+ puts "\n=== Custom Payment ==="
80
+ custom_link = X402::Payments.generate_link(
81
+ amount: 0.005,
82
+ resource: "http://localhost:3000/api/premium/data",
83
+ description: "Premium API access"
84
+ )
85
+
86
+ puts "Custom Curl Command:"
87
+ puts custom_link[:curl_command]
88
+ ```
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "x402/payments"
6
+
7
+ # Load .env file if it exists in the examples directory
8
+ env_file = File.join(__dir__, ".env")
9
+ if File.exist?(env_file)
10
+ File.readlines(env_file).each do |line|
11
+ line = line.strip
12
+ next if line.empty? || line.start_with?("#")
13
+ key, value = line.split("=", 2)
14
+ ENV[key] = value if key && value
15
+ end
16
+ end
17
+
18
+ # Configuration
19
+ # NOTE: Replace these with your actual credentials via environment variables
20
+ PRIVATE_KEY = ENV.fetch("X402_PRIVATE_KEY")
21
+ DEFAULT_PAY_TO = ENV.fetch("X402_PAY_TO")
22
+ CHAIN = ENV.fetch("X402_CHAIN", "base-sepolia")
23
+ RESOURCE_URL = "http://localhost:3000/api/weather/paywalled_info"
24
+
25
+ # Configure the gem
26
+ X402::Payments.configure do |config|
27
+ config.default_pay_to = DEFAULT_PAY_TO
28
+ config.private_key = PRIVATE_KEY
29
+ config.chain = CHAIN
30
+ config.max_timeout_seconds = 600
31
+ end
32
+
33
+ puts "=== X402 Payment Generator ==="
34
+ puts "Default Pay To: #{DEFAULT_PAY_TO}"
35
+ puts "Chain: #{CHAIN}"
36
+
37
+ # Generate payment for a resource
38
+ amount = 0.001 # $0.001 USD
39
+
40
+ puts "Generating payment for:"
41
+ puts " Resource: #{RESOURCE_URL}"
42
+ puts " Amount: $#{amount} USD"
43
+ puts
44
+
45
+ # Generate the payment link with curl command
46
+ link = X402::Payments.generate_link(
47
+ amount: amount,
48
+ resource: RESOURCE_URL,
49
+ description: "Payment required for #{RESOURCE_URL}",
50
+ # pay_to: "0xDifferentRecipientAddress" # Optional: override recipient
51
+ )
52
+
53
+ puts "Payment Header (X-PAYMENT):"
54
+ puts link[:payment_header]
55
+ puts
56
+ puts "Curl Command:"
57
+ puts link[:curl_command]
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Payments
5
+ # Chain configurations for supported networks
6
+ CHAINS = {
7
+ "base-sepolia" => {
8
+ chain_id: 84532,
9
+ rpc_url: "https://clean-snowy-hexagon.base-sepolia.quiknode.pro",
10
+ usdc_address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
11
+ explorer_url: "https://sepolia.basescan.org"
12
+ },
13
+ "base" => {
14
+ chain_id: 8453,
15
+ rpc_url: "https://snowy-compatible-ensemble.base-mainnet.quiknode.pro",
16
+ usdc_address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
17
+ explorer_url: "https://basescan.org"
18
+ },
19
+ "avalanche-fuji" => {
20
+ chain_id: 43113,
21
+ rpc_url: "https://muddy-sly-field.avalanche-testnet.quiknode.pro/ext/bc/C/rpc",
22
+ usdc_address: "0x5425890298aed601595a70AB815c96711a31Bc65",
23
+ explorer_url: "https://testnet.snowtrace.io"
24
+ },
25
+ "avalanche" => {
26
+ chain_id: 43114,
27
+ rpc_url: "https://floral-patient-panorama.avalanche-mainnet.quiknode.pro/ext/bc/C/rpc",
28
+ usdc_address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
29
+ explorer_url: "https://snowtrace.io"
30
+ }
31
+ }.freeze
32
+
33
+
34
+ # Currency configurations by chain
35
+ CURRENCY_BY_CHAIN = {
36
+ "base-sepolia" => {
37
+ symbol: "USDC",
38
+ decimals: 6,
39
+ name: "USDC",
40
+ version: "2"
41
+ },
42
+ "base" => {
43
+ symbol: "USDC",
44
+ decimals: 6,
45
+ name: "USD Coin",
46
+ version: "2"
47
+ },
48
+ "avalanche-fuji" => {
49
+ symbol: "USDC",
50
+ decimals: 6,
51
+ name: "USD Coin",
52
+ version: "2"
53
+ },
54
+ "avalanche" => {
55
+ symbol: "USDC",
56
+ decimals: 6,
57
+ name: "USDC",
58
+ version: "2"
59
+ }
60
+ }.freeze
61
+
62
+ class << self
63
+ def chain_config(chain_name)
64
+ CHAINS[chain_name] || raise(ConfigurationError, "Unsupported chain: #{chain_name}")
65
+ end
66
+
67
+ def currency_config_for_chain(chain_name)
68
+ CURRENCY_BY_CHAIN[chain_name] || raise(ConfigurationError, "Unsupported chain for currency: #{chain_name}")
69
+ end
70
+
71
+ def supported_chains
72
+ CHAINS.keys
73
+ end
74
+
75
+ def usdc_address_for(chain_name)
76
+ chain_config(chain_name)[:usdc_address]
77
+ end
78
+
79
+ def chain_id_for(chain_name)
80
+ chain_config(chain_name)[:chain_id]
81
+ end
82
+
83
+ def currency_decimals_for_chain(chain_name)
84
+ currency_config_for_chain(chain_name)[:decimals]
85
+ end
86
+
87
+ def rpc_url_for(chain_name)
88
+ # Priority: 1) Programmatic config, 2) ENV variable, 3) Default from CHAINS
89
+ config = X402::Payments.configuration
90
+
91
+ # Check programmatic configuration
92
+ return config.rpc_urls[chain_name] if config.rpc_urls[chain_name]
93
+
94
+ # Check environment variable
95
+ env_var_name = "X402_#{chain_name.upcase.gsub('-', '_')}_RPC_URL"
96
+ env_rpc = ENV[env_var_name]
97
+ return env_rpc if env_rpc && !env_rpc.empty?
98
+
99
+ # Fall back to default
100
+ chain_config(chain_name)[:rpc_url]
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Payments
5
+ class Configuration
6
+ attr_accessor :default_pay_to, :private_key, :chain, :max_timeout_seconds, :rpc_urls
7
+
8
+ def initialize
9
+ @default_pay_to = ENV.fetch("X402_PAY_TO", nil)
10
+ @private_key = ENV.fetch("X402_PRIVATE_KEY", nil)
11
+ @chain = ENV.fetch("X402_CHAIN", "base-sepolia")
12
+ @max_timeout_seconds = ENV.fetch("X402_MAX_TIMEOUT_SECONDS", "600").to_i
13
+ @rpc_urls = {}
14
+ end
15
+
16
+ def validate!
17
+ raise ConfigurationError, "default_pay_to is required" if default_pay_to.nil? || default_pay_to.empty?
18
+ raise ConfigurationError, "private_key is required" if private_key.nil? || private_key.empty?
19
+ raise ConfigurationError, "chain is required" if chain.nil? || chain.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
41
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eth"
4
+ require "json"
5
+ require "base64"
6
+ require "securerandom"
7
+ require "time"
8
+
9
+ module X402
10
+ module Payments
11
+ class Generator
12
+ attr_reader :config
13
+
14
+ def initialize(config = X402::Payments.configuration)
15
+ @config = config
16
+ end
17
+
18
+ def generate_header(amount:, resource:, description: nil, network: nil, private_key: nil, pay_to: nil, extra: nil)
19
+ config.validate!
20
+
21
+ chain_name = network || config.chain
22
+ key = private_key || config.private_key
23
+ recipient = pay_to || config.default_pay_to
24
+
25
+ chain_config = X402::Payments.chain_config(chain_name)
26
+ currency_config = X402::Payments.currency_config_for_chain(chain_name)
27
+
28
+ # Convert amount to atomic units
29
+ atomic_amount = convert_to_atomic(amount, currency_config[:decimals])
30
+
31
+ # Get the Ethereum account from private key
32
+ account = Eth::Key.new(priv: key)
33
+ sender_address = account.address.to_s
34
+
35
+ # Create nonce (32 random bytes)
36
+ nonce = SecureRandom.random_bytes(32)
37
+
38
+ # Build payment requirements
39
+ payment_requirements = {
40
+ scheme: "exact",
41
+ network: chain_name,
42
+ max_amount_required: atomic_amount.to_s,
43
+ asset: chain_config[:usdc_address],
44
+ pay_to: recipient,
45
+ resource: resource,
46
+ description: description || "Payment required for #{resource}",
47
+ max_timeout_seconds: config.max_timeout_seconds,
48
+ mime_type: "application/json",
49
+ extra: extra || {
50
+ name: currency_config[:name],
51
+ version: currency_config[:version]
52
+ }
53
+ }
54
+
55
+ # Prepare unsigned payment header
56
+ valid_after = (Time.now.to_i - 60).to_s
57
+ valid_before = (Time.now.to_i + config.max_timeout_seconds).to_s
58
+
59
+ header = {
60
+ x402Version: 1,
61
+ scheme: "exact",
62
+ network: chain_name,
63
+ payload: {
64
+ signature: nil,
65
+ authorization: {
66
+ from: sender_address,
67
+ to: recipient,
68
+ value: atomic_amount.to_s,
69
+ validAfter: valid_after,
70
+ validBefore: valid_before,
71
+ nonce: "0x#{nonce.unpack1('H*')}"
72
+ }
73
+ }
74
+ }
75
+
76
+ # Sign the payment header
77
+ signature = sign_payment(account, header, payment_requirements, nonce)
78
+ header[:payload][:signature] = signature
79
+
80
+ # Encode to base64
81
+ encode_payment(header)
82
+ end
83
+
84
+ def generate_link(amount:, resource:, description: nil, network: nil, private_key: nil, pay_to: nil, extra: nil)
85
+ header = generate_header(
86
+ amount: amount,
87
+ resource: resource,
88
+ description: description,
89
+ network: network,
90
+ private_key: private_key,
91
+ pay_to: pay_to,
92
+ extra: extra
93
+ )
94
+
95
+ {
96
+ payment_header: header,
97
+ curl_command: "curl -s -H \"X-PAYMENT: #{header}\" #{resource} | jq ."
98
+ }
99
+ end
100
+
101
+ private
102
+
103
+ def convert_to_atomic(amount, decimals)
104
+ (amount.to_f * (10**decimals)).to_i
105
+ end
106
+
107
+ def sign_payment(account, header, payment_requirements, nonce_bytes)
108
+ auth = header[:payload][:authorization]
109
+ extra = payment_requirements[:extra]
110
+ chain_name = payment_requirements[:network]
111
+ asset = payment_requirements[:asset]
112
+
113
+ # Build EIP-712 typed data
114
+ typed_data = {
115
+ types: {
116
+ EIP712Domain: [
117
+ { name: "name", type: "string" },
118
+ { name: "version", type: "string" },
119
+ { name: "chainId", type: "uint256" },
120
+ { name: "verifyingContract", type: "address" }
121
+ ],
122
+ TransferWithAuthorization: [
123
+ { name: "from", type: "address" },
124
+ { name: "to", type: "address" },
125
+ { name: "value", type: "uint256" },
126
+ { name: "validAfter", type: "uint256" },
127
+ { name: "validBefore", type: "uint256" },
128
+ { name: "nonce", type: "bytes32" }
129
+ ]
130
+ },
131
+ primaryType: "TransferWithAuthorization",
132
+ domain: {
133
+ name: extra[:name],
134
+ version: extra[:version],
135
+ chainId: X402::Payments.chain_id_for(chain_name),
136
+ verifyingContract: asset
137
+ },
138
+ message: {
139
+ from: auth[:from],
140
+ to: auth[:to],
141
+ value: auth[:value].to_i,
142
+ validAfter: auth[:validAfter].to_i,
143
+ validBefore: auth[:validBefore].to_i,
144
+ nonce: nonce_bytes
145
+ }
146
+ }
147
+
148
+ # Sign using eth gem
149
+ signature = account.sign_typed_data(typed_data)
150
+
151
+ # Ensure signature has 0x prefix
152
+ signature = "0x#{signature}" unless signature.start_with?("0x")
153
+ signature
154
+ end
155
+
156
+ def encode_payment(payment_payload)
157
+ json_str = JSON.generate(payment_payload)
158
+ Base64.strict_encode64(json_str)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Payments
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "payments/version"
4
+ require_relative "payments/configuration"
5
+ require_relative "payments/chains"
6
+ require_relative "payments/generator"
7
+
8
+ module X402
9
+ module Payments
10
+ class Error < StandardError; end
11
+
12
+ class << self
13
+ def generate_header(amount:, resource:, description: nil, network: nil, private_key: nil, pay_to: nil, extra: nil)
14
+ generator = Generator.new
15
+ generator.generate_header(
16
+ amount: amount,
17
+ resource: resource,
18
+ description: description,
19
+ network: network,
20
+ private_key: private_key,
21
+ pay_to: pay_to,
22
+ extra: extra
23
+ )
24
+ end
25
+
26
+ def generate_link(amount:, resource:, description: nil, network: nil, private_key: nil, pay_to: nil, extra: nil)
27
+ generator = Generator.new
28
+ generator.generate_link(
29
+ amount: amount,
30
+ resource: resource,
31
+ description: description,
32
+ network: network,
33
+ private_key: private_key,
34
+ pay_to: pay_to,
35
+ extra: extra
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: x402-payments
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-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eth
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.5.11
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.5.11
27
+ - !ruby/object:Gem::Dependency
28
+ name: base64
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ostruct
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
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
+ description: Ruby gem for generating signed payment headers and links using the x402
84
+ protocol. Supports USDC payments on Base and other EVM networks with EIP-712 signing.
85
+ email:
86
+ - zach+402@quiknode.io
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".github/workflows/dependency-review.yml"
92
+ - ".github/workflows/pull_requests.yml"
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - coverage/coverage.svg
97
+ - examples/.env.example
98
+ - examples/.gitignore
99
+ - examples/README.md
100
+ - examples/generate_payment.rb
101
+ - lib/x402/payments.rb
102
+ - lib/x402/payments/chains.rb
103
+ - lib/x402/payments/configuration.rb
104
+ - lib/x402/payments/generator.rb
105
+ - lib/x402/payments/version.rb
106
+ homepage: https://github.com/quiknode-labs/x402-payments
107
+ licenses:
108
+ - MIT
109
+ metadata:
110
+ homepage_uri: https://github.com/quiknode-labs/x402-payments
111
+ source_code_uri: https://github.com/quiknode-labs/x402-payments
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 3.0.0
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.5.16
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Generate x402 payment signatures and links for blockchain micropayments
131
+ test_files: []