Quickstart
Accept recurring stablecoin payments on Solana without holding funds.
The shape of it
Section titled “The shape of it”A mandate is the payer’s signed authorization: it names the payer, the payee, the token, a per-period cap, and a period. Charges execute against it. The payer can revoke it on-chain at any time.
There is no “customer” object and no stored card. The mandate is the relationship.
-
Install
Terminal window npm install @jorvi/sdk -
Create a client
Your secret key stays on your server. It must never reach a browser.
import { Jorvi } from '@jorvi/sdk';const jorvi = new Jorvi(process.env.JORVI_SECRET_KEY!); -
Create a mandate
Amounts are strings in atomic units, never floats.
mintis the SPL mint address of the token, so 2.00 USDC at six decimals is'2000000'.const mandate = await jorvi.mandates.create({payer: payerWalletAddress,payee: yourPayoutWalletAddress,mint: usdcMintAddress,amountPerPeriod: '2000000', // 2.00 USDCperiodSeconds: 2592000, // 30 days},{ idempotencyKey: 'order-1234' },); -
Have the payer authorize it
In the browser. The payer signs once; the cap and the exact terms are bound on-chain at this point.
import { JorviCheckout, detectInjectedWallet } from '@jorvi/checkout';const wallet = detectInjectedWallet(); // Phantom, Solflare, Backpack, and othersif (!wallet) throw new Error('No Solana wallet found');const checkout = new JorviCheckout({ baseUrl, mandateId, clientSecret }, // clientSecret comes from your server{ wallet },);await checkout.view(); // what the payer is about to approveawait checkout.authorize((stage) => console.log(stage)); -
Read what happened
await jorvi.mandates.listCharges(mandate.id);await jorvi.mandates.listRefunds(mandate.id);await jorvi.mandates.retrieve(mandate.id); -
Refund, if you need to
The destination is not a parameter. A refund can only go back to the wallet that paid.
await jorvi.charges.refund(chargeId, {amount: '500000', // optional; omit for a full refundreason: 'customer request',idempotencyKey: 'refund-1234',}); -
Verify the webhook
Charges settle on-chain, so your server learns about them through a signed webhook. Verify before trusting the payload.
import { constructEvent, SIGNATURE_HEADER } from '@jorvi/webhooks';const event = constructEvent(rawBody, // the raw string, not parsed JSONreq.headers[SIGNATURE_HEADER], // 'jorvi-signature'process.env.JORVI_WEBHOOK_SECRET!,);It throws
JorviSignatureErrorif the header is missing or malformed, if the MAC does not match, or if the timestamp falls outside the tolerance window, which is 300 seconds by default. That last check is what stops a captured request being replayed later.
Revoking
Section titled “Revoking”The payer can revoke on-chain themselves, and it needs no cooperation from you or from us. A merchant can also revoke through the API:
await jorvi.mandates.revoke(mandate.id);Once revoked, every subsequent charge fails. It is terminal.
Also on the client
Section titled “Also on the client”jorvi.paymentLinks.create({ name, payee, mint, amountPerPeriod, periodSeconds, trialSeconds });jorvi.webhookEndpoints.create({ url });jorvi.events.list({ limit });jorvi.metrics.get({ from, to });In test mode there are two helpers for moving time and funding a wallet:
await jorvi.test.fund({ owner, mint, amount });await jorvi.test.advance(2592000); // jump a period forwardKeys are prefixed by mode, sk_test_ and sk_live_. Test mode never touches
mainnet. A dockerised local validator runs the real on-chain program locally, so
you can exercise revocation and cap-exceeded paths before talking to anyone.
- How settlement works: where the money goes
- Concepts: the object model