Archive

Posts Tagged ‘artificial-intelligence’

RegCheck MCP Server — Connector Documentation

RegCheck is a vehicle registration lookup API covering 50+ countries, operated by Infinite Loop Development Ltd since 2004. This connector exposes RegCheck’s lookup capability as an MCP tool, allowing Claude to look up vehicle details — make, model, colour, fuel type, engine size, and more — directly within a conversation.


Getting Started

1. Create a RegCheck account

If you don’t already have a RegCheck account, sign up at regcheck.org.uk. Your RegCheck username and password are used to authenticate with the MCP server.

2. Connect to Claude

In Claude, go to Settings → Connectors → Add custom connector and enter:

https://regcheckmcp-526628810409.europe-west2.run.app/mcp

Click Connect. Claude will redirect you to the RegCheck MCP sign-in page. Enter your RegCheck username and password and click Sign in. You will be redirected back to Claude automatically.

The connector uses OAuth 2.1 with PKCE. Your password is never stored by the MCP server — it is used only to verify your RegCheck account at sign-in time.

3. Enable the connector in your conversation

Once connected, enable the RegCheck connector via the + button at the start of a conversation. You can then ask Claude to look up any vehicle registration plate.


Example Prompts

  • “What can you tell me about the car with plate AB12 CDE?”
  • “Look up German plate M-AB 1234 for me.”
  • “What fuel type is French registration AA-123-BB?”
  • “Check the MOT status of UK plate LD21 ABC.”
  • “What model is Australian plate ABC123 in Victoria?”

Claude will invoke the lookup tool automatically when a registration plate is mentioned in context.


Supported Countries

The connector supports vehicle registration lookups in 50+ countries, including:

RegionCountries
EuropeUnited Kingdom, Ireland, Germany, France, Spain, Italy, Netherlands, Belgium, Poland, Portugal, Sweden, Norway, Denmark, Finland, Austria, Switzerland, Czech Republic, Romania, Hungary, and more
AmericasUnited States (all states), Canada, Brazil, Argentina, Mexico, Chile, Colombia
Asia-PacificAustralia (all states), New Zealand, Malaysia, Indonesia, Taiwan
Middle East & AfricaSouth Africa, UAE

For the full list of supported countries and country codes, see regcheck.org.uk.

Countries requiring a state or region parameter

Some countries require a state or province code in addition to the plate number. When Claude asks for a state, provide the standard abbreviation — for example:

  • Australia: NSW, VIC, QLD, WA, SA, TAS, ACT, NT
  • USA: CA, TX, NY, FL, etc.
  • Canada: ON, BC, QC, AB, etc.
  • Brazil: SP, RJ, MG, etc.

Available Tools

lookup_vehicle

Looks up vehicle registration details for a given plate and country.

ParameterDescription
registrationThe vehicle registration plate number
countryTwo-letter ISO country code (e.g. GB, DE, FR, AU)
stateState or province code — required for USA, Australia, Canada, Brazil

Returns: Vehicle details including make, model, year, colour, fuel type, engine size, and any country-specific fields such as MOT/NCT status, tax status, or emissions data.

This tool is read-only — it performs a lookup only and does not modify any data.


Authentication

The RegCheck MCP Server uses OAuth 2.1 with PKCE (Proof Key for Code Exchange).

  • No passwords are stored by the MCP server
  • Access tokens are valid for 1 hour and are signed with HMAC-SHA256
  • The server is stateless — no session data is retained between requests
  • All traffic is over HTTPS

OAuth endpoints

EndpointURL
Authorizationhttps://regcheckmcp-526628810409.europe-west2.run.app/authorize
Tokenhttps://regcheckmcp-526628810409.europe-west2.run.app/token
Discoveryhttps://regcheckmcp-526628810409.europe-west2.run.app/.well-known/oauth-authorization-server

Pricing & Usage

Usage is billed against your RegCheck account. Each vehicle lookup consumes one credit. Credits can be purchased at regcheck.org.uk. New accounts receive a small number of free credits to get started.


Support

For questions about the connector or your RegCheck account:

For issues specific to the MCP connector, please include the country code and a sanitised (last 3 characters removed) version of the plate number in your support request.


Privacy Policy

The RegCheck MCP Server is operated by Infinite Loop Development Ltd.

Data collected by this connector:

DataPurposeRetained
RegCheck usernameAuthenticate API requestsNot stored — used at sign-in only
Vehicle registration platePerform the lookupNot stored — passed to RegCheck API and discarded
Country / state codeRoute the lookup to the correct endpointNot stored

What we do not collect:

  • Conversation content
  • Claude prompt text
  • IP addresses beyond standard GCP Cloud Run access logs (retained 30 days)
  • Any data beyond what is necessary to perform the lookup

Third parties: Lookup requests are forwarded to the RegCheck API at regcheck.org.uk, operated by Infinite Loop Development Ltd. No data is shared with any other third party.

Full privacy policy: infiniteloop.ie/privacypolicy.html

Data controller: Infinite Loop Development Ltd, Ballyliffin, Co. Donegal, Ireland. Contact: info@infiniteloop.ie

Building an MCP Server for a Vehicle Registration API in C# .NET 8

If you’ve spent any time with Claude recently, you may have noticed it can do things that feel surprisingly connected to the real world — looking up domain expiry dates, checking live weather, querying your calendar. Most of that happens through MCP: the Model Context Protocol, an open standard that lets AI assistants talk directly to external APIs and services.

I’ve been running RegCheck since 2004 — a vehicle registration lookup API covering 50+ countries, built on a .NET/IIS stack. This is the story of how I built an MCP server for it over the course of a few hours, deployed it to Google Cloud Run, and made it available for anyone to connect to Claude.

The full source is on GitHub: github.com/infiniteloopltd/RegCheckMCP


What is MCP, and why does it matter for API developers?

MCP is essentially a standardised way for AI assistants to discover and call your API tools. Instead of a user having to copy and paste a plate number into a lookup form, then copy the result back into their conversation, Claude can just do it — invisibly, mid-conversation, whenever it recognises that a vehicle lookup is relevant.

The interesting thing for API developers is that MCP is model-agnostic. The same server works with Claude, and potentially other AI clients that implement the standard. You build it once.


The architecture

The RegCheck API is a classic ASMX web service — it returns XML, with the vehicle data serialised as JSON inside a vehicleJson node. Each country has its own web method: Check for UK, CheckIreland for Ireland, CheckGermany for Germany, and so on across 50+ endpoints.

The MCP server is a thin ASP.NET Core 8 layer that:

  1. Accepts MCP tool calls over Streamable HTTP
  2. Reads the caller’s RegCheck username from an X-Api-Key header
  3. Maps the request to the correct ASMX endpoint for the given country
  4. Parses the XML response and returns the vehicleJson content back to Claude

The .NET MCP SDK

Microsoft maintains an official C# SDK for MCP — ModelContextProtocol.AspNetCore on NuGet. It integrates cleanly with ASP.NET Core’s dependency injection and minimal API hosting. Setting up a basic server is straightforward:

csharp

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient("regcheck");
builder.Services.AddMcpServer()
.WithHttpTransport(options => options.Stateless = true)
.WithTools<VehicleLookupTools>();
var app = builder.Build();
app.MapMcp("/mcp");
var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
app.Run($"http://0.0.0.0:{port}");

The Stateless = true option is important for Cloud Run — it removes the requirement for a session ID header, which is the right choice for a public API where each request is independently authenticated.


Defining tools

Tools are just C# methods decorated with attributes. The [Description] attributes are doing real work here — they’re what Claude reads to decide when to invoke the tool. Vague descriptions mean missed invocations; specific ones mean Claude reliably picks up the tool when a user mentions a plate number.

csharp

[McpServerTool(Name = "lookup_vehicle_uk")]
[Description("Look up a UK vehicle registration plate. Returns make, model, colour, fuel type, " +
"MOT expiry, tax status, and engine size. Use when the user provides a UK vehicle " +
"registration number such as 'AB12 CDE' or 'AB12CDE'.")]
public async Task<string> LookupVehicleUk(
[Description("UK vehicle registration number, e.g. 'AB12CDE'. Spaces are ignored.")]
string registration)
{
return await CallEndpoint("Check", registration, null);
}

For the multi-country case, rather than writing 50+ separate methods, a single generic tool with a country code parameter keeps things maintainable. The country-to-endpoint mapping lives in a dictionary:

csharp

private static readonly Dictionary<string, string> CountryEndpoints = new()
{
{ "IE", "CheckIreland" },
{ "DE", "CheckGermany" },
{ "FR", "CheckFrance" },
// ... 50+ entries
};

A handful of countries (Australia, USA, Canada, Pakistan) require a state parameter as well, handled by a HashSet<string> check with a helpful error message if the caller omits it.


Authentication

The RegCheck API authenticates by username, passed as a query parameter. Rather than building OAuth (which the Anthropic Connectors Directory requires for directory listing, but not for custom connectors), I went with a simple X-Api-Key header — users pass their RegCheck username as the key value.

Because Streamable HTTP sends headers with every request, reading it is trivial via IHttpContextAccessor:

csharp

var apiKey = _httpContextAccessor.HttpContext?.Request.Headers["X-Api-Key"].FirstOrDefault();

No session management, no token storage — just read it fresh on every tool call.


Parsing the ASMX response

The RegCheck ASMX endpoints return XML. The vehicle data itself is JSON, serialised inside a vehicleJson node. Extracting it with LINQ to XML is a one-liner:

csharp

var vehicleJson = XDocument.Parse(xml)
.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "vehicleJson")
?.Value;

Using LocalName handles XML namespace variations cleanly. The result is passed straight back to Claude as a JSON string — Claude can reason over the field names without any further transformation.

A typical response looks like this:

json

{
"Description": "1997 Vauxhall Corsa Breeze, 1389CC Petrol, 5DR, Manual",
"RegistrationYear": "1997",
"CarMake": { "CurrentTextValue": "Vauxhall" },
"CarModel": { "CurrentTextValue": "Corsa" },
"FuelType": { "CurrentTextValue": "Petrol" },
"EngineSize": { "CurrentTextValue": "1389CC" }
}

Testing with MCP Inspector

Before deploying anywhere, I tested locally using the official MCP Inspector — a browser-based tool that acts like Postman for MCP servers:

npx @modelcontextprotocol/inspector

Point it at http://localhost:8080/mcp, add the X-Api-Key header in the connection config, connect, and you get a UI listing all registered tools. You can invoke them manually, see the raw JSON input and output, and iterate on descriptions without touching Claude at all. It significantly tightens the development loop.


Deploying to Cloud Run

The server deploys to Google Cloud Run with a single command:

gcloud run deploy regcheckmcp \
--source . \
--region europe-west2 \
--allow-unauthenticated

Cloud Run injects a PORT environment variable (always 8080), which the server reads at startup. Binding to 0.0.0.0 rather than localhost is essential — Cloud Run won’t route external traffic to a loopback-only listener.

The live endpoint is:

https://regcheckmcp-526628810409.europe-west2.run.app/mcp

Connecting to Claude

In Claude (claude.ai), go to Settings → Connectors → Add custom connector, enter the server URL, and add X-Api-Key as a custom header with your RegCheck username as the value. That’s it — Claude will now invoke the vehicle lookup tools automatically whenever you mention a registration plate in conversation.

You can try something like:

“What year is the car with plate AB12CDE?”

Claude will call lookup_vehicle_uk, get the response, and answer directly — no copying, no form filling.


What’s next

The immediate next step is submitting to the Anthropic Connectors Directory, which requires adding OAuth 2.1 with PKCE — so that users can authenticate via a proper consent flow rather than pasting an API key manually. The MCP server logic doesn’t change; it’s purely an auth layer addition.

Longer term, a VIN lookup tool is a natural addition — the RegCheck API supports it via a VinCheck endpoint, and it’s a different enough use case to warrant its own tool with its own description.


Code

Everything is open source and available at github.com/infiniteloopltd/RegCheckMCP. The full implementation including the country endpoint map, state validation, and XML parsing is under 150 lines of C#.

If you have a REST or ASMX API and you’re wondering whether it’s worth building an MCP server — based on this experience, the answer is yes, and it’s less work than you might think.

The Misleading IndexOutOfRangeException That Means “Your List Isn’t Thread-Safe”

If you’ve ever seen a stack trace like this in a .NET application:

System.IndexOutOfRangeException: Index was outside the bounds of the array.
at System.Collections.Generic.List`1.Enumerator.MoveNext()
at System.Linq.Enumerable.WhereListIterator`1.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at YourCode.SomeMethod(...)

…and you stared at the offending line — something innocuous like myList.Where(x => x.IsActive).ToList() — wondering how on earth a LINQ query could be indexing outside an array, you’ve run into one of the most misleadingly-named exceptions in the framework.

The message says “index out of bounds.” The actual problem is a race condition.

What the stack trace is telling you

Read the frames from the bottom up. Your code called ToList(). ToList() calls the List<T> constructor that takes an IEnumerable<T>. That constructor enumerates the Where iterator, which in turn enumerates the underlying List<T> via its Enumerator. And it’s Enumerator.MoveNext() that throws.

So the iterator is walking your list, and at some point it tries to read element N, and N is past the end of the internal array.

How can that happen? The list got smaller — or its internal buffer got swapped — while the enumerator was mid-walk.

List<T> is implemented as a wrapper around a T[] array plus a _size field. The enumerator captures a reference to the list at construction time, then on each MoveNext() it increments an index and reads list._items[index]. If another thread calls Remove, Clear, or triggers a resize via Add between two MoveNext() calls, the array your enumerator is reading from may have been replaced with a smaller one, or the items may have been shuffled. The result: an index that was valid when you started iterating is no longer valid, and you get IndexOutOfRangeException.

Why you don’t get the “nice” exception

List<T> does have a version-check mechanism. Every mutation increments an internal _version field, and the enumerator records the version it started with. If MoveNext() notices the version has changed, it throws InvalidOperationException: Collection was modified; enumeration operation may not execute — the exception most .NET developers have seen at least once and immediately recognise as a concurrency or mid-loop-mutation bug.

The catch: that version check happens after the index increment and array access. If the racing thread mutates the list in a way that shrinks the array between those two operations, you hit the raw IndexOutOfRangeException before the version check ever runs. Same root cause, much worse error message.

This is also why the bug is so frustrating to reproduce. It depends on precise interleaving of two threads down to the instruction level. You can hammer it in a test loop and not see it for a million iterations, then it triggers twice in five minutes in production.

The minimal reproduction

var list = new List<int>();
for (int i = 0; i < 1000; i++) list.Add(i);
// Thread A — repeatedly enumerate
Task.Run(() =>
{
while (true)
{
var copy = list.Where(x => x > 0).ToList();
}
});
// Thread B — repeatedly mutate
Task.Run(() =>
{
var rng = new Random();
while (true)
{
if (list.Count > 0) list.RemoveAt(rng.Next(list.Count));
list.Add(rng.Next());
}
});

Run that for a few seconds and you’ll get either InvalidOperationException or IndexOutOfRangeException — sometimes both, on different runs. The exception you get is essentially a coin flip determined by exactly when the second thread’s mutation lands relative to the first thread’s bounds check.

The diagnostic giveaway

The single most useful signal in the stack trace is this frame:

   at System.Collections.Generic.List`1.Enumerator.MoveNext()

IndexOutOfRangeException originating from List<T>.Enumerator.MoveNext almost always means concurrent modification. The enumerator’s own code is straightforward — there’s no realistic way for it to compute a bad index on its own. Something outside the enumerator changed the list while it was looking away.

If your stack trace shows that frame, stop looking for off-by-one errors in your LINQ predicate. Start looking for which other thread is writing to the same list.

Fixing it

The fix depends on the access pattern. In rough order of preference:

Use the right collection type. System.Collections.Concurrent has thread-safe equivalents tuned for different shapes of access:

  • ConcurrentBag<T> — many writers, occasional bulk drain. No keyed lookup or removal by item.
  • ConcurrentQueue<T> / ConcurrentStack<T> — FIFO / LIFO producer-consumer pipelines.
  • ConcurrentDictionary<TKey, TValue> — by far the most generally useful. Supports thread-safe add, remove, lookup, and snapshot enumeration via .Keys and .Values. If you’re keying items by an ID, this is almost always what you want.

Crucially: .Values on a ConcurrentDictionary returns a snapshot, so iterating it is safe even if other threads are mutating the dictionary at the same time. No exceptions, no locks.

Lock around access. If you’re stuck with List<T> — maybe the API surface is fixed, or the contention is low enough that the overhead doesn’t matter — wrap every read and every write in a lock on a dedicated private object. Every read and every write. Missing one is enough to bring the bug back.

private static readonly object _lock = new object();
private static readonly List<Thing> _items = new List<Thing>();
public static List<Thing> GetActiveThings()
{
lock (_lock)
{
return _items.Where(t => t.IsActive).ToList();
}
}
public static void AddThing(Thing t)
{
lock (_lock) { _items.Add(t); }
}

The ToList() inside the lock is deliberate — it forces the enumeration to complete before the lock is released, so the returned list is a safe, isolated copy the caller can work with at leisure.

Snapshot, then iterate. A halfway measure for read-mostly workloads:

var snapshot = Volatile.Read(ref _items).ToArray();

…paired with copy-on-write semantics for the writers. This is the pattern behind ImmutableList<T> from System.Collections.Immutable, which is worth knowing about for scenarios with many readers and rare writers.

What I’d take away from this

Two things.

First: when you see IndexOutOfRangeException coming out of a LINQ chain on a List<T>, your first hypothesis should be “another thread is writing to this list,” not “my predicate has a bug.” The stack trace looks like a logic error and it almost never is.

Second: List<T> being non-thread-safe is one of those facts every .NET developer knows in the abstract and still trips over in practice, because the framework gives you no help at all until something explodes. There’s no ThrowIfShared mode, no Roslyn analyzer that flags static List<T> fields, no runtime check at write time. The only feedback you get is a confusing exception from deep inside an enumerator, possibly weeks after deployment.

The fix is almost always “use the right type from System.Collections.Concurrent.” It costs you nothing in code clarity and saves you from a class of bug that’s genuinely painful to track down once it’s loose in production.

Thread Pool Exhaustion in ASP.NET: The Async Database Trap

If you’ve ever migrated a working ASP.NET application from synchronous database calls to async, and suddenly found yourself hitting connection pool timeouts under load, you’ve likely fallen into one of the most subtle and destructive traps in the .NET ecosystem: sync-over-async deadlock.


The Symptom

Everything works fine in development. You push to production, traffic picks up, and then:

Timeout expired. The timeout period elapsed prior to obtaining a connection
from the pool. This may have occurred because all pooled connections were in
use and max pool size was reached.

Your database isn’t overloaded. Your queries are fast. But connections are being swallowed and never returned.


What Actually Happens

To understand the deadlock, you first need to understand two things: the ASP.NET synchronization context, and what blocking on an async method actually does.

The Synchronization Context

In classic ASP.NET (WebForms and MVC on the traditional pipeline), each request runs with a synchronization context that ensures continuations — the code that runs after an await — resume on the same thread that started the request. This is a design choice that simplifies state management, but it has a fatal implication when you block.

The Deadlock Sequence

Consider this code:

csharp

// Somewhere in a sync method:
var result = GetDataAsync().Result; // ← the problem

Here’s what happens step by step:

  1. Thread A handles the request and calls GetDataAsync().Result
  2. Thread A is now blocked — it’s sleeping, waiting for the Task to complete
  3. GetDataAsync() runs its SQL query asynchronously and completes
  4. The async machinery looks for a thread to resume on — but the synchronization context says it must resume on Thread A
  5. Thread A is blocked waiting for the task. The task is waiting for Thread A. Neither can proceed.

This is a classic deadlock. The thread never releases, the SQL connection it holds is never returned to the pool, and every subsequent request that hits the same code path adds another frozen thread and another stranded connection.

Why It Only Surfaces Under Load

With light traffic, the thread pool has spare threads. The continuation sneaks onto a different free thread and completes before the pool runs dry. As concurrency increases, all available threads become blocked, no free thread exists to run any continuation, and the whole system seizes.

This is why the bug can pass development and staging entirely undetected.


The Broken Pattern

csharp

public DataTable GetUserData(string userId)
{
// Blocking on an async method — dangerous in ASP.NET
return GetUserDataAsync(userId).Result;
}
public async Task<DataTable> GetUserDataAsync(string userId)
{
using var conn = new SqlConnection(connectionString);
using var cmd = new SqlCommand("sp_GetUser @1", conn);
cmd.Parameters.AddWithValue("@1", userId);
await conn.OpenAsync();
using var reader = await cmd.ExecuteReaderAsync();
var dt = new DataTable();
dt.Load(reader);
return dt;
}

The async method itself is fine. The problem is the caller blocking on it with .Result.


The Fix: Async All the Way Down

The only correct solution is to await the entire call chain without any blocking calls. There must be no .Result, .Wait(), or .GetAwaiter().GetResult() anywhere in the path from the entry point to the database.

csharp

// ✅ Correct: full async chain
public async Task<DataTable> GetUserDataAsync(string userId)
{
using var conn = new SqlConnection(connectionString);
using var cmd = new SqlCommand("sp_GetUser @1", conn);
cmd.Parameters.AddWithValue("@1", userId);
await conn.OpenAsync();
using var reader = await cmd.ExecuteReaderAsync();
var dt = new DataTable();
dt.Load(reader);
return dt;
}

And the caller:

csharp

var data = await GetUserDataAsync(userId); // ✅ not .Result

The WebForms Special Case

WebForms Page_Load is synchronous by signature, which tempts developers to block. The correct bridge is RegisterAsyncTask:

csharp

protected void Page_Load(object sender, EventArgs e)
{
RegisterAsyncTask(new PageAsyncTask(DoWorkAsync));
}
private async Task DoWorkAsync()
{
var data = await GetUserDataAsync(userId);
// ... use data
}

RegisterAsyncTask is ASP.NET’s own sanctioned mechanism for running async work from a sync page lifecycle event. It does not block, does not hold threads, and allows the page pipeline to handle async completion correctly.


Coexisting Sync and Async

A pragmatic migration strategy — rather than converting everything at once — is to maintain both sync and async versions of database methods, and use each only from the appropriate call path:

csharp

// Sync version — for legacy sync call paths
public static DataTable BoundPopulateDataTable(string command, string[] parameters)
{
using var conn = new SqlConnection(ConnectionString);
using var cmd = new SqlCommand(command, conn);
cmd.Parameters.AddRange(ConvertSqlParameters(parameters).ToArray());
conn.Open();
using var reader = cmd.ExecuteReader();
var dt = new DataTable();
dt.Load(reader);
return dt;
}
// Async version — only called from async paths
public static async Task<DataTable> BoundPopulateDataTableAsync(string command, string[] parameters)
{
using var conn = new SqlConnection(ConnectionString);
using var cmd = new SqlCommand(command, conn);
cmd.Parameters.AddRange(ConvertSqlParameters(parameters).ToArray());
await conn.OpenAsync();
using var reader = await cmd.ExecuteReaderAsync();
var dt = new DataTable();
dt.Load(reader);
return dt;
}

The discipline required is simple: never call the async version from a sync context, and never block on it.


Quick Diagnostic Checklist

If you’re seeing connection pool timeouts after an async migration, scan your codebase for these patterns:

PatternRisk
someTask.Result❌ Deadlock
someTask.Wait()❌ Deadlock
someTask.GetAwaiter().GetResult()❌ Deadlock
await someTask✅ Safe
RegisterAsyncTask(...)✅ Safe WebForms bridge

Summary

The async deadlock in ASP.NET is invisible under low load, catastrophic under real traffic, and trivially easy to introduce during a migration. The root cause is always the same: blocking a thread on an async operation inside a synchronization context that needs that thread to resume.

The rule is simple and absolute: if you make a method async, every caller must also be async, all the way to the top of the call stack. There are no shortcuts. .Result is not a shortcut — it’s a time bomb.

Done correctly, async database access is genuinely more scalable. Done incorrectly, it’s worse than sync in every way.

Batch AI Processing: Why Multithreading is the Wrong Instinct

When developers first encounter a large-scale AI classification job — say, two million records that each need to be sent to an LLM for analysis — the instinct is immediately familiar: spin up threads, parallelise the work, saturate the API. It’s the same pattern that works for database processing, file I/O, HTTP scraping. More threads, more throughput.

With LLM APIs, that instinct leads you straight into a wall. And the wall has a name: TPM.


The Problem with Multithreading LLM Calls

Most LLM APIs — OpenAI included — impose a Tokens Per Minute (TPM) limit. This is a rolling window, not a per-request limit. Every token you send in a prompt, and every token the model returns, counts against it.

The naive multithreaded approach burns through this budget in a way that’s both wasteful and hard to control:

The system prompt repeats on every request. If your prompt is 700 tokens and you’re running 20 threads firing one request each, you’re spending 14,000 tokens per second just on prompt overhead — before the model has classified a single record. With a 200,000 TPM limit, you’ve consumed 4.2 minutes of budget in one second.

Burst behaviour triggers rate limits unpredictably. The TPM limit is a rolling window. Twenty threads firing simultaneously create a spike that can exceed the per-minute budget in seconds, even if your average rate would be well within limits. The API returns 429 errors, your retry logic kicks in, those retries themselves consume tokens, and the situation compounds.

Thread count is a blunt instrument. Dialling concurrency up and down doesn’t map cleanly to token consumption because request latency varies. A batch that takes 500ms doesn’t consume the same tokens as one that takes 1,500ms, but both hold a thread slot for their duration.


The Better Model: Semantic Batching

The insight that changes everything is this: the system prompt is a fixed overhead, and you should amortise it across as many classifications as possible per API call.

Instead of:

Thread 1: [system prompt 700 tokens] + [address 1: 15 tokens] → [result: 15 tokens]
Thread 2: [system prompt 700 tokens] + [address 2: 15 tokens] → [result: 15 tokens]
...× 20 threads
Total: 14,000 tokens for 20 classifications

You send:

[system prompt 700 tokens] + [addresses 1-20: 300 tokens] → [results 1-20: 100 tokens]
Total: 1,100 tokens for 20 classifications

That’s a 12× reduction in token consumption for the same work. Suddenly your 200,000 TPM budget — which could only sustain ~270 single-record requests per minute — supports ~3,600 classifications per minute. No extra threads needed.


Key Implementation Details

1. Include an ID in Both Request and Response

The most important correctness detail in batch processing is never rely on positional alignment.

If you send 20 addresses and ask the model to return 20 results, it might return 19. Now you don’t know which one it dropped. If you’re matching by position, records from item 7 onwards get silently misclassified.

The fix is to include a unique identifier in both directions:

User message:
id=548033: product X
id=548034: product Y
...
System prompt format instruction:
Reply ONLY with a JSON array. Format: [{"id":548033,"c":"E"}, ...]

Now you build a dictionary from the response keyed on id, and match each input item explicitly. A missing id means that specific record gets skipped and retried on the next run. Everything else classifies correctly regardless of what the model dropped.

2. Resolve Labels Locally

The model doesn’t need to return the full label text. "Prime City Professionals" costs tokens on every response item. A single letter costs one token.

Keep a static dictionary in your code:

csharp

private static readonly Dictionary<string, string> Labels = new()
{
{ "A", "Prime Product" },
{ "B", "Budget Product" },
// ...
};

The model returns "c":"A", you look up the label locally. This also eliminates a class of hallucination errors where the model invents a label name slightly different from your taxonomy.

Note: even "category" vs "c" matters at scale. In the OpenAI tokenizer, "category" is 3 tokens; "c" is 1. Across 100,000 batch calls, that’s 200,000 tokens — small but free.

3. Track TPM with a Rolling Window, Not Concurrency

Rather than trying to infer safe concurrency from trial and error, measure what you’re actually consuming and throttle directly on that signal.

csharp

// On each successful response, record tokens used with a timestamp
tokenWindow.Enqueue((DateTime.UtcNow, inputTokens + outputTokens));
// Before each request, prune entries older than 60 seconds and sum the rest
var cutoff = DateTime.UtcNow.AddSeconds(-60);
while (window.Peek().t < cutoff) window.Dequeue();
long tpmUsed = window.Sum(x => x.tok);
// Throttle graduated to usage
if (tpmUsed > tpmLimit * 0.98) Thread.Sleep(2000);
else if (tpmUsed > tpmLimit * 0.95) Thread.Sleep(800);
else if (tpmUsed > tpmLimit * 0.85) Thread.Sleep(300);

This gives you automatic, self-correcting throttling that responds to real consumption rather than guessing from thread counts. If a batch of records happens to have longer addresses, the window fills faster and the delay kicks in sooner. No manual tuning required.

4. Resumability via Cursor Pagination

For a job that takes hours or days, stopping and restarting must be safe and cheap. The key is two things working together:

Write results immediately after each batch, not at the end of a page. If you crash mid-page, you’ve lost one batch (20 records), not a thousand.

Use a NULL-check filter combined with cursor pagination. The query for unclassified records looks like:

sql

WHERE segment_category IS NULL AND id > {lastId} ORDER BY id LIMIT 1000

On restart, lastId resets to 0, but the IS NULL filter automatically skips everything already classified. The cursor (id > lastId) keeps the query fast on large tables — OFFSET pagination slows to a crawl at millions of rows because the database still has to scan all preceding rows to find the offset position.

5. Handle Partial Batches Gracefully with Skip vs Error

Not all failures are equal. Distinguish between:

  • Error: something went wrong that warrants logging (HTTP 500, persistent 429 after retries, DB connection failure). These need attention.
  • Skip: the record wasn’t returned in this batch response. Leave it NULL in the database, it will be picked up automatically on the next run. No log noise needed.

This distinction keeps your error output meaningful. If every missing batch item logs as an error, a run with 0.1% skip rate produces thousands of error lines that mask real problems.


The Result

What started as a job estimated at 16–67 days with a naive multithreaded approach settled to around 7 hours using semantic batching — processing two million records through a rate-limited API without a single configuration change to the API account.

The throughput improvement didn’t come from more concurrency. It came from being smarter about what gets sent in each request.

The general principle applies beyond LLM classification: whenever you have a fixed overhead per API call (authentication, context, schema), the correct optimisation is to amortise that overhead across as much work as possible per call, not to fire more calls in parallel.


Summary of Patterns

PatternNaive approachBetter approach
ThroughputMore threadsLarger batches
Rate limitingCatch 429, retryTrack TPM rolling window, throttle proactively
Result matchingPositional array indexID-keyed dictionary
Label resolutionAsk model for full textReturn code, resolve locally
ResumabilityTrack page offsetNULL-check filter + cursor pagination
Failure handlingAll failures are errorsSkip vs Error distinction
DB resilienceCrash on connection dropExponential backoff retry

The instinct to parallelise is correct in principle — you want to keep the API busy. But with token-limited LLM APIs, the right parallelism is within a single request, not across many simultaneous ones.

Enrich Your Qualtrics Surveys with Real-Time Respondent Data Using AvatarAPI

Qualtrics is excellent at capturing what respondents tell you. But what if you could automatically fill in what you already know — or can discover — the moment they enter their email address?

AvatarAPI resolves an email address into rich profile data in real time: a profile photo, full name, city, country, and the social network behind it. By embedding this lookup directly into your Qualtrics survey flow, you collect more information about each respondent without asking a single extra question.


What Data Does AvatarAPI Return?

When you pass an email address to the API, it returns the following fields — all of which can be mapped into Qualtrics Embedded Data and used anywhere in your survey:

FieldDescription
ImageURL to the respondent’s profile photo
NameResolved full name
CityCity of residence
CountryCountry code
ValidWhether the email address is real and reachable
IsDefaultWhether the avatar is a fallback/generic image
Source.NameThe social network the data came from
RawDataThe complete JSON payload

Watch the Video Walkthrough

Before diving into the written steps, watch this complete tutorial — from configuring the Web Service element to rendering the avatar photo on a results page:


Step-by-Step Integration Guide

You can either follow these steps from scratch, or import the ready-made AvatarAPI.qsf template file directly into Qualtrics (see Step 8).


Step 1 — Get Your AvatarAPI Credentials

Sign up at avatarapi.com to obtain a username and password. A free demo account is available for evaluation — use the credentials demo / demo to test before going live.

The API endpoint you will call is:

https://avatarapi.com/v2/api.aspx

Step 2 — Create an Email Capture Question

In your Qualtrics survey, add a Text Entry question with a Single Line selector. This is where respondents will enter their email address.

Note the Question ID assigned to this question (e.g. QID3) — you will reference it when configuring the Web Service. You can find the QID by opening the question’s advanced options.

Tip: Add email format validation via Add Validation → Content Validation → Email to ensure the value passed to the API is always well-formed.


Step 3 — Add a Web Service Element to Your Survey Flow

Navigate to Survey Flow (the flow icon in the left sidebar). Click Add a New Element Here and choose Web Service. Position this element after the block containing your email question and before your results block.

Configure it as follows:

  • URL: https://avatarapi.com/v2/api.aspx
  • Method: POST
  • Content-Type: application/json

Step 4 — Set the Request Body Parameters

Under Set Request Parameters, switch to Specify Body Params and add these three key-value pairs:

{
"username": "your_username",
"password": "your_password",
"email": "${q://QID3/ChoiceTextEntryValue}"
}

The Qualtrics piped text expression ${q://QID3/ChoiceTextEntryValue} dynamically inserts whatever email the respondent typed. Replace QID3 with the actual QID of your email question if it differs.


Step 5 — Map the API Response to Embedded Data Fields

Scroll down to Map Fields from Response. Add one row for each field you want to capture. The From Response column is the JSON key returned by AvatarAPI; the To Field column is the Embedded Data variable name.

From Response (JSON key)To Field (Embedded Data)
ImageImage
NameName
ValidValid
CityCity
CountryCountry
IsDefaultIsDefault
Source.NameSource.Name
RawDataRawData

Note: Qualtrics stores these variables automatically — you don’t need to pre-declare them as Embedded Data elsewhere in the flow, though doing so in the survey flow header keeps things organised.


Step 6 — Display the Avatar Photo on a Results Page

Add a Descriptive Text / Graphic question in a block placed after the Web Service call in your flow.

In the rich-text editor, switch to the HTML source view and paste this snippet:

<img
src="${e://Field/Image}"
alt="Profile Picture"
style="width:100px; height:100px; border-radius:50%;"
/>

The expression ${e://Field/Image} inserts the profile photo URL at runtime. The border-radius: 50% gives it a circular crop for a polished appearance.

You can display other fields using the same pattern:

Name: ${e://Field/Name}
City: ${e://Field/City}
Country: ${e://Field/Country}
Source: ${e://Field/Source.Name}

Step 7 — Test with the Demo Account

Before going live, test the integration using the demo credentials. Enter a well-known email address (such as a Gmail address you know has a Google profile photo) to verify the image and data return correctly.

After a test submission, check the Survey Data tab — all mapped fields (Image, Name, City, Country, etc.) should appear as columns alongside your standard question responses.

Rate limits & production use: The demo credentials are shared and rate-limited. Swap in your own account credentials before publishing a live survey to ensure reliable performance.


Step 8 — Import the Ready-Made QSF Template

Rather than building from scratch, you can import the AvatarAPI.qsf file directly into Qualtrics. This gives you a pre-configured survey with the email question, Web Service flow, and image display block already set up.

To import: go to Create a new project → Survey → Import a QSF file and upload AvatarAPI.qsf. Then update the Web Service credentials to your own username and password, and you’re ready to publish.


How the Survey Flow Works

Once configured, your survey flow has this simple three-part structure:

  1. Block — Respondent enters their email address
  2. Web Service — Silent POST to avatarapi.com/v2/api.aspx; response fields mapped to Embedded Data
  3. Block — Results page displays the avatar photo and enriched profile data

The respondent experiences a seamless survey: they enter their email on page one, the API call fires silently between pages, and they see a personalised result — including their own profile photo — on page two.


Practical Use Cases

Lead enrichment surveys — Capture a prospect’s email and automatically resolve their name, city, and country without asking. Append this data to your CRM export from Qualtrics.

Event registration flows — Display the registrant’s photo back to them as a confirmation step, increasing engagement and reducing drop-off.

Email validation checkpoints — Use the Valid flag in a branch logic condition to route respondents with unresolvable addresses to a correction screen or alternative path.

Research panels — Enrich responses with geographic signals without asking respondents to self-report location, reducing survey length and improving data quality.


Get Started

  • API documentation & sign-up: avatarapi.com
  • API endpoint: https://avatarapi.com/v2/api.aspx
  • Demo credentials: username demo / password demo
  • Video tutorial: Watch on YouTube

Benchmarking reCAPTCHA v3 Solver Services: Speed vs Quality Analysis

When implementing automated systems that need to solve reCAPTCHA v3 challenges, choosing the right solver service can significantly impact both your success rate and operational costs. We conducted a comprehensive benchmark test of five popular reCAPTCHA v3 solving services to compare their performance in terms of both speed and quality scores.

The Results

We tested five major captcha solving services: CapSolver, 2Captcha, AntiCaptcha, NextCaptcha, and DeathByCaptcha. Each service was evaluated both with and without residential proxy support (using Decodo residential proxies).

Speed Performance Rankings

Fastest to Slowest (without proxy):

  1. CapSolver – 3,383ms (3.4 seconds)
  2. NextCaptcha – 6,725ms (6.7 seconds)
  3. DeathByCaptcha – 16,212ms (16.2 seconds)
  4. AntiCaptcha – 17,069ms (17.1 seconds)
  5. 2Captcha – 36,149ms (36.1 seconds)

With residential proxy:

  1. CapSolver – 5,101ms (5.1 seconds)
  2. NextCaptcha – 10,875ms (10.9 seconds)
  3. DeathByCaptcha – 10,861ms (10.9 seconds)
  4. 2Captcha – 25,749ms (25.7 seconds)
  5. AntiCaptcha – Failed (task type not supported with proxy)

Quality Score Results

Here’s where the results become particularly interesting: all services that successfully completed the challenge returned identical scores of 0.10. This uniformly low score across all providers suggests we’re observing a fundamental characteristic of how these services interact with Google’s reCAPTCHA v3 system rather than differences in solver quality.

What Do These Results Tell Us?

1. The Score Mystery

A reCAPTCHA v3 score of 0.10 is at the very bottom of Google’s scoring range (0.0-1.0), indicating that Google’s system detected these tokens as very likely originating from bots. This consistent result across all five services reveals several important insights:

Why such low scores?

  • reCAPTCHA v3 uses machine learning trained on actual site traffic patterns
  • Without established traffic history, the system defaults to suspicious scores
  • Commercial solver services are inherently detectable by Google’s sophisticated fingerprinting
  • The test environment may lack the organic traffic patterns needed for v3 to generate higher scores

As mentioned in our research, CleanTalk found that reCAPTCHA v3 often returns consistent scores in test environments without production traffic. The system needs time to “learn” what normal traffic looks like for a given site before it can effectively differentiate between humans and bots.

2. Speed is the Real Differentiator

Since all services returned the same quality score, speed becomes the primary differentiator:

CapSolver emerged as the clear winner, solving challenges in just 3.4 seconds without proxy and 5.1 seconds with proxy. This represents a 10x speed advantage over the slowest service (2Captcha at 36 seconds).

NextCaptcha came in second place with respectable times of 6.7 seconds (no proxy) and 10.9 seconds (with proxy), making it a solid middle-ground option.

DeathByCaptcha and AntiCaptcha performed similarly at around 16-17 seconds without proxy, though AntiCaptcha failed to support proxy-based solving for this captcha type.

2Captcha was significantly slower at 36 seconds without proxy, though it did improve to 25.7 seconds with proxy enabled.

3. Proxy Support Variations

Proxy support proved inconsistent across services:

  • Most services handled proxies well, with CapSolver, NextCaptcha, DeathByCaptcha, and 2Captcha all successfully completing challenges through residential proxies
  • AntiCaptcha failed with proxy, returning an “ERROR_TASK_NOT_SUPPORTED” error, suggesting their proxy-based reCAPTCHA v3 implementation may have limitations
  • Proxy impact on speed varied: Some services (2Captcha) were faster with proxy, while others (CapSolver, NextCaptcha) were slower

4. Success Rates

All services except AntiCaptcha (with proxy) achieved 100% success rates, meaning they reliably returned valid tokens. However, the validity of a token doesn’t correlate with its quality score—all tokens were valid but all received low scores from Google.

Practical Implications

For High-Volume Operations

If you’re processing thousands of captchas daily, CapSolver’s 3-5 second solve time provides a massive throughput advantage. At scale, this speed difference translates to:

  • Processing 1,000 captchas with CapSolver: ~56 minutes
  • Processing 1,000 captchas with 2Captcha: ~10 hours

For Quality-Sensitive Applications

The uniform 0.10 scores reveal a hard truth: commercial reCAPTCHA v3 solvers may not produce high-quality tokens that pass strict score thresholds. If your target site requires scores above 0.5 or 0.7, these services may not be suitable regardless of which one you choose.

Cost Considerations

Since all services returned the same quality, cost-per-solve becomes the tiebreaker alongside speed:

  • CapSolver: ~$1.00 per 1,000 solves
  • 2Captcha: ~$2.99 per 1,000 solves
  • AntiCaptcha: ~$2.00 per 1,000 solves

CapSolver offers the best speed-to-cost ratio in this comparison.

The Bigger Picture: reCAPTCHA v3 Limitations

These results illuminate a broader challenge with reCAPTCHA v3 solver services. Google’s v3 system is fundamentally different from v2:

  • v2 presented challenges that could be solved by humans or AI
  • v3 analyzes behavior patterns, browser fingerprints, and site-specific traffic history

Commercial solvers can generate valid tokens, but those tokens carry telltale signatures that Google’s machine learning readily identifies. The consistently low scores suggest that Google has effective detection mechanisms for solver-generated traffic.

When Might Scores Improve?

Based on research and documentation:

  1. Production environments with real organic traffic may see better scores
  2. Time – letting reCAPTCHA v3 “train” on a site for days or weeks
  3. Mixed traffic – solver tokens mixed with legitimate user traffic
  4. Residential proxies – though our test showed this alone doesn’t improve scores

Conclusions and Recommendations

If Speed Matters Most

Choose CapSolver. Its 3-5 second solve times are unmatched, and at $1 per 1,000 solves, it’s also the most cost-effective option.

If You Need Proxy Support

Avoid AntiCaptcha for proxy-based v3 solving. CapSolver, NextCaptcha, and DeathByCaptcha all handled residential proxies successfully.

If Quality Scores Matter

Reconsider using solver services entirely. The uniform 0.10 scores suggest that commercial solvers may not be suitable for sites with strict score requirements. Consider alternative approaches:

  • Browser automation with real user simulation
  • Residential proxy networks with actual human solvers
  • Challenging whether reCAPTCHA v3 is the right solution for your use case

The Bottom Line

For raw performance in a test environment, CapSolver dominated with the fastest solve times and lowest cost. However, the universal 0.10 quality scores across all services reveal that speed and cost may be moot points if your application requires high-quality scores that pass Google’s bot detection.

The real takeaway? reCAPTCHA v3 is doing its job—it successfully identifies solver-generated tokens regardless of which service you use. If you need high scores, you’ll need more sophisticated approaches than simply purchasing tokens from commercial solving services.


This benchmark was conducted in January 2026 using production API credentials for all services. Tests were performed with both direct connections and residential proxy infrastructure. Individual results may vary based on site configuration, traffic patterns, and Google’s evolving detection systems.

Google Calendar Privacy Proxy

https://github.com/infiniteloopltd/Google-Calendar-Redactor-Proxy/

A lightweight Google Cloud Run service that creates privacy-protected calendar feeds from Google Calendar. Share your availability with colleagues without exposing personal appointment details.

The Problem

You want to share your calendar availability with work colleagues, but:

  • You have multiple calendars (work, personal, family) that you need to consolidate
  • Google Calendar’s subscribed calendars (ICS feeds) don’t count toward your Outlook free/busy status
  • You don’t want to expose personal appointment details to work contacts
  • Outlook’s native calendar sharing only works with Exchange/Microsoft 365 calendars, not external ICS subscriptions

This service solves that problem by creating a privacy-filtered calendar feed that Outlook can subscribe to, showing you as “Busy” during your appointments without revealing what those appointments are.

How It Works

Google Calendar → This Service → Privacy-Protected ICS Feed → Outlook
   (full details)    (redaction)     (busy blocks only)      (subscription)

The service:

  1. Fetches your Google Calendar ICS feed using the private URL
  2. Strips out all identifying information (titles, descriptions, locations, attendees)
  3. Replaces event summaries with “Busy”
  4. Preserves all timing information (when you’re busy/free)
  5. Returns a sanitized ICS feed that Outlook can subscribe to

Use Cases

  • Multiple calendar consolidation: Combine work, personal, and family calendars into one availability view
  • Privacy-protected sharing: Share when you’re busy without sharing what you’re doing
  • Cross-platform calendaring: Bridge Google Calendar into Outlook environments
  • Professional boundaries: Keep personal life private while showing accurate availability

Quick Start

1. Get Your Google Calendar Private URL

  1. Open Google Calendar
  2. Click the ⚙️ Settings icon → Settings
  3. Select your calendar from the left sidebar
  4. Scroll to “Integrate calendar”
  5. Copy the “Secret address in iCal format” URL

Your URL will look like:

https://calendar.google.com/calendar/ical/info%40infiniteloop.ie/private-xxxxxxx/basic.ics

2. Deploy the Service

# Edit deploy.bat and set your PROJECT_ID
deploy.bat

# Or deploy manually
gcloud run deploy calendar-proxy --source . --platform managed --region europe-west1 --allow-unauthenticated

You’ll get a service URL like: https://calendar-proxy-xxxxxxxxxx-ew.a.run.app

3. Construct Your Privacy-Protected Feed URL

From your Google Calendar URL:

https://calendar.google.com/calendar/ical/info%xxxxx.xxx/private-xxxxxxx/basic.ics

Extract:

  • calendarIdinfo@infiniteloop.ie (URL decoded)
  • privateKeyxxxxxxxxxx (just the key, without “private-” prefix)

Build your proxy URL:

https://calendar-proxy-xxxxxxxxxx-ew.a.run.app/calendar?calendarId=info@infiniteloop.ie&privateKey=xxxxxxx

4. Subscribe in Outlook

Outlook Desktop / Web

  1. Open Outlook
  2. Go to Calendar
  3. Click Add Calendar → Subscribe from web
  4. Paste your proxy URL
  5. Give it a name (e.g., “My Availability”)
  6. Click Import

Outlook will now show:

  • ✅ Blocked time during your appointments
  • ✅ “Busy” status for those times
  • ❌ No details about what the appointments are

What Gets Redacted

The service removes all identifying information:

Original ICS PropertyResult
SUMMARY: (event title)→ "Busy"
DESCRIPTION: (event details)→ Removed
LOCATION: (where)→ Removed
ORGANIZER: (who created it)→ Removed
ATTENDEE: (participants)→ Removed
URL: (meeting links)→ Removed
ATTACH: (attachments)→ Removed
CLASS: (privacy)→ Set to PRIVATE

What Gets Preserved

All timing and scheduling information remains intact:

  • ✅ Event start times (DTSTART)
  • ✅ Event end times (DTEND)
  • ✅ Event duration
  • ✅ Recurring events (RRULE)
  • ✅ Exception dates (EXDATE)
  • ✅ Event status (confirmed, tentative, cancelled)
  • ✅ Time zones
  • ✅ All-day events
  • ✅ Unique identifiers (UID)

Technical Details

Stack: .NET 8 / ASP.NET Core Minimal API
Hosting: Google Cloud Run (serverless)
Cost: Virtually free for personal use (Cloud Run free tier: 2M requests/month)
Latency: ~200-500ms per request (fetches from Google, processes, returns)

API Endpoint

GET /calendar?calendarId={id}&privateKey={key}

Parameters:

  • calendarId (required): Your Google Calendar ID (usually your email)
  • privateKey (required): The private key from your Google Calendar ICS URL

Response:

  • Content-Type: text/calendar; charset=utf-8
  • Body: Privacy-redacted ICS feed

Local Development

# Run locally
dotnet run

# Test
curl "http://localhost:8080/calendar?calendarId=test@example.com&privateKey=abc123"

Deployment

Prerequisites

Deploy

# Option 1: Use the batch file
deploy.bat

# Option 2: Manual deployment
gcloud run deploy calendar-proxy ^
  --source . ^
  --platform managed ^
  --region europe-west1 ^
  --allow-unauthenticated ^
  --memory 512Mi

The --allow-unauthenticated flag is required so that Outlook can fetch your calendar without authentication. Your calendar data is still protected by the private key in the URL.

Security & Privacy

Is This Secure?

Yes, with caveats:

✅ Your calendar data is already protected by Google’s private key mechanism
✅ No data is stored – the service is stateless and doesn’t log calendar contents
✅ HTTPS encrypted – All traffic is encrypted in transit
✅ Minimal attack surface – Simple pass-through service with redaction

⚠️ Considerations:

  • Your private key is in the URL you share (same as Google’s original ICS URL)
  • Anyone with your proxy URL can see your busy/free times (but not details)
  • The service runs as --allow-unauthenticated so Outlook can fetch it
  • If you need stricter access control, consider adding authentication

Privacy Features

  • Strips all personally identifying information
  • Marks all events as CLASS:PRIVATE
  • No logging of calendar contents
  • No data persistence
  • Stateless operation

Recommendations

  • Don’t share your proxy URL publicly
  • Treat it like a password – it grants access to your availability
  • Regenerate your Google Calendar private key if compromised
  • Monitor your Cloud Run logs for unexpected access patterns

Cost Estimation

Google Cloud Run pricing (as of 2025):

  • Free tier: 2M requests/month, 360,000 GB-seconds/month
  • Typical calendar: Refreshes every 30-60 minutes
  • Monthly cost: $0 for personal use (well within free tier)

Even with 10 people subscribing to your calendar refreshing every 30 minutes:

  • ~14,400 requests/month
  • ~$0.00 cost

Troubleshooting

“404 Not Found” when subscribing in Outlook

  • Verify your service is deployed: gcloud run services list
  • Check your URL is correctly formatted
  • Ensure --allow-unauthenticated is set

“Invalid calendar” error

  • Verify your Google Calendar private key is correct
  • Test the URL directly in a browser first
  • Check that your calendarId doesn’t have URL encoding issues

Events not showing up

  • Google Calendar ICS feeds can take 12-24 hours to reflect changes
  • Try re-subscribing to the calendar in Outlook
  • Verify the original Google Calendar ICS URL works

Deployment fails

# Ensure you're authenticated
gcloud auth login

# Set your project
gcloud config set project YOUR_PROJECT_ID

# Enable required APIs
gcloud services enable run.googleapis.com
gcloud services enable cloudbuild.googleapis.com

Limitations

  • Refresh rate: Calendar clients typically refresh ICS feeds every 30-60 minutes (not real-time)
  • Google’s ICS feed: Updates can take up to 24 hours to reflect in the ICS feed
  • Authentication: No built-in authentication (relies on URL secrecy)
  • Multi-calendar: Requires one proxy URL per Google Calendar

Alternatives Considered

SolutionProsCons
Native Outlook calendar sharingBuilt-in, real-timeOnly works with Exchange calendars
Calendly/BookingsProfessional, feature-richMonthly cost, overkill for simple availability
Manual sync (Zapier/Power Automate)WorksComplex setup, ongoing maintenance
This solutionSimple, free, privacy-focusedRelies on ICS feed delays

Contributing

Contributions welcome! Areas for enhancement:

  •  Add basic authentication support
  •  Support multiple calendars in one feed
  •  Caching layer to reduce Google Calendar API calls
  •  Health check endpoint
  •  Metrics/monitoring
  •  Custom “Busy” text per calendar

License

MIT License – free to use, modify, and distribute.

Author

Created by Infinite Loop Development Ltd to solve a real business need for calendar privacy across platforms. https://github.com/infiniteloopltd/Google-Calendar-Redactor-Proxy/

How to Check All AWS Regions for Deprecated Python 3.9 Lambda Functions (PowerShell Guide)

If you’ve received an email from AWS notifying you that Python 3.9 is being deprecated for AWS Lambda, you’re not alone. As runtimes reach End-Of-Life, AWS sends warnings so you can update your Lambda functions before support officially ends.

The key question is:

How do you quickly check every AWS region to see where you’re still using Python 3.9?

AWS only gives you a single-region example in their email, but many teams have functions deployed globally. Fortunately, you can automate a full multi-region check using a simple PowerShell script.

This post shows you exactly how to do that.


🚨 Why You Received the Email

AWS is ending support for Python 3.9 in AWS Lambda.
After the deprecation dates:

  • No more security patches
  • No AWS technical support
  • You won’t be able to create/update functions using Python 3.9
  • Your functions will still run, but on an unsupported runtime

To avoid risk, you should upgrade these functions to Python 3.10, 3.11, or 3.12.

But first, you need to find all the functions using Python 3.9 — across all regions.


✔️ Prerequisites

Make sure you have:

  • AWS CLI installed
  • AWS credentials configured (via aws configure)
  • Permissions to run:
    • lambda:ListFunctions
    • ec2:DescribeRegions

🧪 Step 1 — Verify AWS CLI Access

Run this to confirm your CLI is working:

aws sts get-caller-identity --region eu-west-1

If it returns your AWS ARN, you’re good to go.

If you see “You must specify a region”, set a default region:

aws configure set region eu-west-1


📝 Step 2 — PowerShell Script to Check Python 3.9 in All Regions

Save this as aws-lambda-python39-check.ps1 (or any name you prefer):

# Get all AWS regions (forcing region so the call always works)
$regions = (aws ec2 describe-regions --region us-east-1 --query "Regions[].RegionName" --output text) -split "\s+"

foreach ($region in $regions) {
    Write-Host "Checking region: $region ..."
    $functions = aws lambda list-functions `
        --region $region `
        --query "Functions[?Runtime=='python3.9'].FunctionArn" `
        --output text

    if ($functions) {
        Write-Host "  → Found Python 3.9 functions:"
        Write-Host "    $functions"
    } else {
        Write-Host "  → No Python 3.9 functions found."
    }
}

This script does three things:

  1. Retrieves all AWS regions
  2. Loops through each region
  3. Prints any Lambda functions that still use Python 3.9

It handles the common AWS CLI error:

You must specify a region

by explicitly using --region us-east-1 when retrieving the region list.


▶️ Step 3 — Run the Script

Open PowerShell in the folder where your script is saved:

.\aws-lambda-python39-check.ps1

You’ll see output like:

Checking region: eu-west-1 ...
  → Found Python 3.9 functions:
    arn:aws:lambda:eu-west-1:123456789012:function:my-old-function

Checking region: us-east-1 ...
  → No Python 3.9 functions found.

If no functions appear, you’re fully compliant.


🛠️ What to Do Next

For each function identified, update the runtime:

aws lambda update-function-configuration `
    --function-name MyFunction `
    --runtime python3.12

If you package dependencies manually (ZIP deployments), ensure you rebuild them using the new Python version.


🎉 Summary

AWS’s deprecation emails can be slightly alarming, but the fix is simple:

  • Scan all regions
  • Identify Python 3.9 Lambda functions
  • Upgrade them in advance of the cutoff date

With the PowerShell script above, you can audit your entire AWS account in seconds.

How to Integrate the RegCheck Vehicle Lookup #API with #OpenAI Actions

In today’s AI-driven world, connecting specialized APIs to large language models opens up powerful possibilities. One particularly useful integration is connecting vehicle registration lookup services to OpenAI’s custom GPTs through Actions. In this tutorial, we’ll walk through how to integrate the RegCheck API with OpenAI Actions, enabling your custom GPT to look up vehicle information from over 30 countries.

