PayGate.SDK
0.0.1
dotnet add package PayGate.SDK --version 0.0.1
NuGet\Install-Package PayGate.SDK -Version 0.0.1
<PackageReference Include="PayGate.SDK" Version="0.0.1" />
<PackageVersion Include="PayGate.SDK" Version="0.0.1" />
<PackageReference Include="PayGate.SDK" />
paket add PayGate.SDK --version 0.0.1
#r "nuget: PayGate.SDK, 0.0.1"
#:package PayGate.SDK@0.0.1
#addin nuget:?package=PayGate.SDK&version=0.0.1
#tool nuget:?package=PayGate.SDK&version=0.0.1
PayGate.SDK
.NET SDK for the PayGate.to payment gateway — the API behind withoutkyc.to.
Customers pay with cards, bank transfers or local rails through one of ~20 on-ramp providers, and the merchant is settled in USDC on Polygon. No API key is involved: every payment is bound to a freshly created encrypted receiving wallet.
Install
dotnet add package PayGate.SDK
Target framework: net9.0.
Quick start
builder.Services.AddPayGateClient("0xF977814e90dA44bFA03b6295A0616a897441aceC");
public class CheckoutService(IPayGateClient payGate) {
public async Task<string> StartAsync(Order order, CancellationToken cancellationToken) {
// 1. One wallet per payment, with a callback URL unique to this order.
var wallet = await payGate.CreateWalletAsync(
new CreateWalletRequest {
callbackUrl = $"https://example.com/paygate/callback?order={order.id}"
},
cancellationToken
);
// 2. Store what you need to reconcile the callback later.
order.receivingAddress = wallet.polygonAddressIn;
order.ipnToken = wallet.ipnToken;
// 3. Redirect the customer to the hosted checkout.
return payGate.BuildHostedCheckoutUrl(new HostedCheckoutRequest {
addressIn = wallet.addressIn,
amount = order.amount,
currency = "USD",
email = order.customerEmail
});
}
}
How the HTTP client is used
This is the part worth reading before you ship. A payment gateway SDK is easy to get wrong in a way that only shows up under load, as SocketException: Only one usage of each socket address is normally permitted — ephemeral port exhaustion.
The SDK avoids it as follows.
One handler per gateway host, shared by every request. A SocketsHttpHandler owns a connection pool; a new handler means a new pool. The SDK registers exactly one named HttpClient per host through IHttpClientFactory, so api.paygate.to and checkout.paygate.to each get one pool and never contend with each other.
A cap on concurrent connections. On .NET MaxConnectionsPerServer is unlimited by default, so 500 concurrent calls will happily open 500 sockets. PayGateOptions.maxConnectionsPerServer (default 32) makes the surplus queue on existing connections instead:
builder.Services.AddPayGateClient(payoutAddress, options => {
options.maxConnectionsPerServer = 64;
});
Connections are retired on a timer, not the handler. PooledConnectionLifetime (default 5 minutes) recycles connections so a long-lived client cannot pin a stale DNS record — the classic singleton-HttpClient bug. Because the handler already does this, the SDK sets the factory handler lifetime to Timeout.InfiniteTimeSpan; otherwise IHttpClientFactory would build a second handler every two minutes and each one would carry its own pool.
IPayGateClient is a stateless thread-safe singleton. Inject it anywhere, including into singletons. Do not wrap it in using.
Building a checkout URL costs nothing. BuildHostedCheckoutUrl and BuildProviderCheckoutUrl are pure string operations — no socket, no await. Only ResolveProviderPaymentUrlAsync makes a request.
Where the money goes
Funds travel provider → encrypted receiving wallet (polygon_address_in, held by the gateway) → your payout wallet. value_forwarded_coin in the callback is what actually arrived after the 1% platform fee.
The payout wallet can come from four places, in this order of precedence:
| Source | Use when |
|---|---|
request.payoutAddress |
A different wallet per transaction. |
IPayoutAddressProvider |
The wallet lives in an admin panel or database and can change at runtime. |
IPayGateClientFactory.CreateClient(address) |
Multi-tenant, wallet resolved per tenant. Beats the provider — you named it explicitly. |
PayGateOptions.payoutAddress |
One fixed wallet for the whole application. |
A payout on Polygon is irreversible, and the gateway only rejects addresses that are structurally impossible. A typo in an otherwise well-formed address sends real funds to a wallet nobody controls. So every address is checked before the request leaves the process: the shape always, and the EIP-55 checksum whenever the address is mixed-case — which is what catches a single mistyped or transposed character. This applies to payout, affiliate, sub-affiliate and every mass-payout wallet.
EvmAddress is public, so validate on the way in too — at the admin form, not just at payment time:
if (!EvmAddress.IsValid(form.payoutWallet)) {
return ValidationProblem("Not a valid Polygon wallet address.");
}
settings.payoutWallet = EvmAddress.ToChecksum(form.payoutWallet); // store canonical form
Note that a mass payout has no merchant address at all — the payout wallets are exactly the keys of fees, so payoutAddress is ignored for that call.
If the gateway ever accepts a format EvmAddress does not recognize, set validateAddresses = false on the request.
Registration
| Method | Use when |
|---|---|
AddPayGateClient(payoutAddress, configure?) |
One merchant wallet for the whole application. |
AddPayGateClient(configure?) |
The payout wallet is set per request or by a provider. |
AddPayGatePayoutAddressProvider(resolve) |
The wallet is read from an admin panel or database. |
AddPayGateClientFactory(configure?) |
Multi-tenant: payout wallet or branding resolved per request. |
Wallet from an admin panel
AddPayGateClient(payoutAddress) captures the address at startup, so it cannot change without a restart. Register a provider instead — it is consulted on each wallet creation, and what it returns is validated the same way:
builder.Services.AddPayGateClient();
builder.Services.AddPayGatePayoutAddressProvider(async (services, cancellationToken) => {
var db = services.GetRequiredService<AppDbContext>();
return await db.settings
.Select(s => s.payoutWallet)
.FirstOrDefaultAsync(cancellationToken);
});
The callback runs in its own DI scope, so a DbContext can be used directly without a captive-dependency problem. It fires once per wallet creation, so cache inside the callback if the lookup is expensive. Returning null falls back to PayGateOptions.payoutAddress.
Wallet per transaction
var wallet = await payGate.CreateWalletAsync(new CreateWalletRequest {
callbackUrl = $"https://example.com/paygate/callback?order={order.id}",
payoutAddress = order.sellerWallet
});
builder.Services.AddPayGateClientFactory(options => {
options.timeout = TimeSpan.FromSeconds(20);
options.maxConnectionsPerServer = 64;
});
// later, per tenant — all clients share the same connection pools
var client = factory.CreateClient(tenant.payoutWallet, tenant.branding);
Outside a DI container, keep one PayGateClientFactory for the process lifetime and dispose it on shutdown:
using var factory = new PayGateClientFactory();
var client = factory.CreateClient("0xF977814e90dA44bFA03b6295A0616a897441aceC");
API surface
| Member | Endpoint |
|---|---|
CreateWalletAsync |
GET /control/wallet.php |
CreateAffiliateWalletAsync |
GET /control/affiliate.php |
CreateCustomAffiliateWalletAsync |
GET /control/custom-affiliate.php |
CreateSubAffiliateWalletAsync |
GET /control/custom-sub-affiliate.php |
CreateMassPayoutWalletAsync |
POST /control/mass-payout.php |
GetProvidersAsync |
GET /control/provider-status/ |
ConvertToUsdAsync |
GET /control/convert.php |
GetPaymentStatusAsync |
GET /control/payment-status.php |
BuildProviderCheckoutUrl |
GET /process-payment.php (URL only, no request) |
BuildHostedCheckoutUrl |
GET /pay.php (URL only, no request) |
ResolveProviderPaymentUrlAsync |
GET /process-payment.php, returns the Location target |
Callbacks
The gateway notifies the callback URL with a GET request when a payment settles. Callbacks are not signed, so treat one as a hint and verify it:
app.MapGet("/paygate/callback", async (HttpRequest request, OrderStore orders) => {
if (!PayGateCallbackParser.TryParse(request.QueryString.Value, out var callback)) {
return Results.BadRequest();
}
var order = await orders.FindAsync(callback.GetParameter("order"));
if (order == null || !callback.MatchesWallet(order.receivingAddress)) {
return Results.BadRequest();
}
if (callback.valueCoin < order.expectedUsd) {
return Results.Ok(); // underpaid — do not fulfil
}
await orders.MarkPaidAsync(order, callback.txidOut);
return Results.Ok();
});
callback.parameters holds every query parameter, including the ones you put into the callback URL yourself, so GetParameter("order") returns your own order id.
Things the gateway is strict about
- A wallet per payment. Reusing a callback URL returns the previously created wallet instead of a new one, and two orders end up sharing a receiving address.
- Fee splits must total
0.99. The remaining 1% is the platform fee. The SDK validates this before the request leaves the process and names the actual total in the exception. Shares are capped at 4 decimal places, which is what the query string carries — accepting more would let a split that sums to0.99locally go out rounded and no longer total it. - Request objects are single-use.
IPayGateClientitself is thread-safe, but do not mutate or share a request instance while a call using it is in flight. - Mass payouts are capped at 19 wallets.
- Affiliate and merchant wallets must differ.
- Some providers are single-currency —
stripe,transfi,robinhoodandbitnovoare USD only,upiis INR only,interacis CAD only. CheckGetProvidersAsyncfor the live list and minimums. - No iframes. The customer must be redirected to the provider.
GetPaymentStatusAsyncis rate limited. Drive order state from callbacks and use it for reconciliation, not polling. A429surfaces asPayGateRateLimitExceptionwithretryAfter.
Encoding
Every query value is percent-encoded exactly once, so pass callback URLs, emails and logo URLs unencoded. If you pre-encode a callback URL it goes out double-encoded and the gateway stores the wrong value.
address_in is the one exception: the API returns it already encoded, so it is accepted in either form and normalized to a single level. That is only safe because it is Base64 and cannot contain a literal % — the same trick applied to a callback URL would strip a level from a query that legitimately contains %26 and the callback would arrive split into two parameters.
Exceptions
All SDK exceptions derive from PayGateApiException, which carries statusCode and responseBody.
| Exception | Cause |
|---|---|
PayGateValidationException |
400 — bad wallet, reused callback, invalid fee split |
PayGateRateLimitException |
429 — carries retryAfter |
PayGateNotFoundException |
404 |
PayGateHttpException |
network failure, timeout, other non-success status |
PayGateApiException |
malformed or empty response body |
Argument problems are reported as plain ArgumentException before any request is made.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. 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 was computed. 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. |
-
net9.0
- Microsoft.Extensions.Http (>= 10.0.0)
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 |
|---|---|---|
| 0.0.1 | 86 | 7/26/2026 |