Ruby AI Visibility API
Query the raw answers ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Copilot and Gemini actually give — from Ruby, with nothing but Net::HTTP and JSON from the standard library. POST a query to https://api.agentgeo.org/v1/fetches with your ag_live_ key and the answer text, citations and sources come back as a clean Hash. No Ferrum, no Selenium, no Capybara driving a browser outside your test suite. AgentGEO is the answer-access layer for AI visibility. Your own code (or your users' AI agent over MCP) runs the GEO/AEO analysis — the API just hands you the raw data, structured and identical across all six engines.
Query the raw answers ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Copilot and Gemini actually return — from Ruby, with Net::HTTP and JSON, no gems. No Ferrum, no headless Chrome to drive outside your specs: one POST, and the answer text, citations and sources parse into a Hash your service objects and jobs can act on.
Get your free API key → · Read the quickstart → — Free tier, no credit card required.
Start for freeCall the AI Visibility API from Ruby
POST to https://api.agentgeo.org/v1/fetches with your ag_live_ key as a bearer token and the engines you want in the surfaces array. The whole integration is standard library — net/http for the call, json for parsing — so it drops into a plain script, a Rake task or a Rails service object unchanged. (Want to see the payload first? The raw answer playground runs a query in the browser, no signup.)
require "net/http"
require "json"
require "uri"
def fetch_run(query, *surfaces)
uri = URI("https://api.agentgeo.org/v1/fetches")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer ag_live_your_key_here"
req["Content-Type"] = "application/json"
req.body = JSON.generate(query: query, surfaces: surfaces)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
raise "agentgeo: unexpected status #{res.code}" unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body, symbolize_names: true)
end
# surfaces: chatgpt | perplexity | gemini | google_ai_overview | google_ai_mode | copilot
run = fetch_run("best running shoes for flat feet", "chatgpt")
run[:answers].each do |answer|
puts "#{answer[:surfaceKey]} → #{answer[:answerText]}"
answer[:sources].each { |src| puts " #{src[:position]} #{src[:title]} #{src[:url]}" }
endThe response is a plain JSON object — JSON.parse with symbolize_names: true gives you an idiomatic Hash, no HTML to scrub:
{
"id": "run_84c1f2ab91d3",
"query": "best running shoes",
"surfaces": ["chatgpt"],
"status": "completed",
"recordsDelivered": 1,
"creditsCharged": 1,
"answers": [
{
"surfaceKey": "chatgpt",
"answerText": "For most runners, top picks include ...",
"sources": [
{ "title": "Best Running Shoes", "url": "https://example.com/guide", "position": 1 }
]
}
]
}The response shape is identical across all six engines, so the same Hash walk covers chatgpt, perplexity, gemini, google_ai_overview, google_ai_mode and copilot. Fan one query across all six and diff the citation sets — that diff is the core of any GEO/AEO analysis you'd want to build.
Fanning a query across every engine is one thread per surface — each returns the cited hosts it saw, collected into a Set:
require "set"
SURFACES = %w[chatgpt perplexity gemini google_ai_overview google_ai_mode copilot]
# One thread per engine — reuses fetch_run and returns each surface's cited hosts.
threads = SURFACES.map do |surface|
Thread.new(surface) do |s|
run = fetch_run("best running shoes for flat feet", s)
hosts = run[:answers].flat_map { |a| a[:sources].map { |src| URI(src[:url]).host } }
[s, Set.new(hosts)]
end
end
threads.map(&:value).each do |surface, hosts|
puts "#{surface} cites: #{hosts.to_a.join(', ')}"
endWhat the Ruby API gives you
AgentGEO is a thin answer-access layer, not another closed dashboard. The API delivers the raw material; your Ruby script, Sidekiq worker or Rails service (or your users' AI agent) does the analysis.
- Six engines, one contract — ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Copilot and Gemini behind one request/response shape, so covering another engine is one more string in the
surfacesarray. - Citations and sources, structured — every answer carries
sources[]with title, URL and position, ready to store, rank or diff. No Nokogiri, no regexing links out of markup. - Managed-scraper engine, maintained for you — answers come through supported access paths, so there is no browser session to supervise, no proxy list to rotate and no IP ban to recover from mid-run.
- Provider metadata — every response carries
surfaceKeyandproviderFields, so the dataset you build stays auditable as the engines underneath evolve. - MCP connection, too — the same data is exposed over MCP through the
agentgeo-mcppackage, so Claude Code, Cursor, Codex or any MCP client can pull answers and run the GEO analysis itself, no glue code. - Usage-based billing with a spend cap — start on the free tier with no credit card, then subscription plus overage, priced by usage and never per seat.
Common problems with DIY answer-scraping in Ruby
The DIY route in Ruby is Ferrum, Watir or Capybara pointed at a real site instead of your app: drive ChatGPT or Perplexity headlessly and scrape the DOM. It demos fast and then turns into an operations problem.
- Browser drivers outside the test suite. Capybara and Watir exist to test your app; aiming them at ChatGPT means running headless Chrome in production, where a flaky driver isn't a red spec — it's missing data.
- selenium-webdriver version drift. The browser, the driver and the gem all update on their own schedules; a mismatch that surfaces at 2am is a class of failure a stateless POST simply doesn't have.
- Nokogiri against hydrated SPAs. Perplexity and AI Overviews render answers with client-side JS and hashed class names; your CSS/XPath selectors return
nilafter a silent frontend deploy, and you find out from empty columns. - Sidekiq jobs babysitting browsers. A background job holding a Chrome session ties up a worker for the whole 40–90 second scrape; scale that across six engines and you're sizing your queue around a browser pool.
- Proxy and CAPTCHA churn. Staying unblocked means residential proxies and CAPTCHA solving wired into your jobs — recurring cost and code that has nothing to do with your product.
| DIY scraping in Ruby | AgentGEO API | |
|---|---|---|
| Setup | Ferrum/Selenium, Chrome, proxy rotation | net/http + a key — standard library only |
| Parsing | Nokogiri against hashed class names | Stable JSON, identical across engines |
| Citations | Hand-rolled extraction per engine | Structured answers[].sources[] |
| Background jobs | Workers pinned to browser sessions | A quick HTTP call any worker can make |
| Adding an engine | A new scraper and driver setup | One more key in the surfaces array |
AgentGEO is the answer-access layer; the ranking, alerting or dashboards you build in Ruby on top of it are yours. You own the raw data and ship it inside your own product instead of renting a login to someone else's.
Prefer MCP? Same data, no glue code
Everything the REST endpoint returns is also exposed over MCP. When the consumer is an AI agent rather than a Ruby service — Claude Code, Cursor, Codex or any MCP client — one command wires it up, and the agent pulls answers and runs the GEO analysis itself:
claude mcp add agentgeo -- npx -y agentgeo-mcp --key ag_live_your_key_hereWho builds on this
Mostly Rails teams adding AI-visibility tracking to an existing product — a service object called from an ActiveJob that fans a keyword set across six engines and writes citations to Postgres is an afternoon of Ruby — plus agencies white-labeling GEO/AEO reporting for clients. If you compared closed dashboards like Profound and wanted the raw data underneath, or you were sketching your own scraper, this is the layer to build on.
Ready to pull your first answer? Start free → · Follow the 5-minute quickstart →
Start for freeFrequently asked questions
- Do I need a gem like Ferrum or Selenium to track AI answers in Ruby?
- No. AgentGEO's managed scrapers run server-side, so the whole integration is
Net::HTTPandJSONfrom the standard library — no gems at all. There is no Chrome to drive, no webdriver to keep in sync and no proxy pool to rotate, so the browser-automation failure modes never enter your codebase. - How do I use it from Rails?
- Wrap
fetch_runin a service object underapp/servicesand call it from an ActiveJob (or a scheduled task via thewhenevergem or a Sidekiq cron). It's a plain outbound HTTP request, so it needs no special Rails configuration, and the parsed Hash drops straight into your models or a reporting table. - Is the fan-out thread-safe?
- Yes. Each thread in the example calls
fetch_runindependently and returns its own result — there's no shared mutable state to guard.Net::HTTPcreates a fresh connection per call, so the threads don't contend. For large keyword sets, cap parallelism with a sized thread pool or a queue rather than spawning one thread per query. - What does it cost to run?
- One delivered record costs one credit; failed records cost zero. There's a free tier with no credit card, then usage-based billing — subscription plus overage — with a spend cap, so a runaway job can't produce a surprise invoice. It is never priced per seat.
- Which AI engines does the Ruby API cover?
- Six: ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Copilot and Gemini. The request and response shape is identical across all of them, so adding an engine to your tracker is one more string in the
surfacesarray — no new scraper, no new parser.
Keep reading