What is RegCheck?

RegCheck is a comprehensive vehicle data API that provides detailed information about vehicles based on their registration numbers (license plates). With support for countries including the UK, USA, Australia, and most of Europe, it’s an invaluable tool for automotive businesses, insurance companies, and vehicle marketplace platforms.

Why Integrate with OpenAI Actions?

OpenAI Actions allow custom GPTs to interact with external APIs, extending their capabilities beyond text generation. By integrating RegCheck, you can create a GPT assistant that:

  • Instantly looks up vehicle specifications for customers
  • Provides insurance quotes based on real vehicle data
  • Assists with vehicle valuations and sales listings
  • Answers detailed questions about specific vehicles

Prerequisites

Before you begin, you’ll need:

  • An OpenAI Plus subscription (for creating custom GPTs)
  • A RegCheck API account with credentials
  • Basic familiarity with OpenAPI specifications

Step-by-Step Integration Guide

Step 1: Create Your Custom GPT

Navigate to OpenAI’s platform and create a new custom GPT. Give it a name like “Vehicle Lookup Assistant” and configure its instructions to handle vehicle-related queries.

Step 2: Add the OpenAPI Schema

In your GPT configuration, navigate to the “Actions” section and add the following OpenAPI specification:

yaml

openapi: 3.0.0
info:
  title: RegCheck Vehicle Lookup API
  version: 1.0.0
  description: API for looking up vehicle registration information across multiple countries
servers:
  - url: https://www.regcheck.org.uk/api/json.aspx

paths:
  /Check/{registration}:
    get:
      operationId: checkUKVehicle
      summary: Get details for a vehicle in the UK
      parameters:
        - name: registration
          in: path
          required: true
          schema:
            type: string
          description: UK vehicle registration number
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

  /CheckSpain/{registration}:
    get:
      operationId: checkSpainVehicle
      summary: Get details for a vehicle in Spain
      parameters:
        - name: registration
          in: path
          required: true
          schema:
            type: string
          description: Spanish vehicle registration number
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

  /CheckFrance/{registration}:
    get:
      operationId: checkFranceVehicle
      summary: Get details for a vehicle in France
      parameters:
        - name: registration
          in: path
          required: true
          schema:
            type: string
          description: French vehicle registration number
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

  /VinCheck/{vin}:
    get:
      operationId: checkVehicleByVin
      summary: Get details for a vehicle by VIN number
      parameters:
        - name: vin
          in: path
          required: true
          schema:
            type: string
          description: Vehicle Identification Number
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object

Note: You can expand this schema to include additional endpoints for other countries as needed. The RegCheck API supports over 30 countries.

Step 3: Configure Authentication

  1. In the Authentication section, select Basic authentication
  2. Enter your RegCheck API username
  3. Enter your RegCheck API password
  4. OpenAI will securely encrypt and store these credentials

