AoCAPI 4.0.0

dotnet add package AoCAPI --version 4.0.0
                    
NuGet\Install-Package AoCAPI -Version 4.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AoCAPI" Version="4.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AoCAPI" Version="4.0.0" />
                    
Directory.Packages.props
<PackageReference Include="AoCAPI" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AoCAPI --version 4.0.0
                    
#r "nuget: AoCAPI, 4.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AoCAPI@4.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AoCAPI&version=4.0.0
                    
Install as a Cake Addin
#tool nuget:?package=AoCAPI&version=4.0.0
                    
Install as a Cake Tool

AoC.API

CI NuGet Downloads License

A typed client for Advent of Code: downloads puzzle inputs and samples, reads how many stars an account has earned, and submits answers — identifying itself as the automation guidelines ask. There is also a Rust version.

$ dotnet add package AoCAPI
using AoC.API;

using var session = new Session("53616c7465645f5f...", "github.com/my-username/my-repo by me@example.com");
var puzzle = Puzzle.At(2024, 7);

var input = await session.GetInputTextAsync(puzzle);

Console.WriteLine(await session.SubmitAnswerAsync(puzzle, Part.One, "3749") switch
{
    Verdict.Correct => "gold star",
    var verdict => verdict.ToString(),
});

The cookie is the value of the session cookie on adventofcode.com while logged in (how to find it). It is a credential: treat it like a password. This package keeps it out of ToString output and never logs it.

Targets .NET 8 and .NET 10.

What it does

Call Returns
session.GetInputTextAsync(puzzle) the puzzle's personal input, without its trailing newline
session.GetInputLinesAsync(puzzle) the same, as string[]
session.GetSamplesAsync(puzzle) every sample block on the puzzle page
session.GetSampleTextAsync(puzzle, nth) the nth sample block, counting from one
session.GetSampleLinesAsync(puzzle, nth) the same, as string[]
session.GetStarsAsync() IReadOnlyDictionary<Year, int> — stars earned per event, earliest first
session.GetAcceptedAnswerAsync(puzzle, part) the answer the puzzle page shows as accepted
session.SubmitAnswerAsync(puzzle, part, answer) a Verdict

One session serves a whole event: it holds the cookie and the single HttpClient built from it, and which puzzle a call is about is an argument. Every call takes a CancellationToken. Build the session outside your loop — it is what carries your identification.

With a client of your own

Session also takes an ITransport, which is the seam every request goes out through. HttpClientTransport is the real one, and it can be handed a client you own — from an IHttpClientFactory, say:

using AoC.API.Http;

var options = new ClientOptions(cookie, "github.com/my-username/my-repo by me@example.com")
{
    Timeout = TimeSpan.FromSeconds(10),
};

var transport = new HttpClientTransport(httpClientFactory.CreateClient("adventofcode"), options);
using var session = new Session(transport);

Give it a client of its own: the identification and the session cookie are that client's default headers, so no request can leave without them and no other caller should be sharing it.

Validated coordinates

Puzzle, Year, Day and Part are validated, so an out-of-range coordinate cannot become a request. The pairing is validated too — events up to 2024 run 25 puzzles, and from 2025 on they run 12:

Puzzle.At(2024, 25); // fine
Puzzle.At(2025, 25); // PuzzleException: the 2025 event stops after day 12
Puzzle.At(1066, 1);  // PuzzleException: advent of code started in 2015

Puzzle.TryAt(2025, 25, out var puzzle); // false, for a coordinate you did not write yourself

Verdicts and exceptions

A rejected answer is a Verdict, not an exception — being wrong is a normal outcome. The hierarchy is closed, so a switch over it can be exhaustive:

Verdict Meaning
Correct accepted
Incorrect(Hint?, TimeSpan?) rejected; the hint is TooHigh/TooLow when the site says so, the wait is how long it asks you to wait
AlreadyComplete(bool Matches) the part was already solved, so the site refused to judge; the answer was compared against the accepted one on the puzzle page instead
WrongLevel the site was not asking for an answer to that part — either part one is still unsolved, or the part was never a question, which is day 25's second star

verdict.IsCorrect collapses that to a bool when that is all you need.

Everything that stops a call from producing an answer is an AdventOfCodeException you can catch as one, or branch on: TransportException (the request failed), UnauthorizedException (the cookie is missing, expired or invalid), PuzzleLockedException (the puzzle has not unlocked yet), CooldownException (an answer was submitted too recently, so nothing was judged — it carries the remaining Wait), ParseException (the reply was not one this package recognises) and UnexpectedStatusException (anything else the site returned). Coordinates that name no puzzle are a PuzzleException, which is an ArgumentException: nothing was asked of the site, and nothing will be.

try
{
    var verdict = await session.SubmitAnswerAsync(puzzle, Part.One, answer);
}
catch (CooldownException cooling)
{
    await Task.Delay(cooling.Wait);
}

Testing without a network

ITransport is the seam everything external sits behind, and FakeTransport replays canned replies. This package's own tests run entirely through it — no network, no session cookie — and it ships so a tool built on this package can do the same:

using AoC.API.Http;

var transport = FakeTransport.Serving("1721\n979\n366\n");
using var session = new Session(transport);

var input = await session.GetInputTextAsync(Puzzle.At(2020, 1));

Assert.Equal("1721\n979\n366", input);
Assert.Equal(["https://adventofcode.com/2020/day/1/input"], transport.RequestedUrls);

Automation etiquette

This package follows the Advent of Code automation guidelines. Two of them are settled here; two are deliberately left to you, and this section says which is which so you can be accurate about your own tool.

  • Every request identifies you. You provide your identification when the session is opened, and it becomes one of the HTTP client's default headers, so no call site can omit it and no endpoint can add one of its own. The guidelines ask for github.com/your-repo by you@example.com or similar. Since this is a library, the tool built upon it is the one doing the work, so it is the one that must identify itself.
  • Nothing happens that you did not ask for. A request is made when you call an endpoint and at no other time: nothing polls, retries, prefetches or runs on a schedule. The one call that can make two requests is SubmitAnswerAsync, and only when the site says the part is already solved, in which case it reads the puzzle page to compare your answer against the accepted one.
  • Throttling is yours. This package does not sleep between requests. A library cannot know how a program is being driven, and a hidden delay inside someone else's process is a poor surprise — a caller that already paces itself would end up paying twice. Space your calls out; five seconds between them is a sensible floor.
  • Caching is yours. Puzzle inputs are personal, permanent and unchanging, so download one once and keep it on disk. This package hands you the body and forgets it; it never re-downloads on its own, and it never downloads anything you did not ask for.

Please do not work around the first two.

Design notes

Decisions made during the rewrite, and why:

  • Everything external sits behind ITransport. The endpoints and the parser never learn how a reply was obtained, which is what lets the whole suite run with no network and no cookie — and lets a tool built on this package test itself the same way.
  • No regular expressions. The site's replies are read by a small dependency-free parser in src/AoC.API/Parsing, pinned by saved response bodies for every shape the site sends: correct, wrong, too high, too low, cooldown, already complete and logged out. It also drops emphasis markup and decodes entities, so a sample comes back as the puzzle shows it rather than as page source. The 3.x patterns failed unreadably whenever a wording changed.
  • A cooldown is a TimeSpan. The remaining wait used to be handed back as the site's own prose. It is now parsed, in both shapes the site uses (4m 30s and one minute), so a caller can actually wait for it.
  • One client, built once. 3.x built an HttpClient per request, which is the classic way to exhaust sockets. There is now exactly one, built where the session is opened, and it is the only place the identification and the cookie are set.
  • A wrong answer is not an exception. 3.x threw for replies it did not recognise and returned false for ones it did, which put "you were wrong" and "the site changed" in the same shape. They are now a Verdict and a ParseException respectively.

Migrating from 3.x

Version 4 is a rewrite. Every removed member and its replacement:

3.x 4.x
new Session(cookie, year, day) new Session(cookie, "identification") plus Puzzle.At(year, day)
new Session(cookie, input, pattern) recover the year and day yourself, then Puzzle.At(year, day)
session.GetInputTextAsync() session.GetInputTextAsync(puzzle)
session.GetInputLinesAsync() session.GetInputLinesAsync(puzzle)
session.GetSampleInputTextAsync(nth) session.GetSampleTextAsync(puzzle, nth)
session.GetSampleInputLinesAsync(nth) session.GetSampleLinesAsync(puzzle, nth)
session.GetAllStarsAsync()Dictionary<int, int> session.GetStarsAsync()IReadOnlyDictionary<Year, int>
session.SubmitAnswerAsync(part, answer)SubmissionResult session.SubmitAnswerAsync(puzzle, part, answer)Verdict
SubmissionResult.IsCorrect verdict.IsCorrect
SubmissionResult.CooldownTime (a string) Verdict.Incorrect.Wait, or CooldownException.Wait (a TimeSpan)
SubmissionStatus.OnCooldown CooldownException — nothing was judged, so it is not a verdict
SubmissionResult / SubmissionStatus Verdict says what happened
RegexMatchException, UnknownResponseException ParseException
(reported as one of the above) UnauthorizedException, PuzzleLockedException, UnexpectedStatusException

A whole call site, before and after:

// 3.x
var session = new Session(cookie, 2024, 7);
var input = await session.GetInputTextAsync();
var result = await session.SubmitAnswerAsync(1, "3749");
if (result.Status == SubmissionStatus.OnCooldown) { Console.WriteLine($"wait {result.CooldownTime}"); }
else { Console.WriteLine(result.IsCorrect ? "correct" : "wrong"); }

// 4.x
using var session = new Session(cookie, "github.com/my-username/my-repo by me@example.com");
var puzzle = Puzzle.At(2024, 7);
var input = await session.GetInputTextAsync(puzzle);

try
{
    Console.WriteLine((await session.SubmitAnswerAsync(puzzle, Part.One, "3749")).ToString());
}
catch (CooldownException cooling)
{
    Console.WriteLine($"wait {cooling.Wait}");
}

Note that a rejected answer the site asked you to wait after used to arrive as a cooldown, losing the fact that it had been judged at all. It is now Verdict.Incorrect with a Wait, and CooldownException means only what the site means by it: nothing was judged, so the answer still has to be submitted again.

Development

$ dotnet build                            # net8.0 and net10.0; warnings are errors
$ dotnet test                             # the whole suite; no network
$ dotnet format --verify-no-changes       # style and naming, as CI runs it
$ dotnet pack -c Release                  # the package and its symbols

No test contacts adventofcode.com: endpoints and parsing run through FakeTransport and saved response bodies, and the handful of tests that exercise the real client talk to a loopback listener the test itself stands up. Running the suite on .NET 8 as well as .NET 10 needs both runtimes installed; with only the newer one it rolls forward.

Releases are handled by release-please from conventional commits: merging the generated chore: release vX.Y.Z pull request raises the version, writes the changelog, tags the commit and publishes to NuGet. The version in the csproj and CHANGELOG.md are both written by it, so neither is edited by hand.

License

GPL-3.0

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.0 0 8/20/2026
3.0.0 491 9/26/2025
2.3.0 590 7/31/2024
2.2.11 454 7/29/2024
2.2.10 515 4/26/2024
2.2.9 528 4/21/2024
2.2.8 554 4/7/2024
2.2.7 900 12/9/2023
2.2.6 755 12/4/2023
2.2.5 750 12/4/2023
2.1.5 751 12/3/2023
2.1.4 761 12/3/2023
2.1.3 764 12/2/2023
2.0.3 779 11/10/2023
2.0.2 799 9/15/2023
2.0.1 823 9/6/2023
2.0.0 788 9/6/2023
1.0.5 837 7/30/2023
1.0.4 846 7/29/2023
1.0.3 858 7/29/2023
Loading failed