x402.rb 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: 6d7c4bbda7de50fffc403636c6951b355f6b782a7d0d6332c0287f501aac2b23
4
+ data.tar.gz: fa12d2bc84fe3db789ee7db772d0a94a603f86752b61aeef455dc6db01456ca0
5
+ SHA512:
6
+ metadata.gz: 2cbea85e5c81a576cd9911ffafa5af426ce921665a4e2b45dd4cb157ed69e81f81836d5aea970c3677c46aa5b9bac04a0bcef7a2786fdc88ce1c2c1220bba5bc
7
+ data.tar.gz: 21096796768eaa15b2f2e5bac1e6ed8e19d15c2f151c5a7979e9502ea4392f16c1df7575372450648600ce6bba534992d6ad8b2afaffa702e95aa62cc948467e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Filippo
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # x402.rb
2
+
3
+ Ruby gem for the [x402 protocol](https://docs.x402.org) — HTTP 402-based on-chain payments for APIs and services.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem "x402.rb"
9
+ ```
10
+
11
+ ## Components
12
+
13
+ ### Facilitator Client
14
+
15
+ Interact with an x402 facilitator to verify and settle payments:
16
+
17
+ ```ruby
18
+ require "x402"
19
+
20
+ facilitator = X402::Facilitator.new
21
+ # or: X402::Facilitator.new(base_url: "https://api.cdp.coinbase.com/platform/v2/x402")
22
+
23
+ # Check what's supported
24
+ facilitator.supported
25
+
26
+ # Verify a payment
27
+ result = facilitator.verify(
28
+ payment_payload: payload,
29
+ payment_requirements: requirements
30
+ )
31
+ result.valid? # => true
32
+ result.payer # => "0x..."
33
+
34
+ # Settle a payment
35
+ settlement = facilitator.settle(
36
+ payment_payload: payload,
37
+ payment_requirements: requirements
38
+ )
39
+ settlement.success? # => true
40
+ settlement.transaction # => "0xabc..."
41
+ ```
42
+
43
+ ### Server Middleware (Rack)
44
+
45
+ Gate routes behind x402 payments. Works with Rails, Sinatra, and any Rack app:
46
+
47
+ ```ruby
48
+ # config/application.rb (Rails)
49
+ config.middleware.use X402::Server::Middleware, routes: {
50
+ "GET /api/premium" => {
51
+ accepts: [{
52
+ scheme: "exact",
53
+ network: "eip155:84532",
54
+ amount: "10000", # atomic units (USDC has 6 decimals)
55
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
56
+ pay_to: "0xYourWalletAddress",
57
+ max_timeout_seconds: 60,
58
+ extra: { "name" => "USDC", "version" => "2" }
59
+ }],
60
+ description: "Premium market data",
61
+ mime_type: "application/json"
62
+ }
63
+ }
64
+ ```
65
+
66
+ ```ruby
67
+ # Sinatra
68
+ use X402::Server::Middleware, routes: { ... }
69
+ ```
70
+
71
+ The middleware:
72
+ 1. Returns `402` with a `PAYMENT-REQUIRED` header for gated routes without payment
73
+ 2. Verifies payment via the facilitator when a `PAYMENT-SIGNATURE` header is present
74
+ 3. Settles the payment after the downstream app responds successfully
75
+ 4. Returns the `PAYMENT-RESPONSE` header with settlement details
76
+
77
+ ### Client Middleware (Faraday)
78
+
79
+ Automatically handle 402 responses by signing and retrying:
80
+
81
+ ```ruby
82
+ require "faraday"
83
+ require "x402"
84
+ require "x402/client/middleware"
85
+
86
+ conn = Faraday.new("https://api.example.com") do |f|
87
+ f.use X402::Client::Middleware, signer: my_signer
88
+ f.adapter Faraday.default_adapter
89
+ end
90
+
91
+ # Transparently pays for 402-gated resources
92
+ response = conn.get("/api/premium")
93
+ response.body # => the paid content
94
+ ```
95
+
96
+ The `signer` must implement `#sign(payment_required)` and return an `X402::Types::PaymentPayload` (or `nil` to skip payment). This is where you integrate your wallet/key management.
97
+
98
+ ### Configuration
99
+
100
+ ```ruby
101
+ X402.configure do |c|
102
+ c.facilitator_url = "https://x402.org/facilitator" # default (testnet)
103
+ c.x402_version = 2 # default
104
+ end
105
+ ```
106
+
107
+ ## Protocol Flow
108
+
109
+ ```
110
+ Client Server Facilitator
111
+ | | |
112
+ | GET /premium | |
113
+ |------------------------------>| |
114
+ | | |
115
+ | 402 + PAYMENT-REQUIRED | |
116
+ |<------------------------------| |
117
+ | | |
118
+ | GET /premium | |
119
+ | + PAYMENT-SIGNATURE | |
120
+ |------------------------------>| |
121
+ | | POST /verify |
122
+ | |---------------------------->|
123
+ | | { isValid: true } |
124
+ | |<----------------------------|
125
+ | | |
126
+ | | POST /settle |
127
+ | |---------------------------->|
128
+ | | { success, transaction } |
129
+ | |<----------------------------|
130
+ | | |
131
+ | 200 + PAYMENT-RESPONSE | |
132
+ |<------------------------------| |
133
+ ```
134
+
135
+ ## Types
136
+
137
+ - `X402::Types::PaymentRequired` — server's 402 response with accepted payment options
138
+ - `X402::Types::PaymentRequirements` — a single payment option (scheme, network, amount, asset, payTo)
139
+ - `X402::Types::PaymentPayload` — client's signed payment authorization
140
+ - `X402::Types::SettlementResponse` — settlement result with transaction hash
141
+ - `X402::Types::VerifyResponse` — verification result from facilitator
142
+ - `X402::Types::ResourceInfo` — URL, description, and MIME type of the resource
143
+
144
+ All types support `#to_h`, `.from_h(hash)`, `#encode` (Base64 JSON), and `.decode(string)`.
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Client
5
+ # Faraday middleware that automatically handles x402 payment flows.
6
+ #
7
+ # Usage:
8
+ # conn = Faraday.new("https://api.example.com") do |f|
9
+ # f.use X402::Client::Middleware, signer: my_signer
10
+ # end
11
+ #
12
+ # The signer must respond to #sign(payment_requirements, resource) and return
13
+ # a PaymentPayload (or a Hash with the same structure).
14
+ #
15
+ class Middleware < Faraday::Middleware
16
+ def initialize(app, signer:)
17
+ super(app)
18
+ @signer = signer
19
+ end
20
+
21
+ def call(env)
22
+ response = @app.call(env)
23
+
24
+ return response unless response.status == 402
25
+
26
+ # Parse the payment requirements
27
+ encoded = response.headers[Header::PAYMENT_REQUIRED] ||
28
+ response.headers[Header::PAYMENT_REQUIRED.downcase]
29
+
30
+ return response unless encoded
31
+
32
+ payment_required = Types::PaymentRequired.decode(encoded)
33
+
34
+ # Let the signer pick a payment option and produce a signed payload
35
+ payment_payload = @signer.sign(payment_required)
36
+ return response unless payment_payload
37
+
38
+ payment_payload = if payment_payload.is_a?(Types::PaymentPayload)
39
+ payment_payload
40
+ else
41
+ Types::PaymentPayload.new(**payment_payload)
42
+ end
43
+
44
+ # Retry the request with the payment header
45
+ env.request_headers[Header::PAYMENT_SIGNATURE] = payment_payload.encode
46
+ @app.call(env)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ class Configuration
5
+ attr_accessor :facilitator_url, :x402_version
6
+
7
+ TESTNET_FACILITATOR = "https://x402.org/facilitator"
8
+ PRODUCTION_FACILITATOR = "https://api.cdp.coinbase.com/platform/v2/x402"
9
+
10
+ def initialize
11
+ @facilitator_url = TESTNET_FACILITATOR
12
+ @x402_version = 2
13
+ end
14
+ end
15
+
16
+ class << self
17
+ def configuration
18
+ @configuration ||= Configuration.new
19
+ end
20
+
21
+ def configure
22
+ yield(configuration)
23
+ end
24
+
25
+ def reset_configuration!
26
+ @configuration = Configuration.new
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module X402
8
+ class Facilitator
9
+ attr_reader :base_url
10
+
11
+ def initialize(base_url: nil)
12
+ @base_url = base_url || X402.configuration.facilitator_url
13
+ end
14
+
15
+ # Verify a payment authorization without executing it.
16
+ #
17
+ # @param payment_payload [PaymentPayload, Hash] the client's payment payload
18
+ # @param payment_requirements [PaymentRequirements, Hash] the requirements to verify against
19
+ # @return [Types::VerifyResponse]
20
+ def verify(payment_payload:, payment_requirements:)
21
+ body = {
22
+ x402Version: X402.configuration.x402_version,
23
+ paymentPayload: normalize(payment_payload),
24
+ paymentRequirements: normalize(payment_requirements)
25
+ }
26
+
27
+ response = post("/verify", body)
28
+ Types::VerifyResponse.from_h(response)
29
+ end
30
+
31
+ # Settle (execute) a verified payment on-chain.
32
+ #
33
+ # @param payment_payload [PaymentPayload, Hash] the client's payment payload
34
+ # @param payment_requirements [PaymentRequirements, Hash] the requirements that were accepted
35
+ # @return [Types::SettlementResponse]
36
+ def settle(payment_payload:, payment_requirements:)
37
+ body = {
38
+ x402Version: X402.configuration.x402_version,
39
+ paymentPayload: normalize(payment_payload),
40
+ paymentRequirements: normalize(payment_requirements)
41
+ }
42
+
43
+ response = post("/settle", body)
44
+ Types::SettlementResponse.from_h(response)
45
+ end
46
+
47
+ # Get supported schemes, networks, and extensions.
48
+ #
49
+ # @return [Hash]
50
+ def supported
51
+ get("/supported")
52
+ end
53
+
54
+ private
55
+
56
+ def normalize(obj)
57
+ obj.respond_to?(:to_h) ? obj.to_h : obj
58
+ end
59
+
60
+ def post(path, body)
61
+ uri = URI.parse("#{base_url.chomp("/")}#{path}")
62
+ request = Net::HTTP::Post.new(uri)
63
+ request["Content-Type"] = "application/json"
64
+ request.body = JSON.generate(body)
65
+
66
+ execute(uri, request)
67
+ end
68
+
69
+ def get(path)
70
+ uri = URI.parse("#{base_url.chomp("/")}#{path}")
71
+ request = Net::HTTP::Get.new(uri)
72
+ request["Accept"] = "application/json"
73
+
74
+ execute(uri, request)
75
+ end
76
+
77
+ def execute(uri, request)
78
+ http = Net::HTTP.new(uri.host, uri.port)
79
+ http.use_ssl = uri.scheme == "https"
80
+
81
+ response = http.request(request)
82
+
83
+ unless response.is_a?(Net::HTTPSuccess)
84
+ raise FacilitatorError, "Facilitator returned #{response.code}: #{response.body}"
85
+ end
86
+
87
+ JSON.parse(response.body)
88
+ rescue JSON::ParserError => e
89
+ raise FacilitatorError, "Invalid JSON from facilitator: #{e.message}"
90
+ end
91
+ end
92
+
93
+ class FacilitatorError < StandardError; end
94
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+
6
+ module X402
7
+ module Header
8
+ PAYMENT_REQUIRED = "PAYMENT-REQUIRED"
9
+ PAYMENT_SIGNATURE = "PAYMENT-SIGNATURE"
10
+ PAYMENT_RESPONSE = "PAYMENT-RESPONSE"
11
+
12
+ # Rack normalizes headers to HTTP_UPPER_SNAKE format
13
+ RACK_PAYMENT_REQUIRED = "HTTP_PAYMENT_REQUIRED"
14
+ RACK_PAYMENT_SIGNATURE = "HTTP_PAYMENT_SIGNATURE"
15
+ RACK_PAYMENT_RESPONSE = "HTTP_PAYMENT_RESPONSE"
16
+
17
+ class << self
18
+ def encode(data)
19
+ json = data.is_a?(String) ? data : JSON.generate(data)
20
+ Base64.strict_encode64(json)
21
+ end
22
+
23
+ def decode(value)
24
+ JSON.parse(Base64.strict_decode64(value))
25
+ rescue ArgumentError, JSON::ParserError => e
26
+ raise DecodeError, "Failed to decode x402 header: #{e.message}"
27
+ end
28
+ end
29
+ end
30
+
31
+ class DecodeError < StandardError; end
32
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Server
5
+ # Rack middleware that gates routes behind x402 payments.
6
+ #
7
+ # Usage:
8
+ # use X402::Server::Middleware, routes: {
9
+ # "GET /premium" => {
10
+ # accepts: [{
11
+ # scheme: "exact",
12
+ # network: "eip155:84532",
13
+ # amount: "10000",
14
+ # asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
15
+ # pay_to: "0xYourAddress",
16
+ # max_timeout_seconds: 60,
17
+ # extra: { name: "USDC", version: "2" }
18
+ # }],
19
+ # description: "Premium content",
20
+ # mime_type: "application/json"
21
+ # }
22
+ # }
23
+ #
24
+ class Middleware
25
+ def initialize(app, routes:, facilitator: nil)
26
+ @app = app
27
+ @routes = normalize_routes(routes)
28
+ @facilitator = facilitator || X402::Facilitator.new
29
+ end
30
+
31
+ def call(env)
32
+ route_key = "#{env["REQUEST_METHOD"]} #{env["PATH_INFO"]}"
33
+ route_config = @routes[route_key]
34
+
35
+ # Not a gated route — pass through
36
+ return @app.call(env) unless route_config
37
+
38
+ payment_header = env[Header::RACK_PAYMENT_SIGNATURE]
39
+
40
+ # No payment provided — return 402
41
+ unless payment_header
42
+ return payment_required_response(env, route_config)
43
+ end
44
+
45
+ # Decode and verify payment
46
+ payment_payload = Types::PaymentPayload.decode(payment_header)
47
+ accepted = payment_payload.accepted
48
+
49
+ # Find matching requirements
50
+ matching = route_config[:accepts].find do |req|
51
+ req.scheme == accepted.scheme && req.network == accepted.network
52
+ end
53
+
54
+ unless matching
55
+ return payment_required_response(env, route_config,
56
+ error: "No matching payment scheme/network")
57
+ end
58
+
59
+ # Verify via facilitator
60
+ verify_result = @facilitator.verify(
61
+ payment_payload: payment_payload,
62
+ payment_requirements: matching
63
+ )
64
+
65
+ unless verify_result.valid?
66
+ return payment_required_response(env, route_config,
67
+ error: "Payment verification failed: #{verify_result.invalid_reason}")
68
+ end
69
+
70
+ # Call the downstream app
71
+ status, headers, body = @app.call(env)
72
+
73
+ # Settle the payment
74
+ settlement = @facilitator.settle(
75
+ payment_payload: payment_payload,
76
+ payment_requirements: matching
77
+ )
78
+
79
+ headers[Header::PAYMENT_RESPONSE] = settlement.encode
80
+
81
+ if settlement.success?
82
+ [status, headers, body]
83
+ else
84
+ payment_required_response(env, route_config,
85
+ error: "Settlement failed: #{settlement.error_reason}")
86
+ end
87
+ rescue DecodeError => e
88
+ [400, {"content-type" => "application/json"},
89
+ [JSON.generate({error: e.message})]]
90
+ rescue FacilitatorError => e
91
+ [500, {"content-type" => "application/json"},
92
+ [JSON.generate({error: e.message})]]
93
+ end
94
+
95
+ private
96
+
97
+ def payment_required_response(env, route_config, error: nil)
98
+ payment_required = Types::PaymentRequired.new(
99
+ resource: {
100
+ url: "#{env["rack.url_scheme"]}://#{env["HTTP_HOST"]}#{env["PATH_INFO"]}",
101
+ description: route_config[:description],
102
+ mime_type: route_config[:mime_type]
103
+ },
104
+ accepts: route_config[:accepts],
105
+ error: error || "#{Header::PAYMENT_SIGNATURE} header is required"
106
+ )
107
+
108
+ headers = {
109
+ "content-type" => "application/json",
110
+ Header::PAYMENT_REQUIRED => payment_required.encode
111
+ }
112
+
113
+ [402, headers, [JSON.generate(payment_required.to_h)]]
114
+ end
115
+
116
+ def normalize_routes(routes)
117
+ routes.transform_values do |config|
118
+ config = config.dup
119
+ config[:accepts] = config[:accepts].map do |a|
120
+ a.is_a?(Types::PaymentRequirements) ? a : Types::PaymentRequirements.new(**a)
121
+ end
122
+ config
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Types
5
+ class PaymentPayload
6
+ attr_accessor :x402_version, :resource, :accepted, :payload, :extensions
7
+
8
+ def initialize(accepted:, payload:, resource: nil, x402_version: nil, extensions: nil)
9
+ @x402_version = x402_version || X402.configuration.x402_version
10
+ @resource = if resource.is_a?(Hash)
11
+ ResourceInfo.new(**resource)
12
+ else
13
+ resource
14
+ end
15
+ @accepted = if accepted.is_a?(Hash)
16
+ PaymentRequirements.new(**accepted)
17
+ else
18
+ accepted
19
+ end
20
+ @payload = payload
21
+ @extensions = extensions
22
+ end
23
+
24
+ def to_h
25
+ h = {
26
+ x402Version: x402_version,
27
+ accepted: accepted.to_h,
28
+ payload: payload
29
+ }
30
+ h[:resource] = resource.to_h if resource
31
+ h[:extensions] = extensions if extensions
32
+ h
33
+ end
34
+
35
+ def encode
36
+ Header.encode(to_h)
37
+ end
38
+
39
+ def self.from_h(hash)
40
+ new(
41
+ x402_version: hash["x402Version"],
42
+ resource: ResourceInfo.from_h(hash["resource"]),
43
+ accepted: PaymentRequirements.from_h(hash["accepted"]),
44
+ payload: hash["payload"],
45
+ extensions: hash["extensions"]
46
+ )
47
+ end
48
+
49
+ def self.decode(encoded_string)
50
+ from_h(Header.decode(encoded_string))
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Types
5
+ class PaymentRequired
6
+ attr_accessor :x402_version, :error, :resource, :accepts, :extensions
7
+
8
+ def initialize(resource:, accepts:, error: nil, x402_version: nil, extensions: nil)
9
+ @x402_version = x402_version || X402.configuration.x402_version
10
+ @error = error
11
+ @resource = resource.is_a?(ResourceInfo) ? resource : ResourceInfo.new(**resource)
12
+ @accepts = accepts.map do |a|
13
+ a.is_a?(PaymentRequirements) ? a : PaymentRequirements.new(**a)
14
+ end
15
+ @extensions = extensions
16
+ end
17
+
18
+ def to_h
19
+ h = {
20
+ x402Version: x402_version,
21
+ resource: resource.to_h,
22
+ accepts: accepts.map(&:to_h)
23
+ }
24
+ h[:error] = error if error
25
+ h[:extensions] = extensions if extensions
26
+ h
27
+ end
28
+
29
+ def encode
30
+ Header.encode(to_h)
31
+ end
32
+
33
+ def self.from_h(hash)
34
+ new(
35
+ x402_version: hash["x402Version"],
36
+ error: hash["error"],
37
+ resource: ResourceInfo.from_h(hash["resource"]),
38
+ accepts: (hash["accepts"] || []).map { |a| PaymentRequirements.from_h(a) },
39
+ extensions: hash["extensions"]
40
+ )
41
+ end
42
+
43
+ def self.decode(encoded_string)
44
+ from_h(Header.decode(encoded_string))
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Types
5
+ class PaymentRequirements
6
+ attr_accessor :scheme, :network, :amount, :asset, :pay_to,
7
+ :max_timeout_seconds, :extra
8
+
9
+ def initialize(scheme:, network:, amount:, asset:, pay_to:,
10
+ max_timeout_seconds: 60, extra: nil)
11
+ @scheme = scheme
12
+ @network = network
13
+ @amount = amount.to_s
14
+ @asset = asset
15
+ @pay_to = pay_to
16
+ @max_timeout_seconds = max_timeout_seconds
17
+ @extra = extra
18
+ end
19
+
20
+ def to_h
21
+ h = {
22
+ scheme: scheme,
23
+ network: network,
24
+ amount: amount,
25
+ asset: asset,
26
+ payTo: pay_to,
27
+ maxTimeoutSeconds: max_timeout_seconds
28
+ }
29
+ h[:extra] = extra if extra
30
+ h
31
+ end
32
+
33
+ def self.from_h(hash)
34
+ return nil if hash.nil?
35
+
36
+ new(
37
+ scheme: hash["scheme"],
38
+ network: hash["network"],
39
+ amount: hash["amount"],
40
+ asset: hash["asset"],
41
+ pay_to: hash["payTo"],
42
+ max_timeout_seconds: hash["maxTimeoutSeconds"] || 60,
43
+ extra: hash["extra"]
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Types
5
+ class ResourceInfo
6
+ attr_accessor :url, :description, :mime_type
7
+
8
+ def initialize(url:, description: nil, mime_type: nil)
9
+ @url = url
10
+ @description = description
11
+ @mime_type = mime_type
12
+ end
13
+
14
+ def to_h
15
+ h = {url: url}
16
+ h[:description] = description if description
17
+ h[:mimeType] = mime_type if mime_type
18
+ h
19
+ end
20
+
21
+ def self.from_h(hash)
22
+ return nil if hash.nil?
23
+
24
+ new(
25
+ url: hash["url"],
26
+ description: hash["description"],
27
+ mime_type: hash["mimeType"]
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Types
5
+ class SettlementResponse
6
+ attr_accessor :success, :error_reason, :payer, :transaction,
7
+ :network, :amount, :extensions
8
+
9
+ def initialize(success:, transaction:, network:, payer: nil,
10
+ error_reason: nil, amount: nil, extensions: nil)
11
+ @success = success
12
+ @transaction = transaction
13
+ @network = network
14
+ @payer = payer
15
+ @error_reason = error_reason
16
+ @amount = amount
17
+ @extensions = extensions
18
+ end
19
+
20
+ def success?
21
+ success
22
+ end
23
+
24
+ def to_h
25
+ h = {
26
+ success: success,
27
+ transaction: transaction,
28
+ network: network
29
+ }
30
+ h[:errorReason] = error_reason if error_reason
31
+ h[:payer] = payer if payer
32
+ h[:amount] = amount if amount
33
+ h[:extensions] = extensions if extensions
34
+ h
35
+ end
36
+
37
+ def encode
38
+ Header.encode(to_h)
39
+ end
40
+
41
+ def self.from_h(hash)
42
+ new(
43
+ success: hash["success"],
44
+ transaction: hash["transaction"],
45
+ network: hash["network"],
46
+ payer: hash["payer"],
47
+ error_reason: hash["errorReason"],
48
+ amount: hash["amount"],
49
+ extensions: hash["extensions"]
50
+ )
51
+ end
52
+
53
+ def self.decode(encoded_string)
54
+ from_h(Header.decode(encoded_string))
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Types
5
+ class VerifyResponse
6
+ attr_accessor :valid, :invalid_reason, :payer
7
+
8
+ def initialize(valid:, invalid_reason: nil, payer: nil)
9
+ @valid = valid
10
+ @invalid_reason = invalid_reason
11
+ @payer = payer
12
+ end
13
+
14
+ def valid?
15
+ valid
16
+ end
17
+
18
+ def self.from_h(hash)
19
+ new(
20
+ valid: hash["isValid"],
21
+ invalid_reason: hash["invalidReason"],
22
+ payer: hash["payer"]
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ VERSION = "0.1.0"
5
+ end
data/lib/x402.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "x402/version"
4
+ require_relative "x402/configuration"
5
+ require_relative "x402/header"
6
+ require_relative "x402/types/resource_info"
7
+ require_relative "x402/types/payment_requirements"
8
+ require_relative "x402/types/payment_required"
9
+ require_relative "x402/types/payment_payload"
10
+ require_relative "x402/types/settlement_response"
11
+ require_relative "x402/types/verify_response"
12
+ require_relative "x402/facilitator"
13
+ require_relative "x402/server/middleware"
14
+ require_relative "x402/client/middleware" if defined?(Faraday)
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: x402.rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Filippo
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ description: Enables HTTP 402-based on-chain payments for APIs and services using
41
+ the x402 open standard. Includes Rack server middleware, Faraday client middleware,
42
+ and a facilitator API client.
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - LICENSE
48
+ - README.md
49
+ - lib/x402.rb
50
+ - lib/x402/client/middleware.rb
51
+ - lib/x402/configuration.rb
52
+ - lib/x402/facilitator.rb
53
+ - lib/x402/header.rb
54
+ - lib/x402/server/middleware.rb
55
+ - lib/x402/types/payment_payload.rb
56
+ - lib/x402/types/payment_required.rb
57
+ - lib/x402/types/payment_requirements.rb
58
+ - lib/x402/types/resource_info.rb
59
+ - lib/x402/types/settlement_response.rb
60
+ - lib/x402/types/verify_response.rb
61
+ - lib/x402/version.rb
62
+ homepage: https://github.com/filippo/x402.rb
63
+ licenses:
64
+ - MIT
65
+ metadata:
66
+ homepage_uri: https://github.com/filippo/x402.rb
67
+ source_code_uri: https://github.com/filippo/x402.rb
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.1'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 4.0.8
83
+ specification_version: 4
84
+ summary: Ruby implementation of the x402 payment protocol
85
+ test_files: []