Remove Synth Finance references (#47)

* Remove Synth Finance integration

* Linter noise

* Fix failing (old) test, use it for Twelve Data

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
Juan José Mata
2025-08-01 15:28:55 -07:00
committed by GitHub
parent 656f7e9495
commit 54bc37a651
38 changed files with 35 additions and 1283 deletions

View File

@@ -18,8 +18,7 @@ This rule serves as high-level documentation for how you should write code for t
- Jobs: Sidekiq + Redis
- External
- Payments: Stripe
- User bank data syncing: Plaid
- Market data: Synth (our custom API)
- User bank data syncing: Plaid
## Project conventions

View File

@@ -158,20 +158,11 @@ app/models/
registry.rb <- Defines available providers by concept
concepts/
exchange_rate.rb <- defines the interface required for the exchange rate concept
synth.rb # <- Concrete provider implementation
```
### One-off data
For data that does not fit neatly into a "concept", an interface is not required and the concrete provider may implement ad-hoc methods called directly in code. For example, the [synth.rb](mdc:app/models/provider/synth.rb) provider has a `usage` method that is only applicable to this specific provider. This should be called directly without any abstractions:
```rb
class SomeModel < Application
def synth_usage
Provider::Registry.get_provider(:synth)&.usage
end
end
```
For data that does not fit neatly into a "concept", an interface is not required and the concrete provider may implement ad-hoc methods called directly in code.
## "Provided" Concerns

View File

@@ -17,10 +17,6 @@ SECRET_KEY_BASE=secret-value
# Optional self-hosting vars
# --------------------------------------------------------------------------------------------------------
# Optional: Synth API Key for exchange rates + stock prices
# (you can also set this in your self-hosted settings page)
# Get it here: https://synthfinance.com/
SYNTH_API_KEY=
# Optional: Twelve Data API Key for exchange rates + stock prices
# (you can also set this in your self-hosted settings page)

View File

@@ -1,6 +1,5 @@
# To enable / disable self-hosting features.
SELF_HOSTED=false
# Enable Synth market data (careful, this will use your API credits)
SYNTH_API_KEY=yourapikeyhere
# Enable Twelve market data (careful, this will use your API credits)
TWELVE_DATA_API_KEY=yourapikeyhere

View File

@@ -6,7 +6,6 @@ SELF_HOSTED=false
# Uncomment and fill in live keys when you need to generate a VCR cassette fixture
# ================
# SYNTH_API_KEY=<add live key here>
# ================
# Miscellaneous

View File

@@ -115,7 +115,6 @@ Sidekiq handles asynchronous tasks:
### Multi-Currency Support
- All monetary values stored in base currency (user's primary currency)
- Exchange rates fetched from Synth API
- `Money` objects handle currency conversion and formatting
- Historical exchange rates for accurate reporting

View File

@@ -6,9 +6,6 @@ class Settings::HostingsController < ApplicationController
before_action :ensure_admin, only: :clear_cache
def show
synth_provider = Provider::Registry.get_provider(:synth)
@synth_usage = synth_provider&.usage
twelve_data_provider = Provider::Registry.get_provider(:twelve_data)
@twelve_data_usage = twelve_data_provider&.usage
end
@@ -22,10 +19,6 @@ class Settings::HostingsController < ApplicationController
Setting.require_email_confirmation = hosting_params[:require_email_confirmation]
end
if hosting_params.key?(:synth_api_key)
Setting.synth_api_key = hosting_params[:synth_api_key]
end
if hosting_params.key?(:twelve_data_api_key)
Setting.twelve_data_api_key = hosting_params[:twelve_data_api_key]
end
@@ -43,7 +36,7 @@ class Settings::HostingsController < ApplicationController
private
def hosting_params
params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :synth_api_key, :twelve_data_api_key)
params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :twelve_data_api_key)
end
def ensure_admin

View File

@@ -36,9 +36,7 @@ class Family::AutoMerchantDetector
source: "ai",
name: auto_detection.business_name,
website_url: auto_detection.business_url,
) do |pm|
pm.logo_url = "#{default_logo_provider_url}/#{auto_detection.business_url}"
end
)
end
merchant_id = merchant_id || ai_provider_merchant&.id
@@ -65,9 +63,6 @@ class Family::AutoMerchantDetector
Provider::Registry.get_provider(:openai)
end
def default_logo_provider_url
"https://logo.synthfinance.com"
end
def user_merchants_input
family.merchants.map do |merchant|

View File

@@ -32,14 +32,6 @@ class Provider::Registry
Provider::Stripe.new(secret_key:, webhook_secret:)
end
def synth
api_key = ENV.fetch("SYNTH_API_KEY", Setting.synth_api_key)
return nil unless api_key.present?
Provider::Synth.new(api_key)
end
def twelve_data
api_key = ENV.fetch("TWELVE_DATA_API_KEY", Setting.twelve_data_api_key)
@@ -100,13 +92,13 @@ class Provider::Registry
def available_providers
case concept
when :exchange_rates
%i[synth twelve_data]
%i[twelve_data]
when :securities
%i[synth twelve_data]
%i[twelve_data]
when :llm
%i[openai]
else
%i[synth plaid_us plaid_eu github openai]
%i[plaid_us plaid_eu github openai]
end
end
end

View File

@@ -1,250 +0,0 @@
class Provider::Synth < Provider
include ExchangeRateConcept, SecurityConcept
# Subclass so errors caught in this provider are raised as Provider::Synth::Error
Error = Class.new(Provider::Error)
InvalidExchangeRateError = Class.new(Error)
InvalidSecurityPriceError = Class.new(Error)
def initialize(api_key)
@api_key = api_key
end
def healthy?
with_provider_response do
response = client.get("#{base_url}/user")
JSON.parse(response.body).dig("id").present?
end
end
def usage
with_provider_response do
response = client.get("#{base_url}/user")
parsed = JSON.parse(response.body)
remaining = parsed.dig("api_calls_remaining")
limit = parsed.dig("api_limit")
used = limit - remaining
UsageData.new(
used: used,
limit: limit,
utilization: used.to_f / limit * 100,
plan: parsed.dig("plan"),
)
end
end
# ================================
# Exchange Rates
# ================================
def fetch_exchange_rate(from:, to:, date:)
with_provider_response do
response = client.get("#{base_url}/rates/historical") do |req|
req.params["date"] = date.to_s
req.params["from"] = from
req.params["to"] = to
end
rates = JSON.parse(response.body).dig("data", "rates")
Rate.new(date: date.to_date, from:, to:, rate: rates.dig(to))
end
end
def fetch_exchange_rates(from:, to:, start_date:, end_date:)
with_provider_response do
data = paginate(
"#{base_url}/rates/historical-range",
from: from,
to: to,
date_start: start_date.to_s,
date_end: end_date.to_s
) do |body|
body.dig("data")
end
data.paginated.map do |rate|
date = rate.dig("date")
rate = rate.dig("rates", to)
if date.nil? || rate.nil?
Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} on: #{date}. Rate data: #{rate.inspect}")
Sentry.capture_exception(InvalidExchangeRateError.new("#{self.class.name} returned invalid rate data"), level: :warning) do |scope|
scope.set_context("rate", { from: from, to: to, date: date })
end
next
end
Rate.new(date: date.to_date, from:, to:, rate:)
end.compact
end
end
# ================================
# Securities
# ================================
def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
with_provider_response do
response = client.get("#{base_url}/tickers/search") do |req|
req.params["name"] = symbol
req.params["dataset"] = "limited"
req.params["country_code"] = country_code if country_code.present?
# Synth uses mic_code, which encompasses both exchange_mic AND exchange_operating_mic (union)
req.params["mic_code"] = exchange_operating_mic if exchange_operating_mic.present?
req.params["limit"] = 25
end
parsed = JSON.parse(response.body)
parsed.dig("data").map do |security|
Security.new(
symbol: security.dig("symbol"),
name: security.dig("name"),
logo_url: security.dig("logo_url"),
exchange_operating_mic: security.dig("exchange", "operating_mic_code"),
country_code: security.dig("exchange", "country_code")
)
end
end
end
def fetch_security_info(symbol:, exchange_operating_mic:)
with_provider_response do
response = client.get("#{base_url}/tickers/#{symbol}") do |req|
req.params["operating_mic"] = exchange_operating_mic
end
data = JSON.parse(response.body).dig("data")
SecurityInfo.new(
symbol: symbol,
name: data.dig("name"),
links: data.dig("links"),
logo_url: data.dig("logo_url"),
description: data.dig("description"),
kind: data.dig("kind"),
exchange_operating_mic: exchange_operating_mic
)
end
end
def fetch_security_price(symbol:, exchange_operating_mic: nil, date:)
with_provider_response do
historical_data = fetch_security_prices(symbol:, exchange_operating_mic:, start_date: date, end_date: date)
raise ProviderError, "No prices found for security #{symbol} on date #{date}" if historical_data.data.empty?
historical_data.data.first
end
end
def fetch_security_prices(symbol:, exchange_operating_mic: nil, start_date:, end_date:)
with_provider_response do
params = {
start_date: start_date,
end_date: end_date,
operating_mic_code: exchange_operating_mic
}.compact
data = paginate(
"#{base_url}/tickers/#{symbol}/open-close",
params
) do |body|
body.dig("prices")
end
currency = data.first_page.dig("currency")
exchange_operating_mic = data.first_page.dig("exchange", "operating_mic_code")
data.paginated.map do |price|
date = price.dig("date")
price = price.dig("close") || price.dig("open")
if date.nil? || price.nil?
Rails.logger.warn("#{self.class.name} returned invalid price data for security #{symbol} on: #{date}. Price data: #{price.inspect}")
Sentry.capture_exception(InvalidSecurityPriceError.new("#{self.class.name} returned invalid security price data"), level: :warning) do |scope|
scope.set_context("security", { symbol: symbol, date: date })
end
next
end
Price.new(
symbol: symbol,
date: date.to_date,
price: price,
currency: currency,
exchange_operating_mic: exchange_operating_mic
)
end.compact
end
end
private
attr_reader :api_key
def base_url
ENV["SYNTH_URL"] || "https://api.synthfinance.com"
end
def app_name
"maybe_app"
end
def app_type
Rails.application.config.app_mode
end
def client
@client ||= Faraday.new(url: base_url) do |faraday|
faraday.request(:retry, {
max: 2,
interval: 0.05,
interval_randomness: 0.5,
backoff_factor: 2
})
faraday.response :raise_error
faraday.headers["Authorization"] = "Bearer #{api_key}"
faraday.headers["X-Source"] = app_name
faraday.headers["X-Source-Type"] = app_type
end
end
def fetch_page(url, page, params = {})
client.get(url, params.merge(page: page))
end
def paginate(url, params = {})
results = []
page = 1
current_page = 0
total_pages = 1
first_page = nil
while current_page < total_pages
response = fetch_page(url, page, params)
body = JSON.parse(response.body)
first_page = body unless first_page
page_results = yield(body)
results.concat(page_results)
current_page = body.dig("paging", "current_page")
total_pages = body.dig("paging", "total_pages")
page += 1
end
PaginatedData.new(
paginated: results,
first_page: first_page,
total_pages: total_pages
)
end
end

View File

@@ -18,7 +18,7 @@ class Security < ApplicationRecord
end
def to_combobox_option
SynthComboboxOption.new(
ComboboxOption.new(
symbol: ticker,
name: name,
logo_url: logo_url,

View File

@@ -0,0 +1,13 @@
class Security::ComboboxOption
include ActiveModel::Model
attr_accessor :symbol, :name, :logo_url, :exchange_operating_mic, :country_code
def id
"#{symbol}|#{exchange_operating_mic}"
end
def to_combobox_display
"#{symbol} - #{name} (#{exchange_operating_mic})"
end
end

View File

@@ -1,13 +0,0 @@
class Security::SynthComboboxOption
include ActiveModel::Model
attr_accessor :symbol, :name, :logo_url, :exchange_operating_mic, :country_code
def id
"#{symbol}|#{exchange_operating_mic}" # submitted by combobox as value
end
def to_combobox_display
"#{symbol} - #{name} (#{exchange_operating_mic})" # shown in combobox input when selected
end
end

View File

@@ -2,7 +2,6 @@
class Setting < RailsSettings::Base
cache_prefix { "v1" }
field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
field :twelve_data_api_key, type: :string, default: ENV["TWELVE_DATA_API_KEY"]
field :openai_access_token, type: :string, default: ENV["OPENAI_ACCESS_TOKEN"]

View File

@@ -18,7 +18,7 @@ class Trade::CreateForm
end
private
# Users can either look up a ticker from our provider (Synth) or enter a manual, "offline" ticker (that we won't fetch prices for)
# Users can either look up a ticker from a provider or enter a manual, "offline" ticker (that we won't fetch prices for)
def security
ticker_symbol, exchange_operating_mic = ticker.present? ? ticker.split("|") : [ manual_ticker, nil ]

View File

@@ -7,9 +7,7 @@
"full" => "w-full h-full"
} %>
<% if account.plaid_account_id? && account.institution_domain.present? %>
<%= image_tag "https://logo.synthfinance.com/#{account.institution_domain}", class: "shrink-0 rounded-full #{size_classes[size]}" %>
<% elsif account.logo.attached? %>
<% if account.logo.attached? %>
<%= image_tag account.logo, class: "shrink-0 rounded-full #{size_classes[size]}" %>
<% else %>
<%= render DS::FilledIcon.new(variant: :text, hex_color: color || account.accountable.color, text: account.name, size: size, rounded: true) %>

View File

@@ -3,7 +3,9 @@
<%= turbo_frame_tag dom_id(holding) do %>
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4">
<div class="col-span-4 flex items-center gap-4">
<%= image_tag "https://logo.synthfinance.com/ticker/#{holding.ticker}", class: "w-9 h-9 rounded-full", loading: "lazy" %>
<% if holding.security.logo_url.present? %>
<%= image_tag holding.security.logo_url, class: "w-9 h-9 rounded-full", loading: "lazy" %>
<% end %>
<div class="space-y-0.5">
<%= link_to holding.name, holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>

View File

@@ -6,7 +6,9 @@
<%= tag.p @holding.ticker, class: "text-sm text-secondary" %>
</div>
<%= image_tag "https://logo.synthfinance.com/ticker/#{@holding.ticker}", loading: "lazy", class: "w-9 h-9 rounded-full" %>
<% if @holding.security.logo_url.present? %>
<%= image_tag @holding.security.logo_url, loading: "lazy", class: "w-9 h-9 rounded-full" %>
<% end %>
</div>
<% end %>

View File

@@ -1,49 +0,0 @@
<div class="space-y-4">
<div>
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
<% if ENV["SYNTH_API_KEY"].present? %>
<p class="text-sm text-secondary">You have successfully configured your Synth API key through the SYNTH_API_KEY environment variable.</p>
<% else %>
<p class="text-secondary text-sm mb-4"><%= t(".description") %></p>
<% end %>
</div>
<%= styled_form_with model: Setting.new,
url: settings_hosting_path,
method: :patch,
data: {
controller: "auto-submit-form",
"auto-submit-form-trigger-event-value": "blur"
} do |form| %>
<%= form.text_field :synth_api_key,
label: t(".label"),
type: "password",
placeholder: t(".placeholder"),
value: ENV.fetch("SYNTH_API_KEY", Setting.synth_api_key),
disabled: ENV["SYNTH_API_KEY"].present?,
container_class: @synth_usage.present? && !@synth_usage.success? ? "border-red-500" : "",
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<% if @synth_usage.present? && @synth_usage.success? %>
<div class="space-y-4">
<div class="space-y-2">
<p class="text-sm text-secondary">
<%= t(".api_calls_used",
used: number_with_delimiter(@synth_usage.data.used),
limit: number_with_delimiter(@synth_usage.data.limit),
percentage: number_to_percentage(@synth_usage.data.utilization, precision: 1)) %>
</p>
<div class="w-52 h-1.5 bg-gray-100 rounded-2xl">
<div class="h-full bg-green-500 rounded-2xl"
style="width: <%= [@synth_usage.data.utilization, 2].max %>%;"></div>
</div>
</div>
<div class="bg-gray-100 rounded-md px-1.5 py-0.5 w-fit">
<p class="text-xs font-medium text-secondary uppercase">
<%= t(".plan", plan: @synth_usage.data.plan) %>
</p>
</div>
</div>
<% end %>
</div>

View File

@@ -1,9 +1,6 @@
<%= content_for :page_title, t(".title") %>
<%= settings_section title: t(".general") do %>
<div class="space-y-6">
<%= render "settings/hostings/synth_settings" %>
</div>
<div class="space-y-6">
<%= render "settings/hostings/twelve_data_settings" %>
</div>

View File

@@ -21,13 +21,6 @@ en:
confirm_clear_cache:
title: Clear data cache?
body: Are you sure you want to clear the data cache? This will remove all exchange rates, security prices, account balances, and other data. This action cannot be undone.
synth_settings:
api_calls_used: "%{used} / %{limit} API calls used (%{percentage})"
description: Input the API key provided by Synth
label: API Key
placeholder: Enter your API key here
plan: "%{plan} plan"
title: Synth Settings
twelve_data_settings:
api_calls_used: "%{used} / %{limit} API daily calls used (%{percentage})"
description: Input the API key provided by Twelve Data

View File

@@ -20,13 +20,6 @@ nb:
confirm_clear_cache:
title: Tøm cache?
body: Er du sikker på at du vil tømme cache? Dette vil fjerne alle valutakurser, verdipapirpriser, kontobalanser og andre data. Denne handlingen kan ikke angres.
synth_settings:
api_calls_used: "%{used} / %{limit} API-kall brukt (%{percentage})"
description: Angi API-nøkkelen som er gitt av Synth
label: API-nøkkel
placeholder: Angi API-nøkkelen din her
plan: "%{plan} plan"
title: Synth-innstillinger
update:
failure: Ugyldig innstillingsverdi
success: Innstillinger oppdatert

View File

@@ -1,64 +1,6 @@
# frozen_string_literal: true
namespace :securities do
desc "Backfill exchange_operating_mic for securities using Synth API"
task backfill_exchange_mic: :environment do
puts "Starting exchange_operating_mic backfill..."
api_key = Rails.application.config.app_mode.self_hosted? ? Setting.synth_api_key : ENV["SYNTH_API_KEY"]
unless api_key.present?
puts "ERROR: No Synth API key found. Please set SYNTH_API_KEY env var or configure it in Settings for self-hosted mode."
exit 1
end
securities = Security.where(exchange_operating_mic: nil).where.not(ticker: nil)
total = securities.count
processed = 0
errors = []
securities.find_each do |security|
processed += 1
print "\rProcessing #{processed}/#{total} (#{(processed.to_f/total * 100).round(1)}%)"
begin
response = Faraday.get("https://api.synthfinance.com/tickers/#{security.ticker}") do |req|
req.params["country_code"] = security.country_code if security.country_code.present?
req.headers["Authorization"] = "Bearer #{api_key}"
end
if response.success?
data = JSON.parse(response.body).dig("data")
exchange_data = data["exchange"]
# Update security with exchange info and other metadata
security.update!(
exchange_operating_mic: exchange_data["operating_mic_code"],
exchange_mic: exchange_data["mic_code"],
exchange_acronym: exchange_data["acronym"],
name: data["name"],
logo_url: data["logo_url"],
country_code: exchange_data["country_code"]
)
else
errors << "#{security.ticker}: HTTP #{response.status} - #{response.body}"
end
rescue => e
errors << "#{security.ticker}: #{e.message}"
end
# Add a small delay to not overwhelm the API
sleep(0.1)
end
puts "\n\nBackfill complete!"
puts "Processed #{processed} securities"
if errors.any?
puts "\nErrors encountered:"
errors.each { |error| puts " - #{error}" }
end
end
desc "De-duplicate securities based on ticker + exchange_operating_mic"
task :deduplicate, [ :dry_run ] => :environment do |_t, args|
dry_run = args[:dry_run].present?

View File

@@ -8,7 +8,6 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
sign_in users(:family_admin)
@provider = mock
Provider::Registry.stubs(:get_provider).with(:synth).returns(@provider)
Provider::Registry.stubs(:get_provider).with(:twelve_data).returns(@provider)
@usage_response = provider_success_response(
OpenStruct.new(
@@ -32,7 +31,6 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
test "should get edit when self hosting is enabled" do
@provider.expects(:usage).returns(@usage_response)
@provider.expects(:usage).returns(@usage_response)
with_self_hosting do
get settings_hosting_url
@@ -42,9 +40,9 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
test "can update settings when self hosting is enabled" do
with_self_hosting do
patch settings_hosting_url, params: { setting: { synth_api_key: "1234567890" } }
patch settings_hosting_url, params: { setting: { twelve_data_api_key: "1234567890" } }
assert_equal "1234567890", Setting.synth_api_key
assert_equal "1234567890", Setting.twelve_data_api_key
end
end

View File

@@ -27,7 +27,7 @@ module SecurityProviderInterfaceTest
assert response.success?
assert response.data.first.date.is_a?(Date)
assert_equal 147, response.data.count # Synth won't return prices on weekends / holidays, so less than total day count of 213
assert_equal 147, response.data.count
end
end

View File

@@ -29,8 +29,8 @@ class Family::AutoMerchantDetectorTest < ActiveSupport::TestCase
assert_equal "McDonalds", txn1.reload.merchant.name
assert_equal "Chipotle", txn2.reload.merchant.name
assert_equal "https://logo.synthfinance.com/mcdonalds.com", txn1.reload.merchant.logo_url
assert_equal "https://logo.synthfinance.com/chipotle.com", txn2.reload.merchant.logo_url
assert_nil txn1.reload.merchant.logo_url
assert_nil txn2.reload.merchant.logo_url
assert_nil txn3.reload.merchant
# After auto-detection, all transactions are locked and no longer enrichable

View File

@@ -1,27 +0,0 @@
require "test_helper"
class Provider::RegistryTest < ActiveSupport::TestCase
test "synth configured with ENV" do
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: "123" do
assert_instance_of Provider::Synth, Provider::Registry.get_provider(:synth)
end
end
test "synth configured with Setting" do
Setting.stubs(:synth_api_key).returns("123")
with_env_overrides SYNTH_API_KEY: nil do
assert_instance_of Provider::Synth, Provider::Registry.get_provider(:synth)
end
end
test "synth not configured" do
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: nil do
assert_nil Provider::Registry.get_provider(:synth)
end
end
end

View File

@@ -1,26 +0,0 @@
require "test_helper"
require "ostruct"
class Provider::SynthTest < ActiveSupport::TestCase
include ExchangeRateProviderInterfaceTest, SecurityProviderInterfaceTest
setup do
@subject = @synth = Provider::Synth.new(ENV["SYNTH_API_KEY"])
end
test "health check" do
VCR.use_cassette("synth/health") do
assert @synth.healthy?
end
end
test "usage info" do
VCR.use_cassette("synth/usage") do
usage = @synth.usage.data
assert usage.used.present?
assert usage.limit.present?
assert usage.utilization.present?
assert usage.plan.present?
end
end
end

View File

@@ -33,7 +33,6 @@ class SettingsTest < ApplicationSystemTestCase
test "can update self hosting settings" do
Rails.application.config.app_mode.stubs(:self_hosted?).returns(true)
Provider::Registry.stubs(:get_provider).with(:synth).returns(nil)
Provider::Registry.stubs(:get_provider).with(:twelve_data).returns(nil)
open_settings_from_sidebar
assert_selector "li", text: "Self hosting"

View File

@@ -28,7 +28,6 @@ VCR.configure do |config|
config.hook_into :webmock
config.ignore_localhost = true
config.default_cassette_options = { erb: true }
config.filter_sensitive_data("<SYNTH_API_KEY>") { ENV["SYNTH_API_KEY"] }
config.filter_sensitive_data("<OPENAI_ACCESS_TOKEN>") { ENV["OPENAI_ACCESS_TOKEN"] }
config.filter_sensitive_data("<OPENAI_ORGANIZATION_ID>") { ENV["OPENAI_ORGANIZATION_ID"] }
config.filter_sensitive_data("<STRIPE_SECRET_KEY>") { ENV["STRIPE_SECRET_KEY"] }

View File

@@ -1,81 +0,0 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/rates/historical?date=2024-01-01&from=USD&to=GBP
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.13.1
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Fri, 16 May 2025 13:01:38 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"0c93a67d0c68e6f206e2954a41aa2933"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 146e30b2-e03b-47e3
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- 3cf7ade1-8066-422a-97c7-5f8b99e24296
X-Runtime:
- '0.024284'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ih8sEFqAOWyINqAEtKGKPKO2lr1qAYSVeipyB5F8g2umPODXvCD4hN3G6wTTs2Q7H8CDWsqiOlYkmVvmr%2BWvl2ojOtBwO25Ahk9TbhlcgRO9nT6mEIXOSdVXJpzpRn5Ov%2FMGigpQ"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 940b109b5df1a3d7-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=25865&min_rtt=25683&rtt_var=9996&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=922&delivery_rate=106690&cwnd=219&unsent_bytes=0&cid=e48ae188d1f86721&ts=190&x=0"
body:
encoding: ASCII-8BIT
string: '{"data":{"date":"2024-01-01","source":"USD","rates":{"GBP":0.785476}},"meta":{"total_records":1,"credits_used":1,"credits_remaining":249734,"date":"2024-01-01"}}'
recorded_at: Fri, 16 May 2025 13:01:38 GMT
recorded_with: VCR 6.3.1

File diff suppressed because one or more lines are too long

View File

@@ -1,82 +0,0 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/user
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.13.1
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Fri, 16 May 2025 13:01:39 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"c5c1d51b68b499d00936c9eb1e8bfdbb"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 3abc1256-5517-44a7
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- aaf85301-dd16-4b9b-a3a4-c4fbcf1d3f55
X-Runtime:
- '0.014386'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=OaVSdNPSl6CQ8gbhnDzkCisX2ILOEWAwweMW3rXXP5rBKuxZoDT024srQWmHKGLsCEhpt4G9mqCthDwlHu2%2BuZ3AyTJQcnBONtE%2FNQ7fKT9x8nLz4mnqL8iyynLuRWQSUJ8SWMj5"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 940b109d086eb4b8-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=32457&min_rtt=26792&rtt_var=14094&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2826&recv_bytes=878&delivery_rate=108091&cwnd=229&unsent_bytes=0&cid=a6f330e4d5f16682&ts=309&x=0"
body:
encoding: ASCII-8BIT
string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"zach@maybe.co","name":"Zach
Gollwitzer","plan":"Business","api_calls_remaining":249733,"api_limit":250000,"credits_reset_at":"2025-06-01T00:00:00.000-04:00","current_period_start":"2025-05-01T00:00:00.000-04:00"}'
recorded_at: Fri, 16 May 2025 13:01:39 GMT
recorded_with: VCR 6.3.1

View File

@@ -1,105 +0,0 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/tickers/AAPL?operating_mic=XNAS
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.13.1
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Fri, 16 May 2025 13:01:37 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"75f336ad88e262c72044e8b865265298"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- ba973abf-7d96-4a9a
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- 76cb13a6-0d7e-4c36-8df9-bb63110d9e2a
X-Runtime:
- '0.099716'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=aDn7ApAO9Ma86gZ%2BJKCUCFjH2Re%2BtXdB5gcqYj2KTGXJKNpgf5TNgzbrp5%2Bw%2FGL5nTvtp%2B7cxT8MMcLWjAV6Ne1r6z5YBFq1K4W7Zw5m1lhMiqYLnTnEs2Oq85TjzOvpsE%2BmC33d"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 940b10910abdd2ec-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=28163&min_rtt=27237&rtt_var=12066&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=905&delivery_rate=83590&cwnd=239&unsent_bytes=0&cid=7ef62bd693b52ccd&ts=240&x=0"
body:
encoding: ASCII-8BIT
string: '{"data":{"ticker":"AAPL","name":"Apple Inc.","links":{"homepage_url":"https://www.apple.com"},"logo_url":"https://logo.synthfinance.com/ticker/AAPL","description":"Apple
Inc. designs, manufactures, and markets smartphones, personal computers, tablets,
wearables, and accessories worldwide. The company offers iPhone, a line of
smartphones; Mac, a line of personal computers; iPad, a line of multi-purpose
tablets; and wearables, home, and accessories comprising AirPods, Apple TV,
Apple Watch, Beats products, and HomePod. It also provides AppleCare support
and cloud services; and operates various platforms, including the App Store
that allow customers to discover and download applications and digital content,
such as books, music, video, games, and podcasts. In addition, the company
offers various services, such as Apple Arcade, a game subscription service;
Apple Fitness+, a personalized fitness service; Apple Music, which offers
users a curated listening experience with on-demand radio stations; Apple
News+, a subscription news and magazine service; Apple TV+, which offers exclusive
original content; Apple Card, a co-branded credit card; and Apple Pay, a cashless
payment service, as well as licenses its intellectual property. The company
serves consumers, and small and mid-sized businesses; and the education, enterprise,
and government markets. It distributes third-party applications for its products
through the App Store. The company also sells its products through its retail
and online stores, and direct sales force; and third-party cellular network
carriers, wholesalers, retailers, and resellers. Apple Inc. was founded in
1976 and is headquartered in Cupertino, California.","kind":"common stock","cik":"0000320193","currency":"USD","address":{"country":"USA","address_line1":"One
Apple Park Way","city":"Cupertino","state":"CA","postal_code":"95014"},"exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"},"ceo":"Mr. Timothy
D. Cook","founding_year":1976,"industry":"Consumer Electronics","sector":"Technology","phone":"408-996-1010","total_employees":161000,"composite_figi":"BBG000B9Y5X2","market_data":{"high_today":212.96,"low_today":209.54,"open_today":210.95,"close_today":211.45,"volume_today":44979900.0,"fifty_two_week_high":260.1,"fifty_two_week_low":169.21,"average_volume":61769396.875,"price_change":0.0,"percent_change":0.0}},"meta":{"credits_used":1,"credits_remaining":249737}}'
recorded_at: Fri, 16 May 2025 13:01:37 GMT
recorded_with: VCR 6.3.1

View File

@@ -1,83 +0,0 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/tickers/AAPL/open-close?end_date=2024-08-01&operating_mic_code=XNAS&page=1&start_date=2024-08-01
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.13.1
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Fri, 16 May 2025 13:01:36 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"72340d82266397447b865407dda15492"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 4c3462aa-2471-40b4
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- bdbc757d-2528-44c3-ae08-9788e8ee15f7
X-Runtime:
- '0.034898'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=2Mu4PK4XTsAq%2Bn1%2F2yxy%2Blj7kz3ZCiQ9t8ikr2m19BrhQhrqfeUQfPwxbLc1WIgGMIxpPInKYtDVIX3En%2FGpTNQLAeu%2FpuLKv%2BRmCx%2B7u28od5L%2F9%2BLmEhFWqJjs8Y6C1O2a3SKv"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 940b108f29129d03-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=27793&min_rtt=26182&rtt_var=13041&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=970&delivery_rate=74111&cwnd=244&unsent_bytes=0&cid=9bcc030369a615fb&ts=210&x=0"
body:
encoding: ASCII-8BIT
string: '{"ticker":"AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs (Global
Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-08-01","open":224.37,"close":218.36,"high":224.48,"low":217.02,"volume":62501000}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","total_records":1,"current_page":1,"per_page":100,"total_pages":1},"meta":{"total_records":1,"credits_used":1,"credits_remaining":249738}}'
recorded_at: Fri, 16 May 2025 13:01:36 GMT
recorded_with: VCR 6.3.1

File diff suppressed because one or more lines are too long

View File

@@ -1,104 +0,0 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/tickers/search?country_code=US&dataset=limited&limit=25&name=AAPL
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.13.1
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Fri, 16 May 2025 13:01:38 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"3e444869eacbaf17006766a691cc8fdc"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 701ae22a-18c8-4e62
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- edb55bc6-e3ea-470b-b7af-9b4d9883420b
X-Runtime:
- '0.355152'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=QGeBWdYED%2F%2FgT9BzborFAnM%2FG6UiNmI0ej212XGHWdFwYXUvTJ2GyqA9hMJrpYIvgbHdQ9Ed0MsQUv3KFb57VXQq0T6UXTNPa%2BFRPepK0hsXeGDLxch04v6KnkTATqcw2M8HuYHS"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 940b1097a830f856-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=26401&min_rtt=25556&rtt_var=11273&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2825&recv_bytes=939&delivery_rate=89615&cwnd=244&unsent_bytes=0&cid=cf6d0758d165295d&ts=500&x=0"
body:
encoding: ASCII-8BIT
string: '{"data":[{"symbol":"AAPL","name":"Apple Inc.","logo_url":"https://logo.synthfinance.com/ticker/AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"APLY","isin":"US88634T8577","name":"YieldMax
AAPL Option Income ETF","logo_url":"https://logo.synthfinance.com/ticker/APLY","currency":"USD","exchange":{"name":"Nyse
Arca","mic_code":"ARCX","operating_mic_code":"XNYS","acronym":"NYSE","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPD","name":"Direxion
Daily AAPL Bear 1X ETF","logo_url":"https://logo.synthfinance.com/ticker/AAPD","currency":"USD","exchange":{"name":"Nasdaq/Nms
(Global Market)","mic_code":"XNMS","operating_mic_code":"XNAS","acronym":"","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPU","isin":"US25461A8743","name":"Direxion
Daily AAPL Bull 2X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPU","currency":"USD","exchange":{"name":"Nasdaq/Nms
(Global Market)","mic_code":"XNMS","operating_mic_code":"XNAS","acronym":"","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPB","isin":"XXXXXXXR8842","name":"GraniteShares
2x Long AAPL Daily ETF","logo_url":"https://logo.synthfinance.com/ticker/AAPB","currency":"USD","exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPD","isin":"US25461A3041","name":"Direxion
Daily AAPL Bear 1X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPD","currency":"USD","exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPU","isin":"US25461A8743","name":"Direxion
Daily AAPL Bull 1.5X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPU","currency":"USD","exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPJ","isin":"US00037T1034","name":"AAP,
Inc.","logo_url":"https://logo.synthfinance.com/ticker/AAPJ","currency":"USD","exchange":{"name":"Otc
Pink Marketplace","mic_code":"PINX","operating_mic_code":"OTCM","acronym":"","country":"United
States","country_code":"US","timezone":"America/New_York"}}]}'
recorded_at: Fri, 16 May 2025 13:01:38 GMT
recorded_with: VCR 6.3.1

View File

@@ -1,82 +0,0 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/user
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.13.1
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Fri, 16 May 2025 13:01:36 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"7b8c2bf0cba54bc26b78bdc6e611dcbd"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 1b53adf6-b391-45b2
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- f88670a2-81d2-48b6-8d73-a911c846e330
X-Runtime:
- '0.018749'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=oH4OsWB6itK0jpi%2FPs%2BswVyCZIbkJGPfyJaoR4TKFtTAfmnqa8Lp6aZhv22WKzotXJuAKbh99VdYdZIOkeIPWbYTc6j4rGw%2BkQB3Hw%2Fc44QxDBJFdIo6wJNe8TGiPAZ%2BvgoBVHWn"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 940b108c38f66392-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=33369&min_rtt=25798&rtt_var=15082&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2826&recv_bytes=878&delivery_rate=112256&cwnd=205&unsent_bytes=0&cid=1b13324eb0768fd3&ts=285&x=0"
body:
encoding: ASCII-8BIT
string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"zach@maybe.co","name":"Zach
Gollwitzer","plan":"Business","api_calls_remaining":249738,"api_limit":250000,"credits_reset_at":"2025-06-01T00:00:00.000-04:00","current_period_start":"2025-05-01T00:00:00.000-04:00"}'
recorded_at: Fri, 16 May 2025 13:01:36 GMT
recorded_with: VCR 6.3.1