Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Paymos.Wallet — .NET SDK

Send, swap and bridge USDT, USDC, BTC, ETH and the rest of the wallet's catalogue from .NET — no browser, no wallet extension, and no seed phrase anywhere in your environment. It is built for the services that pay people out: marketplaces, exchanges, payroll and treasury jobs.

Your process holds one half of the signing key; the Paymos server holds the other. Neither side alone can move funds — every swap and withdrawal is co-signed, and your half never reaches the server.

  • Non-custodial — a leaked API key can't sign; your co-signing share stays in your process.
  • Fee-transparent — every quote is a dry preview with a full Platform / Network / Route fee breakdown you see before signing anything.
  • Human amounts, no floats — you pass decimal strings like "100" / "0.5"; responses come back as raw integer strings.
  • netstandard2.0 + net8.0 — from .NET Framework 4.7.2 to modern .NET, one package.

The SDK talks in plain asset terms (USDC@base, ETH@arb) and hides the routing and the 2-of-2 signing behind a small, safe surface.

Install

dotnet add package Paymos.Wallet

The native co-signing core is not inside the package. On first use the SDK downloads the pinned, platform-matching build from the paymos-wallet-core releases, verifies its SHA-256 against the release's checksum, and caches it per user.

A prebuilt core exists for Linux x86-64, Linux arm64 and Windows x64. The SDK also recognises macOS on both architectures, but no macOS build is published yet — there is no Mac to produce one on, and cross-compiling an Apple dylib elsewhere is not the same thing. On any platform without a prebuilt core, build the crate yourself and set PAYMOS_NATIVE_LIB to the resulting libwallet_mpc.so / .dylib / wallet_mpc.dll. The same variable skips the download when you want it skipped: an offline host, or a corporate proxy that will not reach GitHub.

Get access & your API key

The SDK controls a Paymos vault, which you own from the Paymos wallet. The wallet is a Telegram-only Mini App — there is no website login; you use it entirely inside Telegram.

1. Open the wallet. In Telegram, open @Ox0000000000000000000Bot and tap Open Wallet. Access is invite-based. Create your vault if you don't have one.

2. Mint an API key. In the Mini App go to Settings → Vault API and create one of:

Key Can do Carries
read AssetsAsync, BalancesAsync, QuoteSwapAsync, QuoteWithdrawAsync, MovementAsync, MovementsAsync API key only
full everything a read key does, plus SwapAsync and WithdrawAsync API key + your co-signing share

The key is a vs_live_… string. A full key's co-signing share is decrypted on your device and never leaves your process. Treat a full secret like a private key.

3. Point the SDK at it with an environment variable:

export PAYMOS_VAULT_SECRET="vs_live_…"
# optional — defaults to https://wallet.paymos.io
# export PAYMOS_BASE_URL="https://wallet.paymos.io"

Quickstart

using Paymos.Wallet;

var w = Wallet.FromEnv();                   // reads PAYMOS_VAULT_SECRET

// Balances: per-asset, raw amount strings + a nullable USD value.
foreach (var b in await w.BalancesAsync())
    Console.WriteLine($"{b.Asset} {b.AmountRaw} {b.Usd}");

// Preview a withdraw — a dry, money-safe quote. Headline is the fee breakdown.
var q = await w.QuoteWithdrawAsync("USDC@base", "25", "0xRecipient…");
Console.WriteLine($"{q.Fees?.Platform} {q.Fees?.Network} {q.Fees?.Total}");

// Move money (full key only). Amounts are human decimal strings.
var mv = await w.SwapAsync("USDC@base", "ETH@arb", "100");
var settled = await w.WaitAsync(mv.Id);     // poll to a terminal status
Console.WriteLine(settled.Status);

Core concepts

Amounts: human in, raw out

Amounts you pass are human decimal strings ("100", "0.5") — the server owns each asset's decimals and scales them to raw. Amounts in responses come back as raw integer strings. Never a float, in either direction. Amounts.ParseUnits / Amounts.FormatUnits convert between the two.

Moving money

SwapAsync and WithdrawAsync (full key only) create a real movement and drive the 2-of-2 co-sign, then return a Movement. Before any signature the SDK re-checks that the server's quote echoes exactly the assets and amount you approved — a quote that does not (a decimals bug, contract drift, a misbehaving server) throws and signs nothing. The recipient address and, for exact_out, the vault debit come from the server; see Security for what that means.

var mv = await w.SwapAsync("USDC@base", "ETH@arb", "100", minReceive: "0.03");
var wd = await w.WithdrawAsync("USDC@base", "25", "0xRecipient…", maxDebit: "26");

Withdraw modes

  • "exact_out" (default) — the recipient receives exactly amount; the vault debit (amount + fees) is server-computed.
  • "total_in" — amount is the total debited from the vault; the recipient gets that minus fees.

Safety caps

  • slippageBps (swap / quoteSwap, default 50 = 0.5%) — the slippage tolerance sent with the quote; the route's quoted minimum, Receive.Min, already reflects it.
  • minReceive (swap) — a human decimal in the receive asset. If the quote's minimum receive (Receive.Min) is below it, throws SlippageExceededException and signs nothing.
  • maxDebit (swap / withdraw) — a human decimal ceiling on the total vault debit. Strongly recommended for unattended exact_out payouts, whose input side is server-computed.
  • idempotencyKey — a durable payout id; a retry after an ambiguous failure replays the same movement instead of paying twice.

Waiting and statuses

WaitAsync(id) polls a movement until it reaches a terminal status — completed | failed | refunded | expired | cancelled — and throws on timeout.

Errors

Every call throws a subclass of PaymosException, which carries Message and Status (the HTTP status, or null for a transport-level failure). Branch on the type instead of string-matching a message.

