Developers

Rent back.
Inside your dApp.

Since SIMD-0437 your users’ token accounts hold more rent than Solana asks for. Add UNRENT where they connect their wallet: one scan, two clicks, in your design. Your app keeps 5% of every reclaim.

01 Overview

A few lines.
No backend.

No API key, no server of your own, no dependency. The browser talks to UNRENT, the user’s wallet signs, the SOL lands in the user’s wallet and your share in yours, in the same transactions.

The flow

  1. 1 wallet connects → free scan: reclaimable SOL, fee, net
  2. 2 the widget shows the net amount; the user clicks Reclaim
  3. 3 build → unsigned transactions, checked in the browser
  4. 4 the user’s wallet signs
  5. 5 submit → SOL in the user’s wallet, 5% in yours

02 Live preview

Demo data · nothing is signed

Your fonts.
Your colors.

The real widget, on demo numbers. It sits in a shadow root, so your CSS cannot break it; eight CSS variables make it look like your app. The only UNRENT mark is a small “powered by unrent.xyz”.

Theme

Variant

CSS for this theme
unrent-reclaim {
  --unrent-font: system-ui, sans-serif;
  --unrent-bg: #16131f;
  --unrent-fg: #f4f2ff;
  --unrent-muted: #a39fb8;
  --unrent-border: #2b2640;
  --unrent-accent: #9945ff;
  --unrent-accent-fg: #ffffff;
  --unrent-radius: 16px;
}

03 Quick start

Three steps.
Ten minutes.

  1. 01Get your partner code

    Open Referral, connect the wallet that should receive your share and sign the message (no transaction, no cost). The code you pick, e.g. yourdapp, is your partner code. The same page shows your reclaims and earnings.

  2. 02Add the widget

    Script tag: any site, no build step
    <script src="https://unrent.xyz/embed/v1.js" defer></script>
    
    <unrent-reclaim partner="yourdapp"></unrent-reclaim>
    npm
    npm install github:unrentsol/unrent-embed#v0.1.0
    Mount it
    import { mount } from 'unrent-embed';
    
    const widget = mount('#unrent', { partner: 'yourdapp' });
  3. 03Hand it the connected wallet

    The widget never connects wallets itself; it uses the one your app connected. Give it a signer and update it when the wallet changes. While no wallet is connected, or there is nothing to reclaim, it renders nothing.

    import { fromWalletStandard } from 'unrent-embed';
    
    widget.setSigner(fromWalletStandard(wallet, wallet.accounts[0])); // connected
    widget.setSigner(null);                                           // disconnected
    
    // Custom element: set the property instead
    document.querySelector('unrent-reclaim').signer = signer;

04 Wallet libraries

Any wallet.
One signer.

A signer is an address and a function that signs wire transactions without sending them. Helpers cover the common libraries; anything else fits createSigner.

React + @solana/wallet-adapter-react
import { useEffect, useRef } from 'react';
import { useWallet } from '@solana/wallet-adapter-react';
import { VersionedTransaction } from '@solana/web3.js';
import { fromWalletAdapter, mount, type Widget } from 'unrent-embed';

export function ReclaimRent() {
  const wallet = useWallet();
  const el = useRef<HTMLDivElement>(null);
  const widget = useRef<Widget | null>(null);

  useEffect(() => {
    widget.current = mount(el.current!, { partner: 'yourdapp' });
    return () => widget.current?.destroy();
  }, []);

  useEffect(() => {
    widget.current?.setSigner(
      wallet.connected ? fromWalletAdapter(wallet, VersionedTransaction) : null
    );
  }, [wallet.connected, wallet.publicKey]);

  return <div ref={el} />;
}
@solana/react · @solana/kit
import { useSignTransaction } from '@solana/react';
import { createSigner } from 'unrent-embed';

const signTransaction = useSignTransaction(account, 'solana:mainnet');
const signer = createSigner(account.address, async (txs) => {
  const out = [];
  for (const transaction of txs) out.push((await signTransaction({ transaction })).signedTransaction);
  return out;
});
Wallet Standard, no adapter library
import { getWallets } from '@wallet-standard/app';
import { fromWalletStandard } from 'unrent-embed';

const wallet = getWallets().get().find((w) => w.name === 'Phantom');
const signer = fromWalletStandard(wallet, wallet.accounts[0]);
Anything else
import { createSigner } from 'unrent-embed';

// Uint8Array[] in, signed Uint8Array[] out: unchanged, in order, not sent.
const signer = createSigner(address, (txs) => myWallet.signAll(txs));

Vue, Svelte and plain HTML examples are in the repository.

05 Make it yours

Eight variables.
Seven options.

Set the variables on the element or any parent. For finer control: ::part(container), ::part(button), ::part(powered).

--unrent-font
Font family
inherit
--unrent-bg
Background
transparent
--unrent-fg
Text
currentColor
--unrent-muted
Secondary text
62% of the text color
--unrent-border
Hairline border
16% of the text color
--unrent-accent
Button
#14f195
--unrent-accent-fg
Button text
#000
--unrent-radius
Corner radius
12px

mount(element, options)

partner
Your partner code from /referral.
required
variant
'banner': one line for headers and sidebars.
'card'
theme
The variables above as an object: font, background, foreground, muted, border, accent, accentForeground, radius.
inherit
labels
Replace any text, e.g. for translations (DEFAULT_LABELS).
English
hideWhenEmpty
false also shows “connect a wallet”, “checking…” and “nothing to reclaim”.
true
dismissible
A close button; hides the widget for that wallet until reload.
true
onScan · onReclaim · onError
Callbacks. The custom element fires unrent:scan, unrent:reclaim, unrent:error.

06 Headless client and API

Your own UI.
Same checks.

The widget runs on this client. reclaim() builds, checks every transaction, has the wallet sign and submits. Base URL https://unrent.xyz/api/v1, open to any origin, all free.

unrent-embed
import { createUnrent } from 'unrent-embed';

const unrent = createUnrent({ partner: 'yourdapp' });

const scan = await unrent.scan(address);
// { reclaimableSol, accounts, fee: { bps, sol }, partner: { bps, sol }, netSol, nextStep }

if (scan.netSol > 0) {
  const result = await unrent.reclaim(signer, {
    onStatus: (s) => console.log(s) // building → signing → sending
  });
  // { status: 'confirmed', netSol, transactions: [{ signature, explorer }], leftForNextClaim }
}

POST/embed/scan

{ wallet, partner }

Reclaimable SOL, the fee, your share, the net and the next rent step. Reads the chain only.

POST/embed/build

{ wallet, partner, accounts? }

Unsigned, simulated v0 transactions (base64) and a claimId. No burns.

POST/reclaim/submit

{ claimId, signed }

Checks the signed transactions against the build and sends them.
Try it
curl -s -X POST https://unrent.xyz/api/v1/embed/scan \
  -H 'content-type: application/json' \
  -d '{"wallet":"<address>","partner":"yourdapp"}'

Errors

400
Invalid wallet, unknown or paused partner code
404
Nothing to reclaim (any more)
409
A claim of this wallet is still on its way: wait a minute
422
The wallet cannot pay the network fee, or a simulation failed
429
Rate limited per visitor IP
503
Busy or paused: retry shortly

Every call, type and error: API reference.

07 Your share

10% fee.
Split in two.

The service fee is 10% of the reclaimed SOL. For a reclaim in your app, 5% of the reclaimed SOL goes to your wallet and 5% to UNRENT. Your users pay exactly what they pay on unrent.xyz.

Example: 1 SOL reclaimed

User
0.90 SOL
Your app
0.05 SOL
UNRENT
0.05 SOL
Paid on chain
Your share is a transfer inside the user’s reclaim transactions, so it lands the moment the reclaim does. Nothing to invoice, claim or withdraw.
Every claim in your app
Whoever referred a wallet before, a reclaim made in your interface pays you. Your app does not bind wallets: reclaims on unrent.xyz follow the usual referral rules.
Holder discount
$UNRNT holders pay a lower fee (tiers). Your share is capped at the fee the wallet pays, so a holder at 5% or less pays it all to you.
Payout wallet
It must hold Solana’s minimum balance (about 0.00089 SOL). Until it does, payouts into it would fail the user’s transaction, so they are skipped and the whole fee goes to UNRENT.

08 Security

Checked twice.
Before signing.

UNRENT never holds keys or funds, and neither does your app. On top of the server’s checks, the client parses every transaction in the browser and signs nothing unless all of them are plain reclaims:

  • ✓A v0 transaction without address lookup tables, so every account is visible, paid by the connected wallet.
  • ✓Token and Token-2022: only WithdrawExcessLamports and CloseAccount, each paying the wallet and authorized by it. No transfers, approvals, burns or authority changes.
  • ✓System: only transfers out of the wallet (the fee). Compute Budget is allowed. Any other program is refused.
  • ✓All transfers together are at most the fee the build announced.

So even a compromised server could at worst ask for the announced fee, never for tokens or more SOL. The server re-scans on every build, simulates every transaction and accepts only the transactions it built. Details →

09 Good to know

Short list.
Read once.

Mainnet
The API serves Solana mainnet.
No burns
The embed never touches tokens: it returns excess rent and closes accounts that are already empty. Dust clean-up stays on unrent.xyz.
Network fee
The user’s wallet pays Solana’s fee, a few thousandths of a cent per transaction, so it needs a little SOL.
Large wallets
One reclaim holds up to 8 transactions, as wallets limit what they sign at once. The widget then offers “Reclaim the rest”.
Rate limits
Scans are limited per visitor IP, as on unrent.xyz. The widget scans once per wallet connection.
CSP
Allow connect-src https://unrent.xyz, and script-src https://unrent.xyz for the script tag.
Branding
Keep “powered by unrent.xyz” visible and the fee in the text: users should know what they sign.
Open source
MIT licensed, on GitHub. Issues and pull requests welcome. unrentsol/unrent-embed ↗