Skip to content
Privatly

Developers

Confidentiality you can program.

Encrypted balances, claimable transfers, view grants and policy-bound agents on Solana, behind one SDK: @privatly/sdk.

The SDK described here is in development; today the app uses these interfaces internally. The REST routes below are live and documented as they run.

Start

Quickstart

Install the SDK, wrap a privacy provider for the connected wallet, unlock and register once.

1Install

Terminal
npm i @privatly/sdk

2Create a client

The client wraps a PrivacyProvider. The live provider is Umbra encrypted token accounts (Arcium). Keys are derived from one wallet signature; the SDK never sees a seed phrase or private key.

client.ts
import { createPrivacyClient, httpQuoter } from "@privatly/sdk";

// provider: a PrivacyProvider bound to the connected wallet account
// (Umbra on Solana mainnet). Signing stays inside the wallet.
const client = createPrivacyClient({
  provider,
  quote: httpQuoter("https://privatly.xyz"),
});

await client.unlock();   // one signature derives this wallet's private keys
await client.register(); // once per wallet: creates the encrypted account

Core

Balances and transfers

Shield into an encrypted balance, send receiver-claimable transfers, claim what was sent to you. Deposits and withdrawals between public wallets and encrypted balances stay visible on-chain.

  1. 01

    Quote

    The server returns the fee for the action and amount, with a short expiry.

  2. 02

    Prove and sign

    A Groth16 proof is generated in the browser; the wallet signs the transaction.

  3. 03

    Finalize

    Arcium's encrypted computation updates the balance; the callback is observed.

send.ts
// Public wallet to encrypted balance. The deposit is visible on-chain.
await client.shield("USDC", 250_000_000n); // 250 USDC in base units

// Receiver-claimable transfer from the encrypted balance.
const { result, quote } = await client.send({
  symbol: "USDC",
  amount: 100_000_000n,
  recipient,
  source: "private",
  amountUsd: 100, // from your price source, used for the fee quote only
});

quote.privacyExecutionFee; // show before signing; the SDK does not collect it
result.signatures;         // signed by the user's wallet
Private operation result fields
FieldTypeDescription
signaturesstring[]Signed by the user's wallet. The server verifies these when an execution is reported.
relatedSignaturesstring[]Not signed by the user: encrypted computation callbacks and relayer transactions.
callback"finalized" | "pending" | "failed" | "none"Whether the encrypted computation finished. failed means staged funds may need recovery.

Claim on the receiving side

claim.ts
// Recipient side: find transfers sent to this wallet, then claim them.
const pending = await client.scanClaimable();
const { claimedIds, failed } = await client.claim(pending);

Autonomy

Agents

An agent is a server-held key plus a Squads smart account. You sign the account and its daily spending limits; the runtime executes structured objectives within them.

  1. 1.Create the agent. The server generates its key and stores it encrypted at rest (AES-256-GCM).
  2. 2.Sign the Squads smart account creation and the per-token daily spending limits. Those limits are the hard cap.
  3. 3.Fund the vault with what the agent may spend. Vault balances are public on-chain.
  4. 4.Add objectives: a TWAP swap through Jupiter or a recurring payment.
  5. 5.On every run the server evaluates your policy first. Above the approval threshold, you sign the vault transaction yourself.

Target SDK shape

In the app this runs on the /api/agents routes with your session. previewPolicy exists today and runs the same evaluation the server does; the agent handle is the interface we are building toward.

agent.ts
import { previewPolicy, type PrivateAgent } from "@privatly/sdk";

// Same evaluation the server runs before every agent execution.
const decision = previewPolicy(policy, {
  amountUsd: 120,
  asset: "USDC",
  category: "payments",
  spentTodayUsd: 300,
});

// Target shape, backed today by /api/agents/** in the app.
const agent: PrivateAgent = await agents.get(agentId);
await agent.addObjective({
  kind: "recurring_payment",
  asset: "USDC",
  amount: 120,
  recipient: vendor,
  intervalDays: 7,
  maxRuns: 12,
});

Runs are picked up by a scheduled job on the server. A client-side policy check is a preview; the server decides, and the on-chain limit caps.

Disclosure

View keys

A view key is an on-chain compliance grant (Umbra compliance grants (X25519)). It lets a named viewer decrypt your activity. It is revocable at any time.

disclose.ts
// Let an auditor decrypt this wallet's activity. Revocable on-chain.
const { signature, record } = await client.grantViewAccess({
  granteeAddress: auditorWallet,
  granteeX25519: auditorX25519PublicKey, // 32 bytes
});

// Keep record.nonce: revoking needs it.
await client.revokeViewAccess(record);
Grant record fields
FieldTypeDescription
granteeAddressstringWallet authorized to request re-encryption.
granteeX25519Uint8ArrayThe viewer's X25519 public key, 32 bytes.
granterX25519Uint8ArrayYour master viewing public key.
noncebigintIdentifies the grant on-chain. Required to revoke.

Grants have no on-chain expiry. An expiry date in the app is a reminder to revoke, not an enforced deadline. Agents cannot create grants.

REST

API

These routes run the app today. They use a Sign in with Solana session cookie, so they are built for the browser. A key-authenticated public API is part of the SDK work.

REST routes
RouteAuthDescription
Session
GET/api/auth/nonceNoneIssues a sign-in nonce, bound to an httpOnly cookie for 10 minutes.
POST/api/auth/verifyNoneVerifies a Sign in with Solana signature and starts a 7 day session.
GET/api/auth/sessionNoneCurrent session wallet and admin flag, or null.
POST/api/auth/logoutNoneEnds the session.
Pricing
POST/api/quoteNoneAuthoritative fee quote. Plan tier from the session; optional x-api-key is metered.
GET/api/pricesNoneUSD prices for supported tokens from the Jupiter Price API.
Swaps and executions
GET, POST/api/swap/quoteSessionJupiter ExactIn quote with the platform fee for the session wallet's plan.
POST/api/swap/buildSessionBuilds the Jupiter swap transaction for the session wallet to sign.
POST/api/executionsSessionReports a confirmed execution. Signatures are verified on-chain, the quote settles into the ledger.
GET/api/activitySessionActivity of the session wallet. Optional ?agent=<id>.
Disclosure and plans
GET, POST/api/view-keysSessionView grant metadata. The grant itself is created on-chain first and verified here.
POST/api/view-keys/revokeSessionMarks a grant revoked after the on-chain revocation is verified.
GET, POST/api/subscriptionsSessionCurrent plan; activate a plan from a verified USDC payment to the treasury.
Messaging
GET, POST/api/messaging-keysSessionPublish your X25519 public key; look up another wallet's key.
GET, POST/api/messages/conversationsSessionList conversations; create or return one with a peer.
GET, POST/api/messages/{conversationId}MemberRead and send sealed messages. The server sees ciphertext only.
POST/api/messages/{conversationId}/readMemberRead marker.
Agents
GET, POST/api/agentsSessionList agents; create one (server-generated key, encrypted at rest).
GET, PATCH, DELETE/api/agents/{id}OwnerDetail with live on-chain state; pause, resume, edit policy; delete when empty.
POST/api/agents/{id}/setupOwnerSquads smart account lifecycle: create, spending limits, fund, withdraw.
GET, POST, PATCH/api/agents/{id}/objectivesOwnerStructured objectives: TWAP swap or recurring payment.
GET/api/agents/{id}/executionsOwnerExecution history.
GET, POST/api/agents/{id}/approvalsOwnerApprove (owner-signed vault transaction) or decline a pending run.
GET/api/vaultsSessionAgent vaults with live balances and spending limits.
Integrators
GET, POST/api/integratorsSessionYour integrator record, usage and payouts; register one per wallet.
POST/api/integrators/keysSessionCreate an API key. The full key is returned once.
DELETE/api/integrators/keys/{id}SessionRevoke an API key.

Errors

Every error is JSON: { "error": string }. Unexpected failures return 500 with no internal details.

Error statuses
FieldTypeDescription
400statusInvalid JSON or malformed parameter
401statusSign in required, expired sign-in, or invalid API key
403statusWallet does not match the session, or plan limit reached
404statusNot found, or not yours
409statusConflict: already applied, outdated quote, or limit reached
422statusSchema validation failed. Body includes zod issues
502statusUpstream (Jupiter, RPC) unavailable

Sign in with Solana

Session
// 1. Nonce (sets an httpOnly cookie scoped to /api/auth)
GET /api/auth/nonce
200 { "nonce": "9f2c4a..." }

// 2. The wallet signs a Sign in with Solana message containing that nonce,
//    the current host as domain, and an issuedAt within the last 10 minutes.

// 3. Verify
POST /api/auth/verify
{
  "address": "<base58 wallet>",
  "signedMessage": "<base64 message bytes>",
  "signature": "<base64 ed25519 signature>"
}
200 { "wallet": "<base58 wallet>" }   // sets privatly_session, 7 days

POST /api/quote

Accepts an action, a USD amount and an asset, never a fee. The plan tier comes from the session. Signed-in requests get a persisted quote id that /api/executions settles once. An x-api-key header, when present, must be valid and is counted for that integrator.

Terminal
curl -X POST https://privatly.xyz/api/quote \
  -H "Content-Type: application/json" \
  -H "x-api-key: $API_KEY" \
  -d '{"action":"PRIVATE_TRANSFER","amountUsd":1000,"asset":"USDC"}'
Request body
{
  "action": "PRIVATE_TRANSFER", // PRIVATE_SWAP | PRIVATE_TRANSFER | AGENT_EXECUTION
                                // PRIVATE_REBALANCE | ENCRYPTED_MESSAGE | VIEW_KEY
  "amountUsd": 1000,            // 0 to 1,000,000,000
  "asset": "USDC"               // 1 to 64 characters
}
Quote response fields
FieldTypeDescription
privacyExecutionFeemicro-USDProtocol fee plus relay cost. The single fee line users see.
networkCostmicro-USDEstimated Solana fee. Paid by the wallet to the network.
totalFeemicro-USDprivacyExecutionFee + networkCost.
discountsAppliedDiscount[]Plan or volume discounts, each with a reason. A fee floor applies.
expiresAtepoch msQuotes are short lived. Request a new one after expiry.
iduuid?Present for signed-in requests. Reference it when reporting the execution.
200 OK
{
  "quote": {
    "action": "PRIVATE_TRANSFER",
    "protocolFee": {
      "$big": "800000"
    },
    "providerCost": {
      "$big": "0"
    },
    "relayerCost": {
      "$big": "0"
    },
    "networkCost": {
      "$big": "2000"
    },
    "integratorShare": {
      "$big": "0"
    },
    "privacyExecutionFee": {
      "$big": "800000"
    },
    "totalFee": {
      "$big": "802000"
    },
    "baseProtocolFee": {
      "$big": "800000"
    },
    "discounts": [],
    "ruleSnapshot": {
      "model": "bps_with_cap",
      "bps": 8,
      "minimumUsd": 0.05,
      "maximumUsd": 25
    },
    "quotedAt": 1791329918362,
    "expiresAt": 1791329948362,
    "source": "server"
  },
  "tier": "free"
}
Example for a 1,000 USD transfer on the free plan. Amounts are bigint micro-USD serialized as { "$big": "..." }.

GET /api/prices

Prices
GET /api/prices
200 {
  "prices": { "SOL": <usd>, "USDC": <usd>, "USDT": <usd>, "JitoSOL": <usd> }, // missing when unpriced
  "at": <epoch ms>
}

Swaps

The server decides the fee and the treasury account again when building, so a tampered quote is refused.

Swap
GET /api/swap/quote?inputMint=<mint>&outputMint=<mint>&amount=<base units>&slippageBps=50
200 {
  "quote": { /* Jupiter quote response, platformFee included when collectable */ },
  "fee": { "bps": 15, "mint": "<mint the fee is taken in>" }  // bps 0 when not collectable
}

POST /api/swap/build
{ "userPublicKey": "<session wallet>", "quoteResponse": { /* quote from above */ } }
200 { "transaction": "<base64 versioned transaction>", "lastValidBlockHeight": <number | null> }
409 { "error": "Quote is outdated. Refresh and try again." }

POST /api/executions

Report an execution
POST /api/executions
{
  "kind": "private_transfer",      // private_swap | private_transfer | shield | unshield | claim
                                   // view_key_created | view_key_revoked | agent_setup | subscription
  "title": "Sent USDC",
  "signatures": ["<signature>"],   // 1 to 8, each verified on-chain as signed by the session wallet
  "quoteId": "<uuid from /api/quote>", // optional: settles that quote into the revenue ledger once
  "publicFields": [{ "label": "Asset", "value": "USDC" }],
  "privatePayload": "<ciphertext>" // optional, encrypted on the client
}
200 { "id": "<activity id>" }
422 { "error": "Transaction not signed by this wallet (5Kd1x2ab…)" }

POST /api/subscriptions

The wallet first sends the plan price in USDC to the treasury. The server checks the transfer on-chain: signer, destination, amount, age, and that it was never used before.

Plans
POST /api/subscriptions
{ "tier": "pro", "signature": "<USDC transfer to the treasury, signed by the session wallet>" }
200 { "tier": "pro", "expiresAt": "2026-11-04T12:00:00.000Z" }
409 { "error": "This payment was already applied" }

Program

Integrator program

Wallets, apps and agent platforms that route executions through their integration earn a share of the protocol fee. Register and issue API keys in the dashboard today.

Swap fee: 0.15% of volume. Integrator share: 3 bps, never more than half the protocol fee. Minimum payout $50.

Integrator and affiliate comparison
AttributeIntegratorAffiliate (planned)
What they doEmbed the product in a wallet, app or agent platformSend users to the product
Share3 of 15 bps on attributed swaps (20% of the fee)10% of fees from referred users
DurationFor as long as executions route through the integration180 days per referred user
AttributionVerified only: API keys, hashed server sideReferral link, time bounded

Status: integrator records, API keys and request metering are live. Attributing settled executions to a key ships with the public API; until then attributed revenue is zero. Client-supplied attribution is always ignored.

Create a key

API keys
POST /api/integrators/keys
{ "label": "Production" }
201 {
  "key": "privatly_live_…",   // shown once; only a SHA-256 hash is stored
  "record": { "id": "<uuid>", "label": "Production", "prefix": "privatly_live_ab12cd",
              "createdAt": "…", "lastUsedAt": null, "revokedAt": null }
}
Open the integrator dashboard