Skip to content

Human checkout ​

Curvy hosts the payment page. Your shop creates a signed deposit request (without a recovery address), redirects the buyer to Curvy, and confirms the payment with verifyPayment using the attempt’s payment reference.

Keys ​

MaterialWherePurpose
Spending / viewing keysOfflineLater spend of your Curvy balance — never on the shop
Public S, V, BabyJubjubShop backendDerive one-time payment destinations
Request signing key (secp256k1)Shop backendSign the checkout payload
Signer address list/.well-known/curvy-payments.jsonLet Curvy verify the signature

A breach of public receiving keys plus the request signing key can mint fraudulent links to you. It cannot steal your shielded balance or redirect an existing payment.

Creating a payment ​

ts
import { initialize } from "@0xcurvy/payments-sdk/merchant";
import { signPaymentIntent } from "@0xcurvy/payments-sdk/intent";
import { buildCheckoutUrl } from "@0xcurvy/payments-sdk/transport";

const sdk = initialize({
  recipient: {
    S: process.env.CURVY_PUBLIC_S!,
    V: process.env.CURVY_PUBLIC_V!,
    babyJubjubPublicKey: process.env.CURVY_PUBLIC_BABYJUBJUB!,
  },
  chainId,
  merchantOrigin: "https://shop.example",
  confirmations: 12,
  ttlSeconds: 600,
});

const request = await sdk.createPaymentRequest({
  amount: orderTotal,
  token: usdcAddress,
});

const signed = await signPaymentIntent(request, (typedData) =>
  yourSigner.signTypedData(typedData),
);

const checkoutUrl = buildCheckoutUrl(CURVY_CHECKOUT_ORIGIN, signed);

Store the payment reference (ephemeralKeyX, ephemeralKeyY) when you create the request — for example in your database or in an httpOnly session cookie, whichever fits your stack. Do not put your payment reference in the signed checkout package.

Confirming a payment ​

After checkout, the customer is redirected to {merchantOrigin}{checkoutCompletePath}#txHash=…. Call verifyPayment with the stored payment reference and the hash from the URL fragment:

ts
const confirmed = await sdk.verifyPayment({
  publicClient,
  aggregatorAddress: CURVY_AGGREGATOR,
  ephemeralKey: [ephemeralKeyX, ephemeralKeyY],
  txHash: shieldTxHash,
});

See Confirming payments for optional txHash omission and retry guidance.

Publishing your signing keys ​

Serve this from your origin (CORS *, short cache):

http
GET https://shop.example/.well-known/curvy-payments.json
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=60
json
{
  "version": 1,
  "signers": [
    {
      "address": "0xYourSignerAddress",
      "alg": "eip712-secp256k1",
      "notAfter": "2027-01-01T00:00:00.000Z"
    }
  ]
}

Build the document with buildMerchantKeySet from @0xcurvy/payments-sdk/merchant/keys.

What you sign ​

EIP-712 domain name "Curvy Payments", version "1". If checkoutCompletePath is omitted from input, it defaults to /checkout/complete and is always present in the typed data.

FieldMeaning
ephemeralKeyX / ephemeralKeyYPayment reference — the on-chain identity for this attempt
tokenAsset the customer must send
amountExact amount (token units)
chainIdNetwork
ownerHashBinds the portal destination to your identity
viewTagProtocol discovery aid
merchantOriginYour origin for the return
checkoutCompletePathAbsolute path on that origin (optional; default /checkout/complete)
expiryUnix seconds

No recovery and no paymentId. Recovery is chosen on Curvy’s page (connected wallet by default).

Return URL ​

After a successful shield, checkout navigates top-level to:

{merchantOrigin}{checkoutCompletePath}#txHash=<shield tx>
PieceValue
OriginSigned merchantOrigin
PathSigned checkoutCompletePath, or /checkout/complete
QueryEmpty
FragmenttxHash= plus the shield hash (not the customer’s transfer)

The hash is a hint. Confirm with verifyPayment on your server — see Confirming payments.