How to Solve ALTCHA in C# and Post the Token Back Unchanged

solve altcha in c# - How to Solve ALTCHA in C# and Post the Token Back Unchanged

To solve ALTCHA in C# there is nothing to read. ALTCHA is proof of work, not recognition: the site hands out a challenge and the client has to brute-force the number that satisfies it. There is no image, no audio and no guess involved, which makes a solve deterministic and fast. Either it finds the answer, or the challenge was malformed or had already expired. CapSkip added ALTCHA in version 1.2.6, and the .NET SDK exposes it as one method that takes the page URL plus the challenge. The part that actually catches people is what happens after: the token has to go back into the form exactly as the solver returned it.

What you need

  • CapSkip 1.2.6 or later running on a Windows machine. ALTCHA support arrived in that release.
  • The CapSkip .NET package, which targets .NET Standard 2.0, so .NET Framework 4.6.1 and up, .NET Core 2.0 and up, and .NET 6 and later.
  • The URL of the page the widget sits on, and the endpoint that widget fetches its challenge from.
  • An address for the solver. Local mode answers on 127.0.0.1 for that device only; Server mode listens on your network address or public IP so another box can reach it. Step 4 covers which one you want, and both live under connection settings.
# dotnet add package CapSkip
dotnet add package CapSkip

Step 1: find the endpoint the ALTCHA widget calls

Everything else depends on this one value, so get it first. Open DevTools, go to the Network tab and reload the page the widget sits on. The widget makes a request for its challenge, usually to a path with altcha in it. That request URL is what you pass to the solver, and the JSON that endpoint returns is the challenge document itself, which you can pass instead of the URL.

Do not guess the attribute that names it, because it changed between widget generations. Read the page source.

Widget generationAttribute that names the challenge
v1 and v2challengeurl for an endpoint, with a separate challengejson attribute for an inline challenge
v3 and laterchallenge, and that same attribute takes either a URL or the challenge data

The three display styles, native, checkbox and switch, are purely visual. They all submit the same payload and the difference never reaches the solver, so you do not have to work out which one you are looking at. ALTCHA covers the widget attributes in its own integration docs.

Step 2: the solve call, and the two ways to supply a challenge

One method, two arguments: the page URL, then an options dictionary carrying the challenge. Give it the endpoint and CapSkip fetches the challenge for you.

// dotnet add package CapSkip
using CapSkip;

var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);

// CapSkip fetches the challenge, then brute-forces the counter.
var result = await solver.AltchaAsync(
    "https://example.com/signup",
    new Dictionary<string, object?>
    {
        ["challenge_url"] = "https://example.com/altcha/challenge",
    });

Console.WriteLine(result.Token);   // base64 payload for the form field
Console.WriteLine(result.Number);  // the counter that satisfied it

Two fields on the result are ALTCHA only. Token is the base64 payload the form wants, and Number is the counter that solved the challenge. The Code property carries the same string as Token, so either works, but Token is named for the field it goes into and reads better at the call site. The GeeTest fields and the Turnstile user agent stay null here.

Number is worth logging. It is reported for both ALTCHA generations even though their payloads differ: a legacy token carries the counter at the top level, while a proof-of-work v2 token does not, keeping it inside a solution object instead. CapSkip reads it out of the solution object in its own API response, which reports both generations the same way.

Passing the challenge document instead

If your code already fetched the challenge, pass the document and no network request happens at all. This is the faster path when you are already scraping the page, and the one to use when the challenge arrives embedded in the HTML rather than from an endpoint.

// No fetch happens: the document is already here.
var result = await solver.AltchaAsync(
    "https://example.com/signup",
    new Dictionary<string, object?>
    {
        ["challenge_json"] = new Dictionary<string, object>
        {
            ["algorithm"] = "SHA-256",
            ["challenge"] = "YOUR_CHALLENGE_HASH",
            ["salt"] = "YOUR_SALT",
            ["signature"] = "YOUR_SIGNATURE",
            ["maxnumber"] = 1000000,
        },
    });

That option takes a dictionary, which is serialised for you, or a JSON string if you have one already. Sending both the endpoint and the document is allowed, and the inline document wins, because fetching would only re-obtain what you just supplied. The two paths also behave differently under load: an inline challenge that has already expired is refused straight away rather than hashed pointlessly, while an endpoint lets the solver fetch a fresh challenge if the first one died while the job sat in the queue.

Which algorithms the solver covers

The same method handles both generations. The legacy scheme is covered with SHA-1, SHA-256, SHA-384 and SHA-512, and proof-of-work v2 is covered with PBKDF2 and iterative SHA. PBKDF2 is the default that ALTCHA itself recommends, so that covers the large majority of live sites.

Argon2id and scrypt are the exceptions, and they are refused rather than attempted: a task using one comes back in about a third of a second with ERROR_CAPTCHA_UNSOLVABLE and is never retried. That is deliberate. A memory-hard function is not something a retry fixes, so failing immediately beats looking busy. For ALTCHA that result points at the algorithm rather than an unreadable image, and the error code has a guide of its own.

Step 3: post the token back unchanged, before it expires

The widget submits its payload in a form field named altcha, so that is where your token goes. This is the step that quietly breaks.

// Send it exactly as it came back: no trimming,
// no re-encoding, no reordering.
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["email"] = "[email protected]",
    ["altcha"] = result.Token!,
});

var response = await http.PostAsync("https://example.com/signup", body);

The token is base64 of a JSON document whose fields are covered by the server’s own HMAC signature. Any modification invalidates it, so anything that looks like tidying up will break the submit: trimming whitespace, decoding and re-encoding it, or rebuilding the JSON with the keys in a different order. Some integrations read the payload out of a JSON body field rather than a form field, so check what the page’s own submit sends and mirror that.

The other way this step fails is timing. Challenge windows are short and some sites close them inside two minutes, and when one expires the site refuses the answer with a bare verification failure that looks exactly like a wrong answer. There is nothing in the error to tell you which of the two happened. Three habits avoid it: fetch the challenge immediately before solving rather than at the top of a long run, submit the token in the same unit of work that solved it, and never hold a token while a person fills in a form.

The client’s own polling timeouts are not what limits you here, because both are far longer than that two minute window. ALTCHA is CPU work rather than a browser session, so it runs on the default polling timeout and not the longer reCAPTCHA one.

Constructor optionDefaultWhat it covers
defaultTimeout120 secondsALTCHA and image CAPTCHA polling
recaptchaTimeout300 secondsreCAPTCHA, Turnstile and GeeTest polling
pollingInterval5 seconds maximumPolling starts at 0.25 seconds and backs off to this

Step 4: where the solver runs, and which connection mode that needs

The samples above use 127.0.0.1 because that is right when your code and the solver share a machine. As soon as the code that calls the solver runs somewhere else, such as a container, a build agent, a VPS or a managed host, loopback no longer points at the solver, and the first solve throws a NetworkException.

Switch CapSkip to Server mode and it listens on your network address or public IP instead, so any of those can reach it over the API. A static public IP is recommended if you are going over the internet, with a firewall rule that allows only the addresses you expect. Server mode changes where the solver listens and nothing else: it is still your hardware, and it is still unmetered. Read the host from an environment variable so one build works in both places. The client does not read CAPSKIP_HOST by itself, so pass it to the constructor, as the full example below does.

Where the C# runsWhich connection mode
On the CapSkip machine, in an IDE or a console appLocal mode. 127.0.0.1 is genuinely correct
On another box on the same networkServer mode, on that machine’s private address
On a container host, VPS or managed platformServer mode with a static public IP and a firewall rule

One ALTCHA-specific note on proxies. A proxy is supported here, but it is used only for the challenge fetch. There is no browser session to route, so it has no effect on the proof of work itself.

Full working example

// dotnet add package CapSkip
using CapSkip;

var http = new HttpClient();
var solver = new CapSkipClient(
    host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
    port: 8080);

try
{
    var result = await solver.AltchaAsync(
        "https://example.com/signup",
        new Dictionary<string, object?>
        {
            ["challenge_url"] = "https://example.com/altcha/challenge",
        });

    // Submit here, while the challenge is still fresh.
    var body = new FormUrlEncodedContent(new Dictionary<string, string>
    {
        ["email"] = "[email protected]",
        ["altcha"] = result.Token!,
    });
    var response = await http.PostAsync("https://example.com/signup", body);

    Console.WriteLine($"{(int)response.StatusCode} after counter {result.Number}");
}
catch (ApiException ex)
{
    // ERROR_CAPTCHA_UNSOLVABLE here means Argon2id or scrypt.
    Console.WriteLine($"refused: {ex.Message}");
}
catch (CapSkip.TimeoutException)
{
    Console.WriteLine("gave up waiting; defaultTimeout is 120 seconds");
}

The other types are the same shape with a different method. RecaptchaAsync takes a sitekey and a page URL, TurnstileAsync and GeetestAsync work the same way, and image solving is a base64 call. The full method list is on the C# CAPTCHA solver page.

Challenge-page Turnstile is the one type that needs more than a sitekey. Its extra values are covered in the C# challenge-page guide.

Common errors and what they mean

What you seeCauseFix
A bare verification failure from the site, with a token that looks fineThe challenge expired before the form was submittedFetch, solve and submit in one unit of work
ERROR_CAPTCHA_UNSOLVABLE inside an ApiException, in about a third of a secondThe challenge uses Argon2id or scryptNothing to retry. Those two are refused by design
A ValidationException on the callNeither challenge option was supplied, or an option was passed that ALTCHA does not takePass the challenge endpoint or the challenge document, and drop anything else
A NetworkException on the first solveCapSkip is not running, or the host and port are wrongStart CapSkip, then check whether it should be in Local mode or Server mode
The Token property on the result reads nullToken is populated for ALTCHA onlyCall AltchaAsync. On an ALTCHA result the Code property holds the same string
The build fails on an ambiguous TimeoutExceptionCapSkip and System both define that short nameWrite CapSkip.TimeoutException in full, or catch CapSkipError
The form rejects a token your logs show was solvedSomething re-encoded, trimmed or reordered the payloadPass the string straight through, untouched

FAQ

Does solving ALTCHA in C# need a browser?

No, and that is the useful part. ALTCHA hands out a hashing problem rather than something to look at, so the work is CPU only and finishes in milliseconds. You need no WebDriver, no headless Chrome and no user agent. A console app with an HttpClient is enough, which also means it runs happily inside a worker service, a queue consumer or a build step where driving a browser would be awkward.

Can a .NET app on a hosted platform reach the solver?

Yes. Switch CapSkip to Server mode under connection settings so it listens on a network address instead of loopback, then point CAPSKIP_HOST at that address. A container host, a VPS, a CI agent or a managed app service all connect the same way, over the same HTTP API. Use a static public IP if the route goes over the internet, and restrict it with a firewall rule. The solver stays on hardware you own in every one of those cases, so nothing about the licence or the solve count changes.

Should I pass the endpoint or the challenge document?

Pass the endpoint unless you already have the document. It is one entry in the options dictionary, it saves you a request, and if the challenge goes stale while the job is queued the solver fetches a fresh one by itself. Pass the document when your scraper already read it off the page, when the challenge is embedded in the HTML rather than served from an endpoint, or when fetching it needs cookies or headers your code has and the solver does not. In that last case the proxy option is worth knowing about, since for ALTCHA it applies to the fetch and only the fetch.

Why is my counter a different number every time?

Because it is the answer to that particular challenge, not a property of the site. Each challenge carries its own salt, so the number that satisfies it changes on every issue, and it can land anywhere up to the maxnumber the challenge allows. A large counter simply means more hashing was needed, which shows up as a few extra milliseconds and nothing else. It is useful in logs as evidence the work was really done, and useless as something to cache.

The short version

Read the challenge endpoint off the widget, pass it to the one ALTCHA method with the page URL, and post the token back into the field named altcha without touching it. Keep the fetch, the solve and the submit in the same block, because the challenge window can close inside two minutes and an expired challenge looks exactly like a wrong answer. Expect ERROR_CAPTCHA_UNSOLVABLE only from Argon2id and scrypt, which are refused outright rather than attempted. Switch to Server mode the moment the calling code stops sharing a machine with the solver.

One last thing that changes how you design the retry. Because a local captcha solver computes the proof of work on a machine you already own, retrying an expired challenge costs a few milliseconds of your own CPU and nothing else, so you can afford to fetch a fresh challenge rather than nursing a stale one.