Exception When
AuthException 401 — API key missing, malformed, or unrecognized
ForbiddenException 403 — the key is valid but lacks the scope this call needs
InsufficientFundsException 400 — the vault balance can't cover the amount (plus fees)
QuoteExpiredException 400 — the referenced quote expired; fetch a fresh one
SlippageExceededException 400 or local — the quoted minimum fell below your floor
CrossAssetWithdrawNotAllowedException 400 — a withdraw changed the asset (that's a swap)
RouteUnavailableException 400 — no route could be quoted right now; retry shortly
RateLimitedException 429 — carries .RetryAfter (seconds, or null)
ConflictException 409 — idempotency key reused with a different request
PaymosException base — 404, any other 4xx/5xx, local refusals, transport errors

Calling SwapAsync / WithdrawAsync with a read-only key throws before any movement is created — a read key can never sign.

Security

  • A leaked API key alone cannot move funds — no share, no signature (and the key can be revoked).
  • A leaked share alone cannot move funds — no server co-sign.
  • Both are required, on purpose. The SDK additionally checks every message before it co-signs: it must bind to the disclosed payload, move one token from your vault, match the asset's fingerprint, and add up to no more than the quote's debit.
  • What those checks do not cover: the recipient. The route's deposit address is chosen by the server and is not verified by the SDK. And the debit ceiling is only yours if you set it — for exact_out the debit is computed by the server, so pass maxDebit on every unattended payout. The token fingerprint, likewise, is the server's unless you pin it (below).

Treat a full vs_live_… secret like a private key, and back up the share — losing it means losing your ability to co-sign.

License

MIT for this wrapper — see LICENSE. The co-signing core it downloads is proprietary; its binary distribution grants no rights to its source.


FAQ

How do I send USDT (TRC20) from code?

Install the SDK, point it at your vault, and ask for a withdrawal with the asset written the way the wallet writes it — USDT@tron. The same call sends USDT on BNB Chain (USDT@bsc), Ethereum (USDT@eth), Polygon, Solana, Arbitrum, Avalanche or TON: the asset string is the only difference.

dotnet add package Paymos.Wallet

Is this custodial? Who can move the money?

Nobody alone. The vault is 2-of-2: your process holds one signing share, the server holds the other, and a transfer needs both. A stolen API key cannot sign, and the server cannot sign without you. Your share never leaves your process.

What is the difference between a quote and a transfer?

A quote is a dry run. It prices the route, separates platform, network and route fees, and reserves nothing — ask for as many as you like. A transfer is the same numbers, signed. Nothing moves until your half of the signature exists.

Does it blind-sign whatever the server asks for?

No, and this is the part worth reading twice. The server discloses the exact message it wants signed; the SDK recomputes the digest itself and refuses unless that message belongs to your vault, moves no more than the quote's debit, moves the token whose fingerprint /assets publishes for the asset you are sending, and matches the request you made. A message failing any of those gets a refusal, not a signature.

Two things it cannot judge. The recipient — the route's deposit address — is chosen by the server and not checked. And the debit is the server's figure for exact_out unless you cap it with maxDebit; for unattended payouts, always do.

The token check is only as independent as its fingerprint. By default that comes from the same server that builds the messages, so it catches a server bug or a compromised signing path, not a server that lies in both places. To anchor it outside the server, pin the fingerprints yourself: a pinned asset is checked against your values only, and whatever /assets says is ignored.

var wallet = new Wallet(secret, pinnedFingerprints: new PinnedFingerprints
{
    // Obtained out of band, e.g. sha256 of the token id you verified yourself — not copied from
    // this server's /assets, or pinning proves nothing.
    ["USDC@base"] = "<64 lowercase hex chars>",
});

Which networks and assets are supported?

USDT moves on nine chains — Tron, Ethereum, BNB Chain, Polygon, Solana, Arbitrum, Optimism, Avalanche and TON — and USDC on eight: that same list without Tron and TON, with Base instead. Add the native coin of each of those chains, and BTC, LTC, DOGE, XRP, BCH, DASH, ZEC and ADA, and the catalogue is 21 assets across 18 chains. It is also the authority: an asset it does not list cannot be quoted or sent, in any SDK.

How are fees reported?

Every quote breaks the cost into platform, network and route, and amounts come back as raw integer strings rather than floats: money that has been through a float is money with a rounding error in it. You pass human amounts in ("100", "0.5") and read exact integers back.

Can I use it for automated payouts?

That is what it is for: headless, no browser, no wallet extension, no seed phrase in the environment. A worker holding one share can pay out continuously. Losing that machine costs the ability to sign until you issue a new secret; if it may have been stolen, revoke its API key at once — the full secret on it can co-sign until you do. Pass maxDebit on every payout it makes.

What happens if a transfer is interrupted?

Every money-moving call takes an idempotency key. Repeating a call with the same key returns the original operation instead of starting a second one, so a retry after a timeout cannot pay twice.

The same wallet, in eight languages

Every SDK here speaks to the same vault API and enforces the same rules; pick the one your service is written in. The signing core is shared, so a fix there reaches all of them.

Language Package Repository
Python pip install paymos-wallet python-wallet-sdk
TypeScript / Node.js npm install @paymos/wallet typescript-wallet-sdk
Go go get github.com/paymos-labs/go-wallet-sdk go-wallet-sdk
Rust cargo add paymos-wallet rust-wallet-sdk
C# / .NET (this one) dotnet add package Paymos.Wallet csharp-wallet-sdk
Java io.paymos:wallet java-wallet-sdk
Ruby gem install paymos-wallet ruby-wallet-sdk
PHP composer require paymos/wallet php-wallet-sdk
Signing core paymos-wallet-core paymos-wallet-core

Documentation

  • wallet.paymos.io — the product, the supported networks, and guides for the routes people ask about most: USDT BEP20 to TRC20, USDC between chains, and the rest.
  • Networks and assets — every chain and token the wallet carries, with the standard each one is named by.
  • USDT BEP20 → TRC20 — a worked route, priced live; the other pairs are linked from it.

About

.NET SDK for the Paymos wallet vault (Paymos.Wallet on NuGet).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages