diff --git a/Svrnty.CQRS.Altcha.Abstractions/AltchaAttribute.cs b/Svrnty.CQRS.Altcha.Abstractions/AltchaAttribute.cs
new file mode 100644
index 0000000..7867335
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/AltchaAttribute.cs
@@ -0,0 +1,27 @@
+namespace Svrnty.CQRS.Altcha.Abstractions;
+
+///
+/// Marks a command or query as requiring proof-of-work (or equivalent
+/// anti-abuse evidence) before the framework will dispatch it to the handler.
+///
+///
+/// The accompanying request type should implement
+/// to carry the widget's solution payload. The framework's Altcha
+/// authorization check (registered via AddSvrntyAltcha()) reads the
+/// solution off the request and calls the configured .
+///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+public sealed class AltchaAttribute : Attribute
+{
+ ///
+ /// When true (default), a valid mobile-attestation token on the
+ /// request satisfies the requirement without needing a proof-of-work
+ /// solution. The Altcha check reads
+ /// ["mobile_attested"]
+ /// — when stamped true by an earlier check (e.g. an Apple
+ /// App Attest / Play Integrity verifier), the PoW check is skipped.
+ /// Set to false on commands where PoW must always run regardless
+ /// of caller.
+ ///
+ public bool AllowMobileAttestationBypass { get; set; } = true;
+}
diff --git a/Svrnty.CQRS.Altcha.Abstractions/AltchaChallenge.cs b/Svrnty.CQRS.Altcha.Abstractions/AltchaChallenge.cs
new file mode 100644
index 0000000..cc3628b
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/AltchaChallenge.cs
@@ -0,0 +1,28 @@
+namespace Svrnty.CQRS.Altcha.Abstractions;
+
+///
+/// Server-issued Altcha challenge. Shape matches what the
+/// altcha widget v3
+/// expects from its challengeurl.
+///
+public sealed class AltchaChallenge
+{
+ /// Hashing algorithm name (e.g. SHA-256).
+ public required string Algorithm { get; init; }
+
+ /// Hex-encoded hash the client must find a preimage for.
+ public required string Challenge { get; init; }
+
+ /// Hex-encoded salt (embeds an expiry timestamp).
+ public required string Salt { get; init; }
+
+ ///
+ /// HMAC-SHA256 of algorithm|challenge|salt|maxnumber using the
+ /// server's signing secret. Lets the verify step trust the issued
+ /// challenge without keeping per-challenge state.
+ ///
+ public required string Signature { get; init; }
+
+ /// Upper bound of the PoW search space (equals the requested complexity).
+ public required uint MaxNumber { get; init; }
+}
diff --git a/Svrnty.CQRS.Altcha.Abstractions/AltchaVerifyResult.cs b/Svrnty.CQRS.Altcha.Abstractions/AltchaVerifyResult.cs
new file mode 100644
index 0000000..70cad68
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/AltchaVerifyResult.cs
@@ -0,0 +1,20 @@
+namespace Svrnty.CQRS.Altcha.Abstractions;
+
+///
+/// Outcome of an Altcha solution verification.
+///
+public sealed class AltchaVerifyResult
+{
+ public required bool Ok { get; init; }
+
+ ///
+ /// Diagnostic only — not surfaced to end users. Suggested values:
+ /// signature-invalid, expired, pow-incorrect,
+ /// replayed, redis-unreachable, malformed.
+ ///
+ public string? Reason { get; init; }
+
+ public static AltchaVerifyResult Success { get; } = new() { Ok = true };
+
+ public static AltchaVerifyResult Fail(string reason) => new() { Ok = false, Reason = reason };
+}
diff --git a/Svrnty.CQRS.Altcha.Abstractions/IAltchaChallengeProvider.cs b/Svrnty.CQRS.Altcha.Abstractions/IAltchaChallengeProvider.cs
new file mode 100644
index 0000000..bcf4e88
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/IAltchaChallengeProvider.cs
@@ -0,0 +1,13 @@
+namespace Svrnty.CQRS.Altcha.Abstractions;
+
+///
+/// Mints an Altcha challenge for the widget to solve. The default
+/// implementation in Svrnty.CQRS.Altcha.Grpc talks to a self-hosted
+/// altcha service; the Svrnty.CQRS.Altcha.MinimalApi package exposes
+/// GET /api/altcha/challenge that returns this DTO as the JSON
+/// shape the widget expects.
+///
+public interface IAltchaChallengeProvider
+{
+ Task CreateAsync(CancellationToken cancellationToken = default);
+}
diff --git a/Svrnty.CQRS.Altcha.Abstractions/IAltchaVerifier.cs b/Svrnty.CQRS.Altcha.Abstractions/IAltchaVerifier.cs
new file mode 100644
index 0000000..59c198a
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/IAltchaVerifier.cs
@@ -0,0 +1,18 @@
+namespace Svrnty.CQRS.Altcha.Abstractions;
+
+///
+/// Verifies an Altcha solution payload. The default implementation in
+/// Svrnty.CQRS.Altcha.Grpc talks to a self-hosted altcha service
+/// over gRPC; consumers may register their own implementation (e.g. an
+/// in-process variant or a different transport) and the
+/// -based pipeline picks it up
+/// automatically.
+///
+public interface IAltchaVerifier
+{
+ ///
+ /// Base64-encoded JSON payload produced by the Altcha widget — the same
+ /// string carried on .
+ ///
+ Task VerifyAsync(string payload, CancellationToken cancellationToken = default);
+}
diff --git a/Svrnty.CQRS.Altcha.Abstractions/IHasAltchaSolution.cs b/Svrnty.CQRS.Altcha.Abstractions/IHasAltchaSolution.cs
new file mode 100644
index 0000000..5d98b8b
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/IHasAltchaSolution.cs
@@ -0,0 +1,16 @@
+namespace Svrnty.CQRS.Altcha.Abstractions;
+
+///
+/// Implemented by command and query POCOs that carry an Altcha widget
+/// solution. The framework's Altcha check reads
+/// off the materialized request, so the value travels naturally over HTTP
+/// (JSON body field) and gRPC (proto field) without any extra plumbing.
+///
+public interface IHasAltchaSolution
+{
+ ///
+ /// Base64-encoded JSON payload produced by the Altcha widget. Null /
+ /// empty causes the check to reject the request.
+ ///
+ string? AltchaSolution { get; set; }
+}
diff --git a/Svrnty.CQRS.Altcha.Abstractions/IMobileAttestationProvider.cs b/Svrnty.CQRS.Altcha.Abstractions/IMobileAttestationProvider.cs
new file mode 100644
index 0000000..c5005bc
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/IMobileAttestationProvider.cs
@@ -0,0 +1,21 @@
+namespace Svrnty.CQRS.Altcha.Abstractions;
+
+///
+/// Phase 3 placeholder — when a future module implements Apple App Attest /
+/// Google Play Integrity verification, it stamps
+/// ["mobile_attested"]
+/// based on the verification result, and the Altcha check reads that flag
+/// to short-circuit when
+/// is true.
+///
+///
+/// Intentionally left abstract and unwired in this phase. The interface
+/// exists so Phase 3 can drop in an implementation without touching command
+/// definitions or the Altcha check.
+///
+public interface IMobileAttestationProvider
+{
+ /// Platform-specific attestation token from the request.
+ /// true if attestation passes; false otherwise.
+ Task VerifyAsync(string attestationToken, CancellationToken cancellationToken = default);
+}
diff --git a/Svrnty.CQRS.Altcha.Abstractions/Svrnty.CQRS.Altcha.Abstractions.csproj b/Svrnty.CQRS.Altcha.Abstractions/Svrnty.CQRS.Altcha.Abstractions.csproj
new file mode 100644
index 0000000..ccabbb3
--- /dev/null
+++ b/Svrnty.CQRS.Altcha.Abstractions/Svrnty.CQRS.Altcha.Abstractions.csproj
@@ -0,0 +1,33 @@
+
+
+ net10.0
+ true
+ 14
+ enable
+ enable
+
+ Svrnty
+ David Lebee, Mathias Beaulieu-Duncan
+ icon.png
+ README.md
+ https://git.openharbor.io/svrnty/dotnet-cqrs
+ git
+ true
+ MIT
+
+ portable
+ true
+ true
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Svrnty.CQRS.sln b/Svrnty.CQRS.sln
index 37176b1..7ea601d 100644
--- a/Svrnty.CQRS.sln
+++ b/Svrnty.CQRS.sln
@@ -43,6 +43,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Events.Abstract
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Events.RabbitMQ", "Svrnty.CQRS.Events.RabbitMQ\Svrnty.CQRS.Events.RabbitMQ.csproj", "{3C7412EF-13C2-41F3-9D4C-D2BEC4843C8C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.Altcha.Abstractions", "Svrnty.CQRS.Altcha.Abstractions\Svrnty.CQRS.Altcha.Abstractions.csproj", "{753AF4E2-958B-4285-B4DF-8B654E5CF0FC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -257,6 +259,18 @@ Global
{3C7412EF-13C2-41F3-9D4C-D2BEC4843C8C}.Release|x64.Build.0 = Release|Any CPU
{3C7412EF-13C2-41F3-9D4C-D2BEC4843C8C}.Release|x86.ActiveCfg = Release|Any CPU
{3C7412EF-13C2-41F3-9D4C-D2BEC4843C8C}.Release|x86.Build.0 = Release|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|x64.Build.0 = Debug|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Debug|x86.Build.0 = Debug|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|x64.ActiveCfg = Release|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|x64.Build.0 = Release|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|x86.ActiveCfg = Release|Any CPU
+ {753AF4E2-958B-4285-B4DF-8B654E5CF0FC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE