Reward your users with stocks
How it works
- A user clicks one of your widgets (or your campaign's link, anywhere) and lands on your campaign's page on sprout.gift.
- They sign in with their wallet, the one they use with your project. Signing is free and sends no transaction.
- Sprout checks each task for that wallet (on-chain, on Farcaster, or by asking your endpoint) and shows the user a checklist. Each task can have a Go button that takes them to it.
- Once every task is done, the user opens their card: their browser makes a Claim Code and their wallet signs, for free, that the card goes to it. Sprout checks every task once more, buys the campaign's stocks from your budget and locks them in a card only that code can claim.
- They save the code and claim, right away or after your unlock date, to any wallet.
One card per wallet, enforced on-chain, along with the per-card maximum, the card count and the end date. Your budget stays yours and only ever goes back to your treasury wallet, set when you create the campaign: withdraw any time, and whatever is left when the campaign ends is sent there automatically.
Built-in tasks
Sprout checks these itself. You paste a link when you create the campaign; nothing to build or host.
| Task | You give | Sprout checks |
|---|---|---|
| Use your app | A transaction where you did the action yourself (a swap, a deposit, a mint…) | That the user's wallet shows up in the same event of the same contract, in a transaction made after the campaign starts. Smart wallets count too. |
| Hold your token | The token or NFT collection's address, and how many | The wallet's balance when the user checks |
| Farcaster | Your profile, or a link to a cast | That a Farcaster account using the wallet follows you, or liked or recasted the cast |
| Real people only | A tick | A Coinbase Verified Account (Coinbase's attestation on Base), or a Farcaster account in good standing (Neynar score 0.5 or more) |
“Use your app” follows your contract's events from the campaign's start, so only actions after launch count. Before launch, “Try it with a wallet” looks at the last few hours instead: do the action once and see it count. Very busy contracts that aren't your own (a popular token, say) can't be followed. Likes, follows and balances can be undone after a user gets their card; an on-chain action can't, which makes it the stronger task.
Your own endpoints
For anything else (an account on your site, a quest): one HTTPS endpoint per task, on your server. Sprout calls it like this:
GET https://api.yourproject.com/sprout/first-swap?address=0xAbC…123 Accept: application/json User-Agent: SproutCampaigns/1 (+https://sprout.gift/campaigns/developers) X-Sprout-Campaign: 0xYourCampaign
| Rule | |
|---|---|
address | The user's signed-in wallet, checksummed. Compare it case-insensitively. |
X-Sprout-Campaign | Your campaign's address, if one endpoint serves several campaigns. |
| Answer | HTTP 200 with JSON {"completed": true} when the task is done, {"completed": false} when it isn't. result and 1 work too. |
| Anything else | Any other status, a redirect, a timeout or a body that isn't that JSON counts as not done. A broken endpoint never gives out cards. |
| Limits | https on port 443, a public domain name (no IPs), 5 seconds, 16 KB. Redirects aren't followed. |
| When | When the user opens the page or taps Check Again, and once more right before their card is made. A yes is remembered for 5 minutes. |
// One endpoint per task. Sprout calls it with the user's wallet:
// GET https://api.yourproject.com/sprout/first-swap?address=0xAbC…
// and counts the task done only for {"completed": true}.
app.get("/sprout/first-swap", async (req, res) => {
const address = String(req.query.address ?? "").toLowerCase();
if (!/^0x[0-9a-f]{40}$/.test(address)) return res.json({ completed: false });
const done = await db.swaps.exists({ wallet: address }); // your own rule
res.json({ completed: done });
});Every task can be tried from the dashboard with any wallet (Try it with a wallet), exactly as Sprout will check it for users. The endpoints answer about public wallets only, so they need no secret; keep them read-only.
Widgets
Add the script once, anywhere on your page:
<script src="https://sprout.gift/embed.js" defer></script>
Then one line for each widget you want. Your campaign's Share tab has them with its address filled in.
| Widget | Line |
|---|---|
| Banner | <div data-sprout-campaign="0xYourCampaign" data-variant="banner"></div> |
| Bar | <div data-sprout-campaign="0xYourCampaign" data-variant="bar"></div> |
| Corner popup | <!-- Slides in at a corner after a few seconds; once closed, it stays away for a week. -->
<div data-sprout-campaign="0xYourCampaign" data-variant="popup"
data-position="bottom-right" data-delay="3"></div> |
| Floating button | <div data-sprout-campaign="0xYourCampaign" data-variant="button" data-position="bottom-right"></div> |
data-position is bottom-right (default) or bottom-left; data-delay is the popup's wait in seconds. From code: Sprout.banner(element, { vault, variant }), Sprout.popup({ vault, position, delay }), Sprout.button({ vault, position }), and Sprout.open(vault) to open the campaign's page from your own button.
Widgets are frames served by sprout.gift in your campaign's current design, so a design change shows up without touching your site. Floating widgets only appear while the campaign is live. A click opens your campaign's page in a new tab; nothing about the user passes through your page.
Public API
GET https://sprout.gift/api/campaigns/{vault}: a campaign's public details, e.g. to hide your own button once active is false. Call it from your server (no CORS); it is cached for 15 seconds. Nothing private: no users, cards, keys or endpoints. Amounts are USDC units (6 decimals).
{
"data": {
"vault": "0x…",
"chainId": 8453,
"name": "Acme Welcome Rewards",
"cardDesign": "based",
"tasks": [
{ "id": "t1", "title": "Make your first swap", "description": "", "actionUrl": "https://app.acme.com/swap" }
],
"mix": [{ "symbol": "AAPL", "weightBps": 5000 }, { "symbol": "NVDA", "weightBps": 5000 }],
"endsAt": "2026-12-31T00:00:00.000Z",
"unlockAt": null,
"claimWindowDays": 90,
"budgetUnits": "100000000",
"cardsLeft": 50,
"active": true
}
}Cards given out, claimed and returned are on your campaign's Overview tab, and on-chain as CardIssued events of your campaign contract.
Security and limits
- Your tasks decide who gets a card. One card per wallet is enforced on-chain; “Real people only” keeps most bots out, and anything beyond that is up to your task rules.
- Each user signs with their own wallet where their card goes, and the contract only accepts that destination: nobody at Sprout can redirect a card. A card also needs two Sprout keys to agree (the task verifier and the issuer).
- The contract caps cards per hour (you choose, and can change it) and per card. Even in the worst case, nothing beyond your campaign's budget is at risk, it can only drain slowly, and you can withdraw it at any time.
- You sign your tasks with the campaign's owner wallet when you create the campaign and whenever you change them (free, no transaction). Sprout's verifier, a separate service, only uses tasks you signed, so nobody else can change what users must do. Changes apply from the next check; cards already given out stay with their users.
- Sprout's fee is 5% of the budget, and at least $0.03 per card (it covers the gas we pay to give each card and send it when it's claimed), paid once on top of the budget when you create the campaign; adding budget later costs 5% of what you add. Cards carry no fee: the whole budget goes to your users. Cards nobody claims within the claim window go back to your treasury.
- Tokenized stocks on Base aren't available in the US, the UK and some other countries, and their issuer can pause or block them. The contracts are not audited.