The authentication header will be automatically included in all API requests made by your GPT.

Step 4: Test Your Integration

Use the built-in test feature in the Actions panel to verify the connection:

  1. Select the checkUKVehicle operation
  2. Enter a test registration like YYO7XHH
  3. Click “Test” to see the response

You should receive a JSON response with vehicle details including make, model, year, engine size, and more.

Step 5: Configure GPT Instructions

Update your GPT’s instructions to effectively use the new Actions:

You are a vehicle information assistant. When users provide a vehicle 
registration number, use the appropriate CheckVehicle action based on 
the country. Present the information in a clear, user-friendly format.

Always ask which country the registration is from if not specified.
Provide helpful context about the vehicle data returned.

Example Use Cases

Once integrated, your GPT can handle queries like:

User: “What can you tell me about UK registration YYO7XHH?”

GPT: [Calls checkUKVehicle action] “This is a 2007 Peugeot 307 X-line with a 1.4L petrol engine. It’s a 5-door manual transmission vehicle with right-hand drive…”

User: “Look up Spanish plate 0075LTJ”

GPT: [Calls checkSpainVehicle action] “Here’s the information for that Spanish vehicle…”

Best Practices and Considerations

API Limitations

  • The RegCheck API is currently in BETA and may change without notice
  • Consider implementing error handling in your GPT instructions
  • Be aware of rate limits on your API account

Privacy and Security

  • Never expose API credentials in your GPT’s instructions or responses
  • Inform users that vehicle lookups are being performed
  • Comply with data protection regulations in your jurisdiction

Optimizing Performance

  • Cache frequently requested vehicle information where appropriate
  • Use the most specific endpoint (e.g., CheckSpain vs. generic Check)
  • Consider implementing fallback behavior for failed API calls

Expanding the Integration

The RegCheck API offers many more endpoints you can integrate:

  • UKMOT: Access MOT test history for UK vehicles
  • WheelSize: Get wheel and tire specifications
  • CarSpecifications: Retrieve detailed specs by make/model/year
  • Country-specific checks: Add support for Australia, USA, and 25+ other countries

Simply add these endpoints to your OpenAPI schema following the same pattern.

Troubleshooting Common Issues

Authentication Errors: Double-check your username and password are correct in the Authentication settings.

404 Not Found: Verify the registration format matches the country’s standard format.

Empty Responses: Some vehicles may not have complete data in the RegCheck database.

Conclusion

Integrating the RegCheck API with OpenAI Actions transforms a standard GPT into a powerful vehicle information assistant. Whether you’re building tools for automotive dealerships, insurance platforms, or customer service applications, this integration provides instant access to comprehensive vehicle data from around the world.

The combination of AI’s natural language understanding with RegCheck’s extensive vehicle database creates a seamless user experience that would have required significant custom development just a few years ago.

Ready to get started? Create your RegCheck account, set up your custom GPT, and start building your vehicle lookup assistant today!