=> {
console.log('About to make a request', url, init)
const response = await fetch(url, init)
console.log('Got response', response)
return response
},
})
```
Note that if given a `DEBUG=true` environment variable, this library will log all requests and responses automatically.
This is intended for debugging purposes only and may change in the future without notice.
### Configuring an HTTP(S) Agent (e.g., for proxies)
By default, this library uses a stable agent for all http/https requests to reuse TCP connections, eliminating many TCP & TLS handshakes and shaving around 100ms off most requests.
If you would like to disable or customize this behavior, for example to use the API behind a proxy, you can pass an `httpAgent` which is used for all requests (be they http or https), for example:
```ts
import http from 'http'
import { HttpsProxyAgent } from 'https-proxy-agent'
// Configure the default for all requests:
const client = new SnagSolutions({
httpAgent: new HttpsProxyAgent(process.env.PROXY_URL),
})
// Override per-request:
await client.assets.create(
{ fileName: 'REPLACE_ME' },
{
httpAgent: new http.Agent({ keepAlive: false }),
}
)
```
## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
1. Changes that only affect static types, without breaking runtime behavior.
2. Changes to library internals which are technically public but not intended or documented for external use. (Please open a GitHub issue to let us know if you are relying on such internals)\_.
3. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
We are keen for your feedback; please open an [issue](https://www.github.com/Snag-Solutions/node-sdk/issues) with questions, bugs, or suggestions.
## Requirements
TypeScript >= 4.5 is supported.
The following runtimes are supported:
- Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more)
- Node.js 18 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions.
- Deno v1.28.0 or higher.
- Bun 1.0 or later.
- Cloudflare Workers.
- Vercel Edge Runtime.
- Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time).
- Nitro v2.6 or greater.
Note that React Native is not supported at this time.
If you are interested in other runtime environments, please open or upvote an issue on GitHub.
## Related pages
REST API authentication, endpoints, and base URL setup.
Understand request limits and hostname-based throttling.
Set up your API key and make your first Snag API call.
Connect AI tools to Snag Docs for context-aware coding help.
---
## Media Guidelines
*Recommended media formats and dimensions used across the Snag admin tool.*
### Favicon
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Size: 32 × 32 px
### Social preview
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Recommended size: 1200 × 630 px
### Page background
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Recommended aspect ratio: 2:1
- Recommended size: at least 2000 × 1000 px
- Note: Large files may increase page load time.
### Header logo
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Any aspect ratio
### Navigation links
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Recommended size: 100 × 100 px
### Rule media
_Add media in rule form_
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Recommended size: 200 × 200 px
### Banner image
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 5:1
- Recommended size: 2000 × 400 px
- This banner blends into the page background, keep important content toward the top.
- On mobile, the banner is taller and the sides may be cropped (2.5:1 ratio).
### Currency image
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Recommended size: 200 × 200 px
### Badge image
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Recommended size: 200 × 200 px
### Page background
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Recommended aspect ratio: 2:1
- Recommended size: at least 2000 × 1000 px
- Note: Large files may increase page load time.
### Strip section
**Dekstop**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 5:2
- Recommended size: 2000 × 800 px
**Mobile**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 3:4
- Recommended size: 600 x 800 px
### Image section
**Desktop**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Any aspect ratio
- Recommended size: at least 2000 px width
**Mobile**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Any aspect ratio
- Recommended size: at least 600 px width
### Carousel section (large)
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 5:2
- Recommended size: 2000 × 800 px
### Featured section
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Recommended size: at least 800 x 800 px
### Featured app section
**Tile media**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 3:2
- Recommended size: at least 600 x 400 px
**Page media**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 5:1
- Recommended size: at least 2000 x 400 px
### Apps grid section
**Tile media**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 3:2
- Recommended size: at least 600 x 400 px
**Page media**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 5:1
- Recommended size: at least 2000 x 400 px
### Claiming box
**Successful claim media**
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 2:1
- Recommended size: at least 800 px in width
### Token info
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Recommended size: at least 200 x 200 px
### Contract image
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Recommended size: 200 × 200 px
### Asset media
- Media type: JPG, JPEG, PNG, GIF, WEBP, AVIF
- Aspect ratio: 1:1
- Recommended size: 1000 × 1000 px
## Related pages
Upload and manage image assets for loyalty rules, rewards, and program branding.
Customize your platform's visual style including themes, colors, and logos.
---
## Billing cycle
*Snag uses recurring billing cycles to manage subscription charges and usage charges. Your billing cycle depends on your selected plan and payment term.*
## Plans Billing Cycle
Your subscription renews on a recurring schedule based on the plan you selected:
### Monthly Plans
You are billed every month on the same calendar date as your initial purchase.
### Annual Plans
You are billed once per year on the same calendar date as your initial purchase. Annual plans include discounted pricing.
---
## Initial Billing Term
When upgrading to a Plus or Pro plan for the first time, the first 3 months are charged upfront. After the initial term ends, billing continues on a monthly schedule (unless annual billing was selected).
---
## Usage Billing Cycle
Usage operates on a monthly cycle for all plans, including annual ones.
- Usage resets monthly.
- Any usage beyond your included monthly records is billed monthly.
- Pro-rated usage is not carried over between months.
You can view both subscription and usage billing dates in the Manage Plan section.
---
## Next Billing Dates in the Dashboard
In your Snag admin under Manage Plan, you’ll see two upcoming charges:
1. **Next plan charge** - The date your subscription will renew (monthly or annually).
2. **Next usage charge** - The date your usage total will be billed for the current cycle.
Both dates update automatically based on your plan and billing history.
---
## Change or cancel your plan
*You can upgrade, downgrade, switch billing frequency, or cancel your subscription at any time.*
## Upgrading your plan
First-time upgrade (from Free to paid plan)
- The upgrade takes effect immediately.
- The initial billing term applies (first 3 months charged upfront).
- Future renewals follow the selected billing schedule (monthly or annual).
- During the upgrade process, you will be asked to confirm your billing email address. This email is used for sending invoices and payment receipts.
---
## Upgrading between paid plans
When switching from one paid plan to another paid plan, upgrades cannot be completed self-serve. You will need to contact us at [partners@snagsolutions.io](mailto:partners@snagsolutions.io), and we’ll process your upgrade within 1–2 business days. This requirement applies to all plan-to-plan upgrades after your initial paid subscription.
---
## Downgrading Your Plan
When downgrading:
- Your current plan remains active until the end of the current billing cycle.
- The downgrade takes effect on your renewal date.
- You will see a list of features that will be removed or limited on the new plan.
- Your new monthly record limit will also be shown.
A banner will appear on the Manage Plan page with the exact date your new plan will begin. You can cancel the downgrade at any time before that date.
---
## Switching Between Monthly and Annual Billing
You can switch your plan’s billing frequency from the plan selection screen.
### Switching to Annual Billing
- No charge occurs immediately.
- The switch takes effect at the start of your next billing cycle.
### Switching to Monthly Billing
- The change takes effect on the next renewal date.
- There is no prorated refund for partially used annual terms.
---
## Canceling Your Plan
You can cancel your plan from the Manage Plan tab.
- Your current plan remains active until the end of the billing cycle.
- No new subscription charges will be created.
- Usage charges generated before the cancellation date will still be billed.
After cancellation, your project automatically moves to the Free plan at the next renewal date.
---
## Billing FAQ
*Answers to common Snag billing questions: usage tracking, record limits, payment methods, plan cancellation, invoices, and overage billing explained.*
Usage is based on records, which are created when Snag processes a state-changing action (transactions, multipliers granted, badges granted, mints).
Free plan: additional records are not processed.
Basic, Plus, Pro, Enterprise: extra records are billed at your plan’s per-record rate during the same billing cycle.
Yes. Snag sends email alerts when you reach 50%, 90% and 100% of your included monthly records.
No, usage beyond your included records is automatically billed during the same billing cycle. You can track your usage at any time in your Snag Admin under Manage Plan or in your Looker dashboard.
You can monitor usage in:
- Snag Admin under Manage Plan tab (real-time totals)
- Looker dashboard (breakdown by record type)
Usage resets at the start of each billing cycle.
Yes. You can cancel at any time. Your plan remains active until the end of the billing cycle, then moves to the Free plan.
Stripe supports:
- Visa
- Mastercard
- American Express
- Discover
- Apple Pay
- Google Pay
Availability may vary by region.
Go to Manage Plan → View Invoices to open the Stripe billing portal, where you can download all past invoices and receipts.
No. The Claim Page add-on is billed separately.
Contact [partners@snagsolutions.io](mailto:partners@snagsolutions.io) to enable or manage this add-on.
Stripe will retry the payment and notify you by email. You can update your payment method at any time through the Stripe portal.
Not yet. Crypto payments are not available today, but we're actively working on adding support in the future.
## Related pages
Compare plan tiers, included records, and feature access.
Monitor your record usage and billing cycle details.
Reach out for billing support or enterprise pricing.
---
## Payment methods & billing info
*All subscription and usage payments, payment methods and billing details are handled securely through Stripe.*
## Payment Methods
You can manage your payment methods through Stripe. In the Manage Plan tab, select Manage Payment Methods to open the Stripe customer portal.

In the Stripe portal you can:
- Add or remove payment methods
- Set your default payment method
- Update your billing address and contact details
Any changes you make in Stripe sync automatically with Snag.
---
## Accepted Payment Methods
Stripe supports the following payment methods (availability may vary by region):
- Credit and debit cards (Visa, Mastercard, American Express, Discover)
- Apple Pay
- Google Pay
---
## Billing and Invoices
You can view your billing history and download invoices through Stripe. In the Manage Plan tab, select Manage Payment Methods to open the Stripe customer portal.

In the Stripe portal you can:
- View all past subscription and usage invoices
- See upcoming or pending charges
- Download invoice PDFs and receipts
- Review detailed line items for each billing cycle
Invoices are generated automatically at the end of each billing cycle or when an upgrade charge is created.
An email is automatically sent to your billing email address each time a new invoice is generated.
---
## Plans & features
*Compare Snag pricing plans and features including Free, Basic, Plus, and Pro & Enterprise tiers.*
Snag offers flexible plans designed to scale with your platform - from early experiments to full ecosystem launches. Every plan includes access to Snag’s core onchain loyalty infrastructure, with higher tiers unlocking advanced automation, analytics and customization.
Below is a breakdown of each plan and what it includes.

## Free Plan
Before choosing a plan, you can start with the Free tier to get a feel for Snag’s product. It’s designed for teams that need to prototype and test a lightweight loyalty program.
The Free plan includes 5,000 monthly records. Once this limit is reached, no additional records are processed for the remainder of the month.
---
## Basic Plan
Designed for teams that need the core functionality required to launch and operate a live loyalty program.
### **Pricing**
- \$382/month (yearly plan) or \$449/month (monthly plan)
- 25,000 monthly records
- \$0.008 cost/additional record
### **Features**
Everything in Free, plus:
- Rules Engine access
- Loyalty Template
- Loyalty Leaderboard
- Badges
- Multipliers
- Page Builder access
- Claim Page available as an add-on
### **Onboarding & Support**
- Email support (≤48h SLA)
---
## Plus Plan
Designed for teams that need expanded functionality, including custom design options, enhanced data capabilities, and multichain support.
### **Pricing**
- \$1,487/month (yearly plan) or \$1,749/month (monthly plan)
- 250,000 monthly records
- \$0.004 cost/additional record
### **Features**
Everything in Basic, plus:
- Advanced Rules Engine access
- Loyalty Template or API-based build
- Custom loyalty page design + build
- X impression tracking and all TikTok, Instagram, Reddit, YouTube, and Steam rules
- Quest chaining
- Custom Looker dashboard w/ daily analytics
- Multi-currency support
- Dedicated support
- 25% off Claim Page add-on
### **Onboarding**
- Enhanced onboarding with Snag team
- Dedicated TG group
- Engineering activation call for API partners (1x)
### **Support**
Everything in Basic, plus:
- Dedicated Telegram group
- SLA: \<24h on business days
---
## Pro Plan
Designed for teams that need advanced features for high-volume programs, including deeper onchain logic, automation, and growth tooling.
### **Pricing**
- Custom pricing
- Custom record limits
### **Features**
Everything in Basic, plus, and:
- Anti-Sybil dashboard
- Custom anti-sybil support
- Hourly rule cadence
- Developer activity rules (GitHub)
- Contract event rule (Reward any onchain event)
- Quality UGC + X impression tracking
- DeFi logic (LP Logic)
- Minting & Rewards Shop
- App hub builder access
- Snag CRM (Onchain user data)
- Snag Stratus function access (Onchain cloud)
- Advanced Stratus relayer access (Onchain points and mint badges as NFTs)
### **Onboarding**
- Guided onboarding with Snag team
- Dedicated TG group w/ engineering access
### **Support**
- Dedicated TG group
- Priority bug fixes
- SLA: \<8h on business days
---
## Enterprise
Designed for teams that need full customization, advanced integrations, and premium support for large-scale ecosystems.
### **Pricing**
- Custom
- Custom record limits
### **Features**
Everything in Pro, plus:
- Advanced Stratus relayer access (onchain points + mint badges as NFTs)
- App Hub builder access
- Custom anti-sybil support
### **Onboarding**
- Guided onboarding w/ Snag team
- Dedicated TG group w/ Eng access
### **Support**
- Dedicated TG group w/ engineering access
- Priority bug fixes + roadmap input
- Founder advisory support
- SLA: \<4h on business days
---
## Related pages
Learn about Snag's loyalty program features included in every plan.
Explore Stratus onchain cloud services available on Pro and Enterprise
plans.
Get help from the Snag team.
Track your monthly record consumption and API usage.
---
## Usage
*Usage refers to the number of records your project generates during a billing cycle. Records represent state-changing actions performed by users or the system. All plans (except Free) include a monthly record limit, and any records beyond that limit are billed at the plan's per-record rate.*

## What Counts as a Record
A record is created whenever Snag processes an action that results in an onchain or platform-level state update. The following actions generate billable records:
### 1. Transactions
A record is created whenever a user completes a rule that results in an update to their loyalty state (e.g., points earned, quests completed, actions validated).
### 2. Multipliers & Badges
A record is created when a multiplier or a badge is granted to a user, such as when Snag checks a condition (e.g., NFT ownership) and applies the multiplier. The check itself does not count as a record, the grant does.
### 3. Mints
A record is created whenever a user mints an asset through Snag.
---
## What Does Not Count as a Record
The following do not generate usage:
- Validation checks that result in no change
- Failed rule attempts
- Page views or frontend interactions
- API requests that do not create state changes
---
## Record Rates
All record types are billed the same way. Each plan has:
- A monthly included record limit.
- A per-record rate for usage beyond that limit.
---
## Usage Reset
Usage resets on the plan’s billing cycle date. All included records and usage counters restart at that time.
---
## Exceeding Included Records
When your project exceeds its included monthly record allotment:
- Free plan:
- No additional records are processed for the remainder of the cycle.
- Basic, Plus, Pro, Enterprise:
- Additional records are automatically billed at the plan’s per-record rate.
- Billing occurs during the same cycle in which the usage is generated.
Snag automatically sends email alerts when your project reaches 50% of its included records and again at 90% and 100%.
---
## Where to See Your Usage
### Snag Admin
In the Snag Admin dashboard under the Manage Plan tab, you can see your total records for the current billing cycle. This number represents the exact count of billable records, and it updates continuously as new records are generated.
### Looker Dashboard
Your project also includes a Google Looker dashboard with a detailed breakdown of usage. This dashboard shows record counts by type (transactions, multipliers, badges, mints) and provides additional analytics to help you understand how your usage is distributed.
---
## Badges
*Create achievement badges to gamify your Web3 loyalty program. Set conditions based on rules, points, or other badges, and track user progress via the API.*
## Overview
The Badges feature in Snag Admin allows you to create and manage badges that users can earn by fulfilling specific condtions. These badges are displated on user profiles along with their progress towward unlocking them.

Users can earn badges by:
- Completing specific rules or set of rules
- Achieving a certain amount of loyalty points
- Unlocking another badge or set of badges
- Meeting any other custom conditions
Each badge displays a list of conditions and tracks user progress toward achieving them. The API provides real-time updates on user completion status, allowing users to see how close they are to earning a badge.

## Creating and Managing Badges
This recipe explains how to manage badges using the Snag API, including creating, updating and deleting badges but also assigning badges to users and revoking them.
### Fetching Badges
The [`GET /api/loyalty/badges`](https://docs.snagsolutions.io/api-reference/badge/get-badges-all-or-by-id) endpoint allows for fetching of loyalty badges created on your Snag account. This endpoint provides various filters to allow pagination. Badges are sorted by their creation date in descending order.
#### Example Request
Request
```
{
"organizationId": "your-organization-id",
"websiteId": "your-website-id",
"limit": 10
}
```
- Note: for paginated requests be sure to add the `startingAfter` property to subsequent requests.
#### Example Response
```
{
"data": [
{
"id": "b1b1b1b1-b1b1-b1b1-b1b1-b1b1b1b1b1b1",
"name": "Example Badge",
"description": "This is an example badge",
"imageUrl": "https://example.com/image.png",
"loyaltyConditions": [
{
"id": "c1c1c1c1-c1c1-c1c1-c1c1-c1c1c1c1c1c1",
"type": "rule",
"description": "Rule completion: Link Click",
"amount": null,
"repeatCount": null,
"requiredCount": null,
"loyaltyRuleGroupId": null,
"association": [
{
"loyaltyRule": {
"id": "r1r1r1r1-r1r1-r1r1-r1r1-r1r1r1r1r1r1",
"name": "Link Click"
},
"loyaltyBadge": null,
"loyaltyRuleGroup": null,
"loyaltyCurrency": null,
"loyaltyLeaderboardView": null
}
],
"loyaltyCurrencyId": null,
"csvUrl": null
}
]
}
],
"hasNextPage": "false"
}
```
### Creating a Badge
The [`POST /api/loyalty/badges`](https://docs.snagsolutions.io/api-reference/badge/create-new-badge) endpoint allows for creating a new badge on your Snag account. This endpoint requires the following parameters:
- `organizationId`: Your organization ID
- `websiteId`: Your website ID
- `name`: The name of the badge
- `description`: A description of the badge
- `imageUrl`: The URL of the badge image
- `rules`: An array of conditions that must be met to earn the badge
### Updating a Badge
The [`POST /api/loyalty/badges/{id}`](https://docs.snagsolutions.io/api-reference/badge/update-badge-by-id) endpoint allows for updating an existing badge on your Snag account.
The `id` parameter is required to identify the badge to update. We only allow updating the `name`, `description`, and `imageUrl` of the badge. The reason for this is to prevent any changes that could affect the badge's conditions and user progress.
### Deleting a Badge
The [`DELETE /api/loyalty/badges/{id}`](https://docs.snagsolutions.io/api-reference/badge/delete-badge-by-id) endpoint allows for deleting an existing badge on your Snag account.
The `id` parameter is required to identify the badge to delete.
### Rewarding User with a Badge
The [`POST /api/loyalty/badges/{id}/reward`](https://docs.snagsolutions.io/api-reference/badge/reward-badge-for-account) endpoint allows for rewarding a user with a badge on your Snag account.
The `id` parameter is required to identify the badge to reward, and either `userId` or `walletAddress` is required to identify the user to reward.
### Revoking a Badge from a User
The [`POST /api/loyalty/badges/{id}/revoke`](https://docs.snagsolutions.io/api-reference/badge/revoke-badge-from-account) endpoint allows to revoke a badge from a user on your Snag account.
The `id` parameter is required to identify the badge to revoke, and either `userId` or `walletAddress` is required to identify the user to revoke.
## Assigning Badges via CSV
For manual badge assignment, upload a CSV file containing wallet addresses of users who should receive a specific badge. The uploaded file should follow this format:
```
walletAddress
0x1...abc
0x2...def
```
After upload, the system will process the file and assign the badge to the listed users.
## Important Notes
This system provides a flexible way to reward user engagement and encourage participation through achievements. The Badge feature is only available to websites with active loyalty programs.
Badges are **not retroactive**. They only track user progress and grant rewards starting **up to 24 hours before their creation**.
If you want to reward users who had already met the badge conditions before the badge was created, you must **manually assign the badge** using the [Reward Badge API](https://docs.snagsolutions.io/api-reference/badge/reward-badge-for-account) or **upload a CSV file** with their wallet addresses.
## Related pages
Set up the loyalty program that powers badge conditions.
Display user rankings alongside badge achievements.
Let users redeem points for NFTs, auctions, and more.
Chain rules together to create multi-step badge journeys.
import FeedbackForm from '/snippets/FeedbackForm.mdx'
---
## Claim Page
*The following is a quickstart guide to creating fully branded claim pages.*
## Overview
[Our asset token claims tab in the admin tool](https://admin.snagsolutions.io/) consists of three separate flows that comprise the entire claim page creation process:
1. Allocation set up
2. Claim page config
3. User facing page config
## Prerequisites
- **Claim Page add-on enabled:** Contact sales if you don't have it
- **ERC20 token to be claimed deployed:** You should already have this and add it in the _Contracts_ tab in the [admin tool](https://admin.snagsolutions.io/)
- **A CSV with the allocation filled in**: Download the template from [here](https://assets.snagsolutions.io/public/templates/snag-token-claims-template.csv) and fill it out. The amount needs to be with the correct decimals.
## Create and configure a token claim
1. Click the **Token claims** tab in the [admin tool](https://admin.snagsolutions.io/) and make sure all the prerequisites above are completed
2. Fill out the _name_ and _select the token_ configured beforehand
3. Upload the pre-filled allocations CSV. Contact us if your CSV has more than 100k rows.
4. When the processing is finished, verify the data and deploy the contract
5. Set the _claim period_, _geo-blocking_ if needed and publish the claim
## Build the Claim Page
### 1. Create the draft
1. Navigate to **Landing Pages** and click **Create page**.
2. In the layout picker choose **Claim Page**
3. Fill out the details and select the _token claim_ created at the previous step
4. A draft page containing the recommended sections appears in the editor.
### 2. Configure page sections
The Claim Page uses purpose-built sections defined in `LandingPage/sections.tsx`:
- **Claiming Box** _(mandatory)_. It includes the core flow. Checking allocation and the actual claiming.
- **Claim Timer** – Displays countdowns before `startTime` and shows status when `endTime` is reached.
- **Token Info** – Long-form description, media, token symbol, and allocation stats.
You can mix in any other section types (strip, hero, FAQ, featured, etc.) to tell the story around your drop.
### 3. Attach the token claim and publish
Need help? [Contact the Snag team](https://www.snagsolutions.io/schedule-a-call) and we’ll walk through your setup.
---
## Page Builder
*Build custom landing pages and app hubs with Snag's no-code page builder. Use carousels, strips, featured sections, and app grids to showcase your ecosystem.*
## Overview
The page builder has multiple different section types that can be customized individually. Depending on the type of page you are trying to build, different section types will be available.

### Section types
1. Carousel
2. Strip
3. Featured
4. All items
5. List
6. Image
7. Text
8. Code
9. Featured apps
10. Apps grid
## Carousel
You have the ability to choose from 3 sizes of carousel:
- Small (small-sized cards)
- Medium (medium-sized cards)
- Large (full size)
You can align the text (title and description) left or to the center.
Then choose between showing multiple collections or items within a specific collection.

If you selected the large carousel you have to individually define the title, description and background for each collection. This gives you more customization options to better highlight individual collections.

Examples:



## Strip
The strip is a banner-type section made out of an image, title, description and button. You can choose what combination of the four you wish to have. You can align the content to the bottom middle or left side, this also impacts the overlay type.
- Bottom - Black overlay gradient from the bottom
- Middle - Full 50% black overlay
- Left - Black overlay gradient from the left side

If you wish, you can also toggle on the custom mobile design under the advanced options to upload a different image that is better suited for mobile devices. This images doesn't have an overlay since the title, description and button are positioned outside of the image. You have the option to position the text either above or below the image.

Examples:



Custom mobile image:

## Featured
The featured section allows you to highlight a specific collection or item. You can align the main image to the left or right and add a custom title, and description.

Examples:


## All items
The all-items section is a grid of 8 items from selected collections. You can also choose to show items from all collections.

Examples:

## List
List all your collections and choose how to sort them. You can sort by:
- Latest sales
- Recently created
- Floor price
- 24h volume
- Unique holders

Examples:

## Image
You can add an image with a custom aspect ratio. The image will always be stretched to the full width of the screen and the height will adapt based on the aspect ratio of the image.
If you wish, you can also upload a different image that is better suited for mobile devices
The image can also have a link attached to it which makes the entire image clickable.

Examples:

## Text
The text section allows you to add rich text with headlines, bullet points and other customization options.

Examples:

## Code
The code section gives you endless possibilities to insert custom code on your page.
Custom code is not validated. Incorrect code may cause issues.

Examples:

## Featured app
The featured app highlights a single app of your choice. You can add a call-to-action button that links to a specific loyalty section or an external page.
This section can also be used as a hero element for your Apps Hub. Instead of featuring a specific app, you can introduce your ecosystem or highlight any key content.
**Media:**
- Tile media is shown on the Apps Hub.
- Page media serves as the banner on the linked loyalty page.
**Layout:**
- Container Style - Displays the featured app in a large tile, keeping it visually aligned with the Apps Grid while making it still prominent.
- Full-width Hero - Expands the featured app into a large banner, ideal for a general hero section introducing your loyalty program.

Examples:


## Apps grid
The apps grid allows you to showcase multiple apps in a structured, visually engaging way. Use it to create a hub for different app experiences within your loyalty ecosystem. Each app can have its own media, text, button, and links.
Apps can be reordered within the grid, and you can choose whether to display or hide quest and points information.
**Media:**
- Tile media is shown on the apps hub.
- Page media serves as the banner on the linked loyalty page.

Examples:

## Related pages
Create NFT listings and auctions for your rewards marketplace.
Set theme colors, fonts, and visual style for your platform.
Set up the loyalty program displayed on your pages.
Launch under your own branded domain.
---
## Rewards Shop
*Create NFT contracts, mint assets, and list them for purchase, auction, or sweepstakes in your Snag rewards shop. Supports ERC-721 and ERC-1155.*
## Overview
[Our asset creation tab in the admin tool](https://admin.snagsolutions.io/) consists of three separate flows that comprise the entire contract set and minting & auction page creation process:
1. Contract set up
2. Metadata configuration / asset creation
3. Listing or auction creation
## Step by step
### Contract set up
1. Access the Asset Creation tab in the menu on the left and hit the create new contract button in the top right.

1. Hit the 'Create New Contract' button on the next modal to confirm you'll be the owner on the contract. On the next tab you'll:
1. Input the collection name, symbol, and image along with optional description and external link.
2. Choose the token type ([ERC-721](https://eips.ethereum.org/EIPS/eip-721) or [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155)). Only one token type can exist on the NFT smart contract.
3. Choose chain
4. _Optional:_ enable meta transaction if you plan to enable gasless transactions in the future via Stratus relayers
.
5. Choose the wallet address for mint and royalty payouts


#### Token gating
Token gating lets you restrict who can claim or buy from this contract by checking wallet ownership or badges at checkout. Configure it during contract set up so every listing tied to the contract uses the same gating rules.

When token gating is enabled, you can choose which assets grant access (NFTs, tokens, specific collections, or badges) and set the minimum balance required. You can use this to:
- limit claims to holders of a partner collection
- require a minimum token balance before minting
- unlock drops for badge holders or onchain members
Configuration options:
- Free-claim versions are supported by the checkbox under each option. Users can claim for free if they own the required tokens or badges.
- Limit the amount of mints to the number of tokens or badges the user owns.
- Require the user to own all the required tokens or badges. In this case, they will be able to mint only one item.
### Add Assets to Contract
1. The next step is to configure metadata to the contract as individual assets. We support individual asset creation to start, with .CSV upload and other multi-asset upload capabilities coming soon.
1. Add asset name, description, info about the artist, media, and optional trait info
2. Optional fields
1. _Add quantity limit_: maximum amount mintable/wallet
2. _Add minimum_: minimum amount to mint/transaction
3. _Burn to redeem_: if you want users to burn the NFT to redeem it. You can add a field they need to fill in, show a blank text or collect shipping details(needs API integration).
3. Click 'Finish' when you're done or 'Duplicate' / 'New Asset' to add more


### Listing
1. You can now hit 'List Assets' on the contract modal in the assets creation tab when ready to either list, create an auction or create a sweepstakes.


#### List assets for purchase

1. Choose the currency you'd like to list in (native currency or loyalty points if enabled)
2. Choose the price and the asset you'd like to list
1. You can select "free claim", if you want users to claim for free and pay only gas fees.
3. Choose listing duration start and (optional) end time
Allowlist (optional)
- You can add a allowlist to the listing to restrict who can claim the asset.
- You can either upload a CSV file with the wallet addresses or add them via the [API](https://docs.snagsolutions.io/api-reference/minting/upsert-minting-allowlist-entries)
- Allowlisted wallets can always claim the asset for free, only paying gas fees.
#### List assets for auction
1. Add an auction name, description, duration, and currency
2. **Note:** Auctions can be tied to one or more than one onchain asset, so please add sufficient description to explain to the user what they're bidding on
3. Choose your auction mechanics:
1. Minimum Bid: The minimum amount any bidder can start the auction with
2. Minimum Bid Increment: The minimum increase on any bid from the previous highest bid
3. Reserve Price: The minimum bid that is valid to win the auction, this is the most important of these three inputs
4. **Multi-Winner Auctions Only:** By default, multi-winner auctions charge each bidder the amount they bid, if 'Reverse Dutch Auction' is toggled we'll instead refund all winners the difference of their bid and the lowest price winning bid
5. **Blind Auctions Only:** If toggled on, users can only see their own bids. All other bids are hidden
6. Choose the prize!


#### List assets for sweepstakes
1. Add the sweepstakes name, description, duration, and entry token purchase currency
2. Choose the entry ticket and specify the total number of available tickets.
3. Choose the prize(s).


1. You're live! View these listings as part of your marketplace!
### Hide a collection from template pages
To prevent a collection from appearing on template pages (for example, the Loyalty page), you can hide it from the Rewards Shop.
1. Click the 'Edit Rewards page' button in the top right of the Rewards Shop.

2. In the Edit Rewards page settings, use the visibility control to hide or unhide the collection so it does not show on template pages (e.g., Loyalty).

## API-based integration
Use the minting API to power a custom checkout or backend-driven flow. This mirrors the same flow used by the Rewards Shop frontend.
API requests use the `X-API-KEY` header. Use the `assetId` and `contractId`
(UUID or onchain address) from the Rewards Shop.
Call `POST /api/minting/contracts/mint` with the `assetId`, `contractId`, `walletAddress`, and `quantity`
```json
{
"assetId": "asset_uuid",
"contractId": "contract_uuid_or_address",
"walletAddress": "0xabc...",
"quantity": 1,
}
```
The response returns `mintingContractAssetMintStatusId`, which you will poll
for status and signature.
Call `GET /api/minting/status/{id}` every 1-2 seconds until a `signature` is
returned or the status moves to `failed`.
- If `signature` is returned, you need to submit the onchain transaction
yourself.
- If `relayerTxId` is present, this is a gasless transaction and you can ignore the next step.
Use the returned signature to execute the mint on the correct chain by
calling the mint function on the contract.
- **EVM**: call [mintWithSignature](https://portal.thirdweb.com/references/typescript/v5/erc721/mintWithSignature) for ERC-721/1155. If `currency` is an
ERC-20 and `price > 0`, approve `price * quantity` before minting.
- **Solana**: execute the base64 transaction using the provided blockhash
and last valid block height.
This is how our internal implementation looks like. For contract interaction we use:
- For EVM chains, [thirdweb](https://portal.thirdweb.com/references/typescript/v5/erc721/mintWithSignature).
- For Solana, [metaplex](https://docs.metaplex.com/sdk-js/minting-tokens).
```ts
import {
sendTransaction,
getContract,
waitForReceipt
} from "thirdweb";
const buyToken = async (
props?: BuyTokenProps
): Promise => {
const res = await axios.post(`/api/minting/contracts/mint`, {
quantity,
assetId,
shippingId: props?.shippingId,
shippingOrderType: props?.shippingOrderType,
emailAddress: props?.emailAddress,
contractId: contractAddress,
agreedToSendW9: props?.agreedToSendW9,
customInputValue: props?.customInputValue,
})
if (res.status !== 202) {
return undefined
}
const mintStatusId = res.data.mintingContractAssetMintStatusId
while (true) {
const { data } = await axios.get(`/api/minting/status/${mintStatusId}`)
if (data.relayerTxId && data.status === MintStatus.minted) {
return data.txHash
}
if (data.signature) {
const signature = JSON.parse(data.signature)
if (network === NetworkType.solana) {
const umi = createUmi()
const { signature: txSig } = await executeBase64Txn(
umi,
signature.signature,
signature.latest
)
await updateMintStatus(mintStatusId, {
status: MintStatus.minted,
txHash: txSig,
})
return txSig
}
const contract = getContract({
address: contractAddress,
client: thirdwebClient,
chain: defineThirdwebChain(chainId),
})
const fnToUse =
tokenType === 'erc721'
? mintWithSignatureErc721
: mintWithSignatureErc1155
const preparedTx = fnToUse({
contract,
signature: signature.signature,
payload: signature.payload,
})
const sentTx = await sendTransaction({
account,
transaction: preparedTx,
})
await waitForReceipt({
client: thirdwebClient,
chain: defineThirdwebChain(chainId),
transactionHash: sentTx.transactionHash,
})
await axios.post(`/api/minting/status/${mintStatusId}`, {
status: 'minted',
txHash: sentTx.transactionHash,
})
return sentTx.transactionHash
}
}
```
After the transaction confirms, you can call `POST /api/minting/status/{id}` with
`status: minted` and the `txHash`. This will be done automatically if you don't call it, but can improve the UX.
## Troubleshooting
### NFTs not showing on your frontend (e.g., `https://example.com/loyalty`)
If some NFTs created in the admin are not visible on your public frontend, walk through the checks below.
These steps apply when your site consumes listings/collections configured in
the admin. If your frontend uses a custom integration, ensure it queries the
same contracts, networks, and listing types you configure here.
Open the Rewards Shop and click 'Edit Rewards page' in the top right, then make sure the collection is set to Visible so it can appear on template pages (e.g., Loyalty).
After saving, the collection becomes eligible to render on template-driven pages.
Creating assets alone will not show them to users. Confirm you used 'List Assets' and that:
- The assets are included in the listing (IDs/ranges set correctly)
- The listing start time is in the past and the end time (if any) has not passed
- The listing status is active
On the listing modal, toggle to 'Unlisted Assets' to find items you may have missed.
Template pages may filter by collection, category, or currency. For example, a Loyalty page typically shows listings priced in loyalty points. If you listed in a different currency (e.g., native token), those items may not appear on that page.
If your page is configured to only show loyalty listings, standard crypto-priced listings won't display there.
Ensure your contract and listings are on the same chain your frontend is reading from. If your site points to a different network (e.g., testnet vs mainnet), listings will not appear.
After creating or updating listings, allow a few minutes for indexing and caching to update, then hard refresh your frontend.
If you use server-side caching or a CDN, purge the cache for the affected pages.
- **Collection not attached to the page**: In 'Edit Rewards page', verify
the targeted collection is included in the page layout/sections. -
**Incorrect ID ranges**: If you used ranges (e.g., 1-10), make sure all
intended IDs fall within those ranges. - **Missing media or required
metadata**: Some templates hide items with incomplete media/fields; confirm
each asset has the required fields and media URLs. - **Frontend filters**:
Check user-facing filters (collection, price type, availability) on the site
aren't excluding your items.
### Solana Mint Setup & Account Initialization
On Solana, accounts must be initialized on-chain before they can be used in transactions. While initialization instructions can be added to the main transaction, this often increases the transaction size beyond Solana's limits.
Account initialization is required in two main scenarios:
1. **Asset/Token Minting**: The asset or token being minted needs an initialized account.
2. **Payment Receipt**: The wallet receiving payment must be initialized for the specific currency (e.g., USDC).
#### Solutions & UX Considerations
To handle these requirements without hitting transaction size limits, we use the following approaches:
1. **Pre-mint for Assets**: We perform a "pre-mint" operation—a free mint that initializes the account for the asset/token. This keeps the main transaction size within limits.
You can perform this pre-mint action directly from the admin dashboard on the listing modal. If you encounter the "Transaction Too Large" error, try running this pre-mint step first.

This pre-mint action currently lacks a specific success message or loading indicator in the UI. It does not count towards the visible mint count (which may still show 0), even though 1 quantity is minted on-chain.
2. **Wallet Initialization**: For the payment recipient wallet, we send a small amount of the required currency (e.g., USDC or SOL) to initialize the account. This ensures the fee recipient address is ready to receive mint payments.
#### Common Errors
If account initialization is not handled correctly, you may encounter the following errors:
**Transaction Simulation Failed**
```text
Simulation failed. Message: Transaction simulation failed: Error processing Instruction 0: invalid account data for instruction.
Logs: [
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]",
"Program log: Instruction: Transfer",
"Program log: Error: InvalidAccountData",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2799 of 600000 compute units",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA failed: invalid account data for instruction"
]
```
**Transaction Too Large**
```text
VersionedTransaction too large
```
## Related pages
Create achievement badges to gamify your loyalty program.
Enable gasless minting with managed private keys.
Set up the loyalty program that powers your rewards shop.
Build custom pages to showcase your collections and rewards.
---
## Snag Reporting (Looker) Overview
*Access Snag's Looker-powered dashboards for loyalty analytics. View transactions, accounts, referrals, minting data, and user engagement insights.*
Snag uses Google's data visualization tool, Looker, to manage reporting for all program data. Using Looker lets us ship faster by removing the need to build every reporting feature as a separate dev task.
Historically, dashboards were shared via custom links per project. Moving forward (as of November ’25), all partners will use a single shared dashboard link, with data automatically isolated by partner instance and permissioned based on your email:
[https://lookerstudio.google.com/u/0/reporting/d2b6a810-d985-4226-9c02-bc3666a1f263/page/tEnnC](https://lookerstudio.google.com/u/0/reporting/d2b6a810-d985-4226-9c02-bc3666a1f263/page/tEnnC)
## How to Use Looker / Best Practices
These are the two most frequent issues users run into:
1. **Filters are case-sensitive and require lowercase entries** - This is especially important for wallet addresses. Before entering an address into a filter, convert it to lowercase. You can use: https://convertcase.net/ to quickly convert upper + lower case addresses to lowercase.
2. **Downloading data uses the hidden “three dots”** - Downloads are accessed from the **hover-only three dots** inside each table or chart, _not_ the three dots at the top of the view next to “Edit.”
Hover → click the three dots → choose your export format.
**Note:** Downloads are capped at **750k rows**. For exports larger than 750k rows, either:
- Split the download using filters, or
- Use our APIs.


## Available data
Below is a summary of the available views in Looker and their core use cases.
Used to look at raw loyalty transaction data for every quest completed. This includes multipliers applied to the transaction, but multipliers themselves aren’t included as ‘rows’ only points earned.
You can also find poll and text entry response submissions for polls and text entry response quests here.
This is your CRM with an overview of associated wallet addresses, social profiles, and email addresses.
Look at top referrers, with summary statistics for number of a users referrals completing account set up and the ability to filter by referrer.
Deep dive into all of the referred addresses from any specific referring user.
Overview of quests completed by quest type and distribution of points distributed + quests completed by wallet address.
Overview of points distributed, outstanding, and spent over time.
All contracts created and assets minted via our minting + rewards shop platform with summaries of both contracts created and specific assets minted.
A summary of all information collected from both our burn to redeem and collect user information flows in our minting + rewards shop.
A summary of all auctions data and auction winners from our minting + rewards shop.
An overview of all badges rewarded to users.
A detailed export of every sweepstakes entry and status (Winner / not winner).
An overview of number of entries + unique users for each sweepstakes.
A deep dive into all quests created on the platform and the % of users engaging with each.
An overview of user first + last transaction dates, IP addresses, and any location data based on IP.
Post text and metadata for tracked X + TikTok posts rewarded via Snag social quests (New Post + Impression quests).
## Related pages
Configure and display public leaderboards for your loyalty program.
Track your platform usage including API calls and active users.
---
## Available loyalty rules
*Explore 50+ loyalty rules in Snag: social quests (X, Discord, Telegram), onchain actions (mint, hold, swap), gaming integrations, and custom function rules.*

## Overview
Snag loyalty supports a wide variety of platforms and user actions both on & offchain. With your Snag Loyalty Program you can track, evaluate, and reward user contributions across social (X, Discord, Telegram, and others) as well as anything onchain (bridge funds, hold token, connect wallet, and more). Using Snag's REST API you can also integrate contributions from an external application like a game or offchain social app.
## User onboarding rules
Reward users for completing their profile by filling out required details such
as bio, social links and other personal information. When setting up this
rule, you can define which fields must be completed to qualify for the reward.
[View more details](/loyalty/rules/complete-profile)
Reward users for referring friends with customizable fixed or percentage rewards.
[View more details](/loyalty/rules/refer-friends)
Reward users for checking in on a link of your choice.
[View more details](/loyalty/rules/check-in)
## Connect accounts rules
Reward users for connecting their email address.
[View more details](/loyalty/rules/connect-email)
Reward users for connecting their X (Twitter) account.
[View more details](/loyalty/rules/connect-twitter)
Reward users for connecting their Discord account.
[View more details](/loyalty/rules/connect-discord)
Reward users for connecting their Telegram account.
[View more details](/loyalty/rules/connect-telegram)
Reward users for connecting their YouTube account.
[View more details](/loyalty/rules/connect-youtube)
Reward users for connecting their Steam account.
[View more details](/loyalty/rules/connect-steam)
Reward users for connecting their Epic account.
[View more details](/loyalty/rules/connect-epic)
Reward users for connecting their Tiktok account.
[View more details](/loyalty/rules/connect-tiktok)
Reward users for connecting their Reddit account.
[View more details](/loyalty/rules/connect-reddit)
Reward users for connecting their Instagram account.
[View more details](/loyalty/rules/connect-instagram)
Reward users for connecting their GitHub account.
[View more details](/loyalty/rules/connect-github)
Reward users for connecting their wallet.
**Supported wallets:** EVM, Solana, IMX, SUI, and TON.
[View more details](/loyalty/rules/connect-wallet)
## Social quests
Reward users for reacting to a post on X (Twitter).
{' '}
[View more details](/loyalty/rules/x-post-reaction)
Reward users for creating a new post on X (Twitter).
{' '}
[View more details](/loyalty/rules/x-new-tweet)
Reward users for following a specific account on X (Twitter).
{' '}
[View more details](/loyalty/rules/follow-x-account)
Reward users for adding specific text to their X (Twitter) bio.
{' '}
[View more details](/loyalty/rules/x-text-in-bio)
Reward users for adding specific text to their X comment.
{' '}
[View more details](/loyalty/rules/x-text-in-comment)
Reward users for adding specific text to their X (Twitter) username.
{' '}
[View more details](/loyalty/rules/x-text-in-username)
Reward users based on the number of their X (Twitter) followers.
[View more details](/loyalty/rules/x-followers)
Reward users for getting views on their eligible X posts.
[View more details](/loyalty/rules/x-post-impression)
Reward users for having a specific role in your Discord server.
**Set up steps:**
Invite the bot to your server using [link] to enable role tracking.
Enter the Discord role ID or channel ID in the rule setup to track users with the specified role.
[View more details](/loyalty/rules/discord-member-role)
Grant a specific Discord role to users when they claim the reward. [View more
details](/loyalty/rules/discord-grant-role)
Reward users for joining your Discord server.
Reward users for sending messages in your Discord server.
[View more details](/loyalty/rules/discord-messages)
Reward users for joining your Telegram group.
[View more details](/loyalty/rules/telegram-join)
Reward users for sending messages in your Telegram group.
[View more details](/loyalty/rules/telegram-messages)
Reward users for adding items to their Steam wishlist.
[View more details](/loyalty/rules/steam-wishlist)
Reward users for subscribing to a YouTube channel.
[View more details](/loyalty/rules/youtube-subscriber)
Reward users for commenting on a YouTube video.
[View more details](/loyalty/rules/youtube-comment)
Reward users for commenting on a Reddit post.
[View more details](/loyalty/rules/reddit-comment)
Reward users for starring a GitHub repository.
[View more details](/loyalty/rules/github-repo-star)
Reward users for forking a GitHub repository.
[View more details](/loyalty/rules/github-repo-fork)
Reward users for becoming a collaborator on a GitHub repository.
[View more details](/loyalty/rules/github-repo-collaborator)
Reward users for merging pull requests to a specific branch in a GitHub repository.
[View more details](/loyalty/rules/github-merge-pr)
Reward users for creating posts on TikTok.
[View more details](/loyalty/rules/tiktok-post)
Reward users for creating posts on Instagram.
[View more details](/loyalty/rules/instagram-post)
Reward users for following a specific account on TikTok.
[View more details](/loyalty/rules/tiktok-follow)
Reward users for following a specific account on X.
This rule require X pro.
Reward users for reposting a post on X (Twitter). You can add a multiplier for verified X accounts.
This rule require X pro.
Reward users for liking a post on X (Twitter). You can add a multiplier for verified X accounts.
This rule require X pro.
Reward users for commenting on a post on X (Twitter).
This rule require X pro.
Reward users for creating a new post on X (Twitter) with a specific hashtag.
{' '}
[View more details](/loyalty/rules/x-new-tweet)
Reward users for getting a like on their X (Twitter) posts from your project.
This rule require X pro.
## Token activity rules
Reward users for minting a specific NFT.
[View more details](/loyalty/rules/token-mint)
Reward users for purchasing a specific NFT.
[View more details](/loyalty/rules/token-purchase)
Reward users for holding a specific NFT.
[View more details](/loyalty/rules/token-hold)
Reward users for selling a specific NFT.
[View more details](/loyalty/rules/token-sale)
Reward users for holding a set amount of ERC-20 tokens or native currency.
[View more details](/loyalty/rules/token-hold-erc20)
Reward users for swapping tokens.
## Smart contract rules
Reward users for interacting with a specific smart contract.
{' '}
{' '}
Reach out to Snag to learn more about how this works.
This allows you to track events on any EVM smart contract & use that data to
create rules.
**Rule set up:**
- You provide your smart contract address, the chain/network on which it is deployed, and a full/partial ABI containing the event you want to track.
- This populates an event dropdown from which you can select the relevant event.
- You can add additional evaluation logic on the parameters emitted in the event (e.g. only reward if `sender` = "0x123…").
**Examples:**
- Tracking trading activity (e.g. rewarding users based on the number of trades).
- Token transfers (e.g. matching specific token ids or amounts).
- Tracking staking activities.
[View more details](/loyalty/rules/contract-event)
Reward users for supplying liquidity on Uniswap V2.
Reward users for supplying liquidity on Uniswap V3.
## Other rules
Reward users for submitting custom input requested by your project.
[View more details](/loyalty/rules/text-input)
Reward users for entering a specific code provided by your project.
[View more details](/loyalty/rules/code-entry)
Reward users for clicking on a link.
[View more details](/loyalty/rules/link-click)
Reward users for selecting the correct answer in a quiz.
[View more details](/loyalty/rules/quiz)
Reward users for answering a poll.
[View more details](/loyalty/rules/poll)
Reward users based on how much they spend in your Shopify store. [View more
details](/loyalty/rules/spend-in-shopify-store)
Reward users for participating in governance votes.
[View more details](/loyalty/rules/snapshot-governance)
Reward users for creating an account with your partner.
[View more details](/loyalty/rules/create-partner-account)
Manual point creation via API. Reward users for any other contribution with a
set amount of points via our POST `api/loyalty/transactions` endpoint.
[View more details](/loyalty/external-rules)
Reward users based on custom logic using Stratus functions. Write JavaScript code to evaluate complex conditions and determine rewards dynamically.
[View more details](/loyalty/rules/stratus-function)
Reward users with points using a CSV upload or API integration.
## Related pages
Configure reward amounts, schedules, and evaluation settings.
Reward users for any action via the API transactions endpoint.
Write custom rule logic with serverless JavaScript functions.
Boost rewards with time-based or condition-based multipliers.
---
## Blocking users
*The blocked users feature allows you to exclude specific wallet addresses from earning rewards through any loyalty rules. This is useful for blocking known bots, suspicious accounts or specific users.*
## How it works
Blocked users will not be able to earn points from any rule, regardless of the trigger or reward conditions. You can manage blocked addresses by uploading a CSV file directly in the dashboard.
## Where to find it

1. Go to **Loyalty Program** in the sidebar.
2. On the **right-hand settings panel**, scroll down and click **Blocked users** (see highlighted area in the screenshot above).
## How to block wallets

Once inside the blocked users modal, follow these steps:
This will provide the structure needed to add wallet addresses. You can also download the current blocklist to review or edit previously submitted files.
---
## Checking Rule Completion Status
*Learn how to verify/check if a user has completed specific loyalty rules and received rewards*
## Overview
When implementing loyalty rules in your application, you often need to know whether a user has already completed a rule and received rewards. This information is useful for:
- Displaying completed rules differently in your UI
- Preventing users from attempting to complete already-completed rules
- Building custom reward history displays
Snag provides a straightforward way to check rule completion and reward status using the transaction entries endpoint.
## Using Transaction Entries to Verify Rule Completion
The transaction entries API endpoint allows you to check whether a user has completed specific rules by looking for reward transactions associated with those rules.
### API Endpoint
GET /api/loyalty/transaction_entries
### Required Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `userCompletedLoyaltyRuleId` | string[] | Array of loyalty rule IDs to check for completion |
| `userId` | string | The ID of the user to check |
### Key Behavior
**Important:** When using the `userCompletedLoyaltyRuleId` parameter, the endpoint will return **only the latest transaction entry** for each specified rule ID. This optimized behavior makes it ideal for quickly checking completion status without filtering through multiple entries per rule.
### Example Request
```javascript
const fetchRuleCompletionStatus = async (userId, ruleIds) => {
const queryParams = new URLSearchParams({
userId: userId,
});
// Add each rule ID to the query parameters
ruleIds.forEach(ruleId => {
queryParams.append('userCompletedLoyaltyRuleId', ruleId);
});
const response = await fetch(
`${API_BASE_URL}/api/loyalty/transaction_entries?${queryParams.toString()}`,
{
method: 'GET',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
}
);
return await response.json();
};
```
## Interpreting Results
The response contains an array of transaction entries, each representing the most recent reward transaction for a completed rule:
1. **Rule is completed**: If a transaction entry exists for a specific rule ID, the user has completed that rule and received rewards.
2. **Rule is not completed**: If no transaction entry exists for a rule ID, the user has not completed that rule or has not received rewards for it.
3. **Latest entries only**: Since the `userCompletedLoyaltyRuleId` parameter is used, the endpoint returns only the latest entry per rule, you'll always see the most recent completion status, which is particularly useful for rules that can be completed multiple times (like daily check-ins).
## Implementation Example
Here's a complete example showing how to check if a user has completed specific rules and display the appropriate UI:
```javascript
// Check if user has completed specific rules
async function checkRuleCompletionStatus(userId, ruleIds) {
try {
const queryParams = new URLSearchParams({ userId });
ruleIds.forEach(ruleId => {
queryParams.append('userCompletedLoyaltyRuleId', ruleId);
});
const response = await fetch(
`${API_BASE_URL}/api/loyalty/transaction_entries?${queryParams.toString()}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
}
);
const { data } = await response.json();
// Create a map of rule IDs to completion status
const completionStatus = {};
ruleIds.forEach(ruleId => {
completionStatus[ruleId] = false;
});
// Update completion status based on transaction entries
// Each entry is guaranteed to be the latest for that rule
data.forEach(entry => {
if (entry.userCompletedLoyaltyRuleId &&
ruleIds.includes(entry.userCompletedLoyaltyRuleId)) {
completionStatus[entry.userCompletedLoyaltyRuleId] = true;
}
});
return completionStatus;
} catch (error) {
console.error('Error checking rule completion status:', error);
throw error;
}
}
// Example usage in a component
function QuestsComponent({ userId, quests }) {
const [completionStatus, setCompletionStatus] = useState({});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const ruleIds = quests.map(quest => quest.ruleId);
checkRuleCompletionStatus(userId, ruleIds)
.then(status => {
setCompletionStatus(status);
setIsLoading(false);
})
.catch(() => {
setIsLoading(false);
});
}, [userId, quests]);
if (isLoading) {
return Loading quests...
;
}
return (
{quests.map(quest => (
))}
);
}
```
## Related Documentation
For more information on handling rule completion, refer to:
- [Verifying Rule Completion](/loyalty/verifying-rule-completion) - Details on the complete process for submitting and verifying rule completion
- [API Reference: Get Loyalty Transaction Entries](https://docs.snagsolutions.io/api-reference/loyalty/get-loyalty-transaction-entries) - Full API documentation for the transaction entries endpoint
## Limitations and Considerations
- For some rule types (like cadence-based rules), completion status may update on a schedule rather than immediately
- Using the `userCompletedLoyaltyRuleId` parameter, the endpoint returns only the latest entry per rule, making it efficient for checking current completion status
- Cache results where appropriate to reduce API calls in your application
---
## Create loyalty rules using API
*Learn how to create different types of loyalty rules through the API with the correct parameters and metadata*
import AiAssistantCallout from '/snippets/ai-assistant-callout.mdx'
## Overview
Loyalty rules define how users earn points, multipliers, or badges in your loyalty program. Each rule type has specific requirements for parameters, metadata, and configuration. This guide covers all available rule types and their required fields.
## Common Parameters
All loyalty rules share these common parameters:
The organization ID that owns this rule.
The website ID where this rule will be applied.
The name of the loyalty rule (max 255 characters).
Optional description of the rule.
The type of loyalty rule. See available types below.
How often eligibility is checked. Options: `once`, `daily`, `weekly`,
`monthly`, `immediately`.
How often users can earn rewards. Options: `once`, `daily`, `weekly`,
`monthly`, `unlimited`, `custom`.
When the rule becomes active (ISO 8601 format).
Optional end time for the rule (ISO 8601 format).
Type of reward: `points`, `multiplier`, or `badge`.
The ID of the loyalty currency (points) to reward.
The blockchain network. Options: `mainnet`, `goerli`, `polygon`, `arbitrum`,
etc.
## Reward Configuration
### Reward Types
Reward users with points for completing actions.
The number of points to reward.
ID of the currency to reward with.
Reward users with a multiplier that enhances their earnings.
The multiplier value (must be \> 1).
Apply multiplier to user's existing balance.
Array of currency IDs to apply multiplier to. Empty array applies to all currencies.
Reward users with a badge.
ID of the badge to reward.
How long the badge lasts: `permanent` or `dynamic`.
## Rule Types
Rule types are grouped below to match the categories shown in the loyalty dashboard. Each type only reads the `metadata` (and top-level) fields listed for it — omit the rest.
## User Onboarding
### Complete Profile Details (`profile_completed`)
Reward users for completing their profile.
Which profile fields must be filled. Keys: `profile_picture`, `name`,
`location`, `nft_portfolio`, `about`, `email`, `twitter`, `discord`,
`telegram`, `evm`, `solana`, `sui`, `ton`, `imx`. If omitted, a default set is
checked (bio, avatar, X, Discord, email).
### Refer a Friend (`referred_user`)
Reward users for referring others.
Points for the referred user.
Points for the referrer.
Currency for referrer reward.
Require referred users to achieve points.
Required points when achievePoints is true.
Require complete profile.
Require Twitter connection.
Require Discord connection.
Require email connection.
### Check In (`check_in`)
Reward users for checking in.
Points for each check-in.
Enable streak tracking for consecutive check-ins.
Optional array of streak milestones and rewards.
Number of consecutive days for this streak.
Points awarded for reaching this milestone.
## Connect Accounts
Reward users for connecting an account. Most connection rules need **no metadata** — pass an empty object `"metadata": {}`.
| Rule type | Metadata |
| --------------------- | ---------------- |
| `connected_email` | None — pass `{}` |
| `connected_phone` | None — pass `{}` |
| `connected_telegram` | None — pass `{}` |
| `connected_tiktok` | None — pass `{}` |
| `connected_github` | None — pass `{}` |
| `connected_youtube` | None — pass `{}` |
| `connected_reddit` | None — pass `{}` |
| `connected_instagram` | None — pass `{}` |
| `connected_steam` | None — pass `{}` |
| `connected_epic` | None — pass `{}` |
The remaining connection rules take metadata:
### Connect X (`connected_twitter`)
Apply a higher reward to verified (blue-check) accounts.
Multiplier applied when `enableVerifiedMultiplier` is true (e.g. `1.5`).
### Connect Discord (`connected_discord`)
Require users to also join specific Discord servers.
Servers (and optionally channels/roles) users must join.
Discord server ID.
Channels to verify activity in.
Optional roles to verify.
### Connect Wallet (`connect_wallet`)
Which wallet type the user must connect (e.g. `evm`, `solana`).
## Social Quests
X (Twitter) engagement rules generally require `oauthCredentialsId` (top-level) so the platform can verify activity, and many accept an optional verified-account multiplier (`metadata.enableVerifiedMultiplier`, `metadata.verifiedMultiplier`).
### React to an X Post (`drip_x_tweet`)
URL of the X post to engage with.
Twitter user ID of the post author.
Require liking the post.
Require reposting.
Require commenting.
Twitter OAuth credentials (top-level field).
### Post on X (`drip_x_new_tweet`)
Text that must be included in the tweet.
Require users to submit post link (default: true).
Require post to include media.
### Follow an X Account (`drip_x_follow`, `twitter_follow`)
X account to follow.
Twitter user ID of the account.
OAuth credentials for Twitter (top-level field).
`drip_x_follow` accepts the account as `metadata.twitterAccountUrl` (full
profile URL) instead of username/ID.
### Add Text to X Bio / Username / Comment (`drip_x_text_in_bio`, `drip_x_text_in_name`, `drip_x_text_in_comment`)
Text that must appear in the user's bio, display name, or comment.
(`drip_x_text_in_comment`) Only reward posts that pass quality/sentiment
checks.
(`drip_x_text_in_comment`) Minimum follower count to qualify.
### Reach X Followers (`twitter_followers`)
Tiered rewards by the user's follower count.
### Get X Post Impressions (`post_impressions`)
Tiered rewards by post view/impression count.
Only reward posts that pass quality/sentiment checks.
Minimum follower count to qualify.
Optional top-level flag to track progress. `mediaUrl` (top-level) can supply a
media reference.
### Repost an X Post (`Retweet`)
Twitter user ID of the account whose posts are monitored for reposts.
### Like an X Post (`twitter_like`)
Account that posted the tweet.
Twitter user ID.
OAuth credentials (top-level field).
### Comment on an X Post (`twitter_comment`)
Twitter user ID of the account whose posts are monitored for comments.
### Post on X with Hashtag (`twitter_post_hashtag`)
Hashtag to search for in recent posts.
### Get an X Post Liked by Project (`tweet_liked_by_project`)
Twitter user ID of the project account whose like qualifies the user.
OAuth credentials for Twitter (top-level field).
Multiply the reward by the tweet's impression count.
### Get Discord Role (`discord_member`)
Servers and roles to verify. Each item uses `id` and `roles`.
### Join Discord Server (`discord_join`)
Servers the user must join. Only the server `id` is required for this type.
### Send Discord Messages (`DiscordMessages`)
Server `id` and the `channels` whose messages count. Each channel can specify
`text` to match and `emojis` reactions.
Optional tiered rewards based on message count.
### Grant a Discord Role (`discord_role_grant`)
Server `id` plus the `roles` to grant. `roles` is required for this type.
Discord server (guild) ID.
Channels to check. Each: `{ id: string, text?: string, emojis?: Array<{ id: string }> }`. Used by `DiscordMessages`.
Roles to verify or grant. Required for `discord_role_grant`.
### Join Telegram Group (`telegram_join`)
ID of the Telegram channel the user must join.
### Send Telegram Messages (`telegram_messages`)
ID of the Telegram channel whose messages count.
Optional tiered rewards based on message count.
### Follow a TikTok Account (`tiktok_follow`)
TikTok profile URL to follow (e.g. `https://www.tiktok.com/@username`).
Despite the field name, this holds the TikTok URL.
### Post on TikTok (`tiktok_post`)
Text to check in post.
Require post link submission.
`IMPRESSIONS_COUNT` or `ELIGIBLE_POST`.
### Post on Instagram (`instagram_post`)
Text or hashtag the post must include.
Only reward posts that pass quality checks.
Minimum follower count to qualify.
### Follow an Instagram Account (`instagram_follow`)
Instagram profile URL to follow (e.g. `https://www.instagram.com/username`).
Despite the field name, this holds the Instagram URL.
### Subscribe to a YouTube Channel (`youtube_subscribers`)
YouTube channel ID the user must subscribe to.
Optional Google API integration ID (top-level field) used for verification.
### Comment on a YouTube Video (`youtube_comment`)
YouTube video ID the user must comment on.
Optional Google API integration ID (top-level field) used for verification.
### Comment on a Reddit Post (`reddit_comment`)
ID of the Reddit post users must comment on.
Full URL of the Reddit post.
### Add an Item to Steam Wishlist (`steam_wishlist`)
Steam application ID the user must wishlist.
### Spend in Shopify Store (`shopify_spend`)
Shopify store domain (top-level field).
Shopify admin API access token (top-level field).
Range-based rewards by purchase amount.
## Token Activity
### Mint an NFT (`MintOn`)
Reward users for minting NFTs.
Collections to track (top-level field).
Only reward users who have never sold the token.
Optional tiered rewards.
### Purchase an NFT (`BoughtOn`)
Reward users for purchasing NFTs.
Collections to track (top-level field).
Only reward existing users.
Only reward users who never sold the token.
Optional tiered rewards.
```json
{
"name": "NFT Purchase Rewards",
"type": "BoughtOn",
"network": "mainnet",
"collections": [{ "address": "0x...", "network": "mainnet" }],
"frequency": "daily",
"interval": "once",
"startTime": "2024-01-01T00:00:00Z",
"rewardType": "points",
"loyaltyCurrencyId": "currency_123",
"amount": 1000,
"metadata": {
"onlyRewardExistingUser": false,
"hasNeverSold": false
}
}
```
### Hold an NFT (`TokenHold`)
Reward users for holding specific NFTs.
Collections to track (top-level field).
Only reward existing users.
Reward only one token ownership per contract.
Tiered rewards by number of tokens held.
```json
{
"name": "Hold Blue Chip NFT",
"type": "TokenHold",
"network": "mainnet",
"collections": [
{
"address": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D",
"network": "mainnet"
}
],
"frequency": "daily",
"interval": "daily",
"startTime": "2024-01-01T00:00:00Z",
"rewardType": "points",
"loyaltyCurrencyId": "currency_123",
"amount": 100,
"metadata": {
"onlyRewardExistingUser": false,
"onlyRewardSingleTokenOwnership": false
}
}
```
### Sell an NFT (`SoldOn`)
Reward users for selling NFTs.
Filter by specific sale currency.
Currency address when hasSaleCurrency is true.
Optional tiered rewards.
### Hold a Fungible Token (`token_hold_erc20`)
Reward users for holding ERC20 tokens.
Reward based on USD value held rather than token count.
Apply a multiplier per holding tier instead of fixed points.
Tiered rewards by amount held.
### Swap a Token (`swap`)
Reward users for token swaps.
`any`, `relay`, or `lifi`.
Relay referrer ID when provider is `relay`.
Source-token filter: `{ mode: "any" | "specific", chain, tokens, trackAmount }`.
Destination-token filter: `{ mode: "any" | "specific", chain, tokens, trackAmount }`.
Require cross-chain swaps.
### Hold tokens in Robinhood (`robinhood_token_hold`)
Token symbols to track (e.g. `["TRUMP", "SPY"]`).
Optional tiered rewards by holding amount. Falls back to top-level `amount` if
omitted.
## Complex Onchain Logic
### Smart Contract Event (`smart_contract_event`)
Reward based on smart contract events.
Contract address to monitor (top-level field).
Event signature to track.
`everyEvent` or specify conditions.
Event parameter filters.
Address parameter name in event.
### Stratus Function (`stratus_function`)
Custom on-chain logic executed by a Stratus function; the function controls reward output.
ID of the Stratus function to run (top-level field).
Optional Stratus subscription ID (top-level field).
Optionally route rewards to the dApp owner. When true, `rewardType` must be
`points` and `network` is required.
### Provide Liquidity on Uniswap V2 / V3 (`liquidity_uniswap_v2`, `liquidity_uniswap_v3`)
Chain for liquidity pools.
Protocol identifier (e.g., "uniswap-v2/eth-usdc").
Array of liquidity pool addresses (max 20, same network/protocol).
`fixed` or `custom`.
USD value per day (when calculationType is `fixed`).
Custom calculation formula (when calculationType is `custom`).
## Developers Contribution
### Star / Fork / Become a Collaborator (`github_repo_star`, `github_repo_fork`, `github_repo_collaborator`)
Repository URL. Must match `https://github.com//`.
### Merge a GitHub PR (`github_merge_PR`)
Repository URL. Must match `https://github.com//`.
Target branch to count merged PRs against. Defaults to `main` if omitted.
Optional tiered rewards by number of merged PRs.
## Other
### Submit Text Input (`text_input`)
Placeholder text for the input field.
Minimum character length required.
### Enter a Code (`code_entry`)
Reward users for entering promo codes.
Type: `code`, `csv`, or `generate`.
Single shared code (when promoCodeType is `code`).
CSV URL with unique codes (when promoCodeType is `csv`).
Number of codes to generate (when promoCodeType is `generate`).
Only allow new users (last 6 hours) to redeem.
### Click a Link (`link_click`)
URL the user clicks.
Delay (1–600 s) before the reward is granted.
Platform shown for the link (e.g. X, Discord, or `Custom`).
Label on the action button.
### Answer a Quiz (`quiz`)
Question text.
Answer options with `isCorrect` flag. Exactly one choice must be correct.
### Answer a Poll (`poll`)
Poll question text.
Answer choices.
Allow users to vote multiple times.
### Participate in Governance Vote (`snapshot_governance`)
Snapshot proposal(s) to track. One proposal per rule.
### Create a Partner Account (`create_partner_account`)
Partner site URL. Must match the partner auth domain.
Label on the action button.
### External Rule (`external_rule`)
Custom rules managed by external services.
Base points amount.
Track completion progress (top-level field).
Range-based rewards with custom amounts.
Multiply reward by number of actions.
Description of what the external rule tracks.
### Points Airdrop (`points_airdrop`)
Airdrop points to specific users via CSV.
CSV URL with wallet addresses and point amounts (top-level field).
Track processing progress.
## dApp Activity
Rank and reward dApps by on-chain activity. These rules rely on several **top-level** fields in addition to `metadata.range`, and route rewards to the dApp owner (`isDappRule`).
Applies to: `dapps_by_gas_spent`, `dapps_by_active_users`, `dapps_by_new_users`, `dapps_by_transaction_count`.
Tiered rewards by rank/threshold for the chosen metric.
Data window for the metric: `daily`, `weekly`, or `monthly`. Leave empty for
all-time.
Optional filter limiting to dApps deployed within this window.
Routes rewards to the dApp owner. Immutable after creation.
Blockchain network for the rule (required when `isDappRule` is true).
`dapps_by_user_retention` appears in the dashboard but is not yet implemented
— creating it will fail. Use one of the four supported dApp metrics above.
## Range-Based Rewards
Some rules support range-based rewards with tiered amounts:
Array of reward ranges.
Minimum value for this range.
Maximum value (use `Number.MAX_SAFE_INTEGER` for unlimited).
Points awarded for this range.
Optional badge for this range.
### Range Examples
```json
"metadata": {
"range": [
{
"startRange": 1,
"endRange": 10,
"amount": 100
},
{
"startRange": 11,
"endRange": 50,
"amount": 500
},
{
"startRange": 51,
"endRange": 9223372036854775807,
"amount": 1000
}
]
}
```
```json
"metadata": {
"range": [
{
"startRange": 1,
"endRange": 9223372036854775807,
"amount": 1.5
}
],
"isTokenHoldMultiplier": true
}
```
## Advanced Configuration
### Max Reward Limits
Maximum points users can earn in the interval.
Interval period: `daily`, `weekly`, `monthly`.
### Claim Type
`manual` (user must claim) or `auto` (auto-rewarded).
### Reward Lifetime
For multipliers/badges: `permanent` (never removed) or `dynamic` (removed if
requirements not met).
### Airdrop Duration
When airdrop becomes effective.
When airdrop ends.
## Validation Examples
```json
{
"organizationId": "org_123",
"websiteId": "website_456",
"name": "Daily Login",
"type": "check_in",
"frequency": "daily",
"interval": "daily",
"startTime": "2024-01-01T00:00:00Z",
"rewardType": "points",
"amount": 10,
"loyaltyCurrencyId": "currency_789",
"network": "ethereum",
"metadata": {}
}
```
```json
{
"organizationId": "org_123",
"websiteId": "website_456",
"name": "Hold Premium NFT",
"type": "TokenHold",
"network": "mainnet",
"collections": [{
"address": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D",
"network": "mainnet"
}],
"frequency": "daily",
"interval": "daily",
"startTime": "2024-01-01T00:00:00Z",
"endTime": "2024-12-31T23:59:59Z",
"rewardType": "points",
"amount": 100,
"loyaltyCurrencyId": "currency_789",
"metadata": {
"hasCustomRewardsUrl": true,
"customRewardsApiUrl": "https://api.example.com/rewards",
"customRewardsApiKey": "key_xyz"
}
}
```
```json
{
"organizationId": "org_123",
"websiteId": "website_456",
"name": "Tweet Challenge",
"type": "drip_x_new_tweet",
"network": "ethereum",
"frequency": "daily",
"interval": "unlimited",
"startTime": "2024-01-01T00:00:00Z",
"rewardType": "points",
"loyaltyCurrencyId": "currency_789",
"metadata": {
"checkText": ["#project", "launch"],
"requirePostLink": true,
"requirePostMediaLink": false,
"range": [
{
"startRange": 1,
"endRange": 5,
"amount": 10
},
{
"startRange": 6,
"endRange": 20,
"amount": 50
},
{
"startRange": 21,
"endRange": 9223372036854775807,
"amount": 100
}
]
}
}
```
## Error Handling
All required fields must be provided. Missing required fields will result in a
400 Bad Request error with details about which fields are missing.
Range values must not overlap. The start range should be less than or equal to
the end range for each range entry.
For Ultra networks (Ultra/UltraTestnet), the collection address format is
`address::symbol`. For example: `accountname::TOKEN`.
Use the loyaltyCurrencyId field consistently across rules to ensure proper
currency management in your loyalty program.
## Rule Type Limitations and Restrictions
### Multiplier Reward Type Support
The `multiplier` reward type is **NOT supported** for the following rule types:
- `check_in`
- `link_click`
- `referred_user`
- `points_airdrop`
Only `points` and `badge` reward types are available for these rules.
### Claim Type Support
Different rule types support different claim types (manual vs auto-reward):
These rules require users to manually claim rewards:
- `check_in`
- `text_input`
- `poll`
- `quiz`
- `create_partner_account`
- `telegram_join`
- `drip_x_follow`
- `drip_x_tweet`
- `drip_x_new_tweet`
- `swap`
These rules automatically reward users (no manual claim button):
- `twitter_followers`
- `TokenHold` (with dynamic lifetime)
- `Retweet`
- `BoughtOn`
- `SoldOn`
- `MintOn`
- `twitter_like`
- `twitter_comment`
- `twitter_post_hashtag`
- `profile_completed`
- `referred_user`
- `snapshot_governance`
- `token_hold_erc20`
- `BoughtWithRoyalties`
- `SoldWithRoyalties`
These rules support both claim types:
- `external_rule`
- `telegram_messages`
- `connect_wallet`
- `connected_epic`
### Reward Lifetime Options
The `rewardLifetime` field (for multipliers and badges) has different support levels:
Most rules only support `permanent` lifetime where rewards never expire:
- `check_in`
- `twitter_followers`
- `Retweet`
- `BoughtOn`, `SoldOn`, `MintOn`
- `twitter_like`, `twitter_comment`, `twitter_post_hashtag`
- `profile_completed`, `referred_user`
- `poll`, `quiz`
- Most social and connection rules
With `permanent` lifetime, once a user qualifies for the reward, they keep it forever even if they stop meeting requirements.
These rules support both `permanent` and `dynamic` lifetime:
- `TokenHold` - Most flexible, supports both
- `external_rule` - Can expire if user stops meeting requirements
With `dynamic` lifetime, rewards are removed if the user no longer meets the requirements (e.g., sold their NFT).
### Interval Restrictions
Some rules have limited interval options:
Rules with `frequency: immediately` typically only support `interval: once` or specific intervals.
Rules with `frequency: none` may support custom intervals.
### Range-Based Rewards
Range-based rewards are **ONLY** supported for:
- `TokenHold`
- `BoughtOn`, `SoldOn`, `MintOn`
- `token_hold_erc20`
- `twitter_followers` (with `rewardCriteria`)
- `external_rule` (with `trackProgress`)
- `shopify_spend`
Range-based rewards require the `metadata.range` array. Maximum range value is
`Number.MAX_SAFE_INTEGER` (use this for unlimited).
### Network-Specific Limitations
For **Ultra** and **UltraTestnet** networks, collection addresses require a
symbol suffix: `address::symbol` (e.g., `accountname::TOKEN`)
### Token Holdings
ERC20 token holding rules (`token_hold_erc20`) require network support for USD
price tracking. Networks without USD support will disable the `rewardPerValue`
option.
### Liquidity Pool Limits
Liquidity rules support a maximum of **20 pools** per rule, all within the
same network and protocol with the same factory address.
### Shopify Integration
Shopify spend rules require providing both `shopifyStoreUrl` and
`shopifyStoreAccessToken`. The access token is used for one-time
authentication during rule creation but is not stored.
---
## Create your first rule
*Create your first Snag loyalty rule in minutes. Open the admin dashboard, pick a rule type, configure reward settings, and start tracking user engagement.*
### Set up instructions
Go to [https://admin.snagsolutions.io](https://admin.snagsolutions.io).
Navigate to the **Loyalty Program** tab in the left-side navigation options.
This opens the rule creation menu.
Choose from the **Type** options. Based on the type selected, various form fields will render which allow you to configure the data source (if relevant), rule evaluation properties (e.g. which twitter account or smart contract to track) and the reward configuration.
Based on the type, fill in the rest of the parameters (for further help please contact us at [support@snagsolutions.io](support@snagsolutions.io)).
This will close the rule creation page and show you the rule that was just created (refresh the page if this does not appear immediately).
### 🎉 Your first loyalty rule is now live!
## Related pages
Browse 50+ rule types for social, onchain, and custom engagement.
Fine-tune reward amounts, schedules, and evaluation logic.
Boost rewards with time-based or condition-based multipliers.
Automate rule creation programmatically with the REST API.
---
## Custom Rewards API
*Configure dynamic rewards for tokens using external APIs or CSV files*
## **Overview**
The Custom Rewards API feature allows you to define dynamic rewards for individual tokens instead of using fixed reward amounts. This feature is available for the following loyalty rule types:
- [Token Hold](/loyalty/rules/token-hold)
- [Token Purchase](/loyalty/rules/token-purchase)
- [Token Sale](/loyalty/rules/token-sale)
- [Token Mint](/loyalty/rules/token-mint)
You can provide custom rewards through either:
- **API Endpoint**: Real-time data from your external service
- **CSV File**: Static data uploaded to a publicly accessible URL
## **API Configuration**
### **API Endpoint Setup**
When using an API endpoint, you need to provide:
- **Custom Rewards URL**: Your API endpoint URL
- **API Key**: Authentication key for your API
The API endpoint must be accessible from Snag's servers and return data in the specified format. Ensure your API is reliable and has appropriate rate limiting.
### **API Response Format**
Your API must return a paginated response with the following structure:
Snag will make requests like this:
```bash
curl -X GET 'https://your-api.com/rewards' \
-H 'x-api-key: YOUR_API_KEY'
```
Your API should return a JSON response in this format:
```json
{
"count": 1000,
"next": "https://your-api.com/rewards?page=2",
"previous": null,
"results": [
{
"tokenId": "123",
"reward": 50,
"points": 50,
"bonus": 50
},
{
"tokenId": "456",
"reward": 100,
"points": 100,
"bonus": 100
}
]
}
```
### **Response Field Requirements**
Total number of reward records available across all pages.
URL for the next page of results. Set to `null` for the last page.
URL for the previous page of results. Set to `null` for the first page.
Array of reward objects for the current page.
Unique identifier for the token. Can be any string format (e.g., "123", "0x123", "hape_123").
Reward amount in points for this token. Must be a positive number.
Alternative field name for reward amount. Used if `reward` is not present.
Alternative field name for reward amount. Used if neither `reward` nor `points` are present.
### **Pagination Support**
The API supports pagination through the `next` field. Snag will automatically fetch all pages until `next` is `null`.
### **Request Details**
Snag's servers will make the following requests to your API:
**Initial Request:**
```bash
curl -X GET 'https://your-api.com/rewards' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'User-Agent: Snag-Loyalty-System/1.0'
```
**Paginated Request:**
```bash
curl -X GET 'https://your-api.com/rewards?page=2' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'User-Agent: Snag-Loyalty-System/1.0'
```
**Request Headers:**
- `x-api-key`: The API key you provided during configuration
- `User-Agent`: `Snag-Loyalty-System/1.0`
**Request Behavior:**
- Snag will make sequential requests to your API
- Each request has a 15-minute timeout
- Snag will follow the `next` URL from your response until it's `null`
- No query parameters are required for the initial request
- Your API should handle pagination through the `next` field in responses
## **CSV File Configuration**
As an alternative to API endpoints, you can provide custom rewards through a CSV file:
### **CSV Format Requirements**
The CSV file must have the following structure:
```csv Example CSV
tokenId,reward
123,50
456,100
789,25
```
```csv Alternative Format
tokenId,points
0x123,75
0x456,150
0x789,30
```
### **CSV Field Requirements**
Unique identifier for the token. Can be any string format.
Reward amount in points for this token. Must be a positive number.
## **Error Handling**
### **API Errors**
If the API is unavailable or returns an error:
- The loyalty rule will continue to execute with default rewards
- An error will be logged for debugging
- Users will receive rewards based on the rule's standard configuration
### **Data Validation**
Invalid reward data is automatically filtered out:
- Tokens with missing or invalid `tokenId` are skipped
- Rewards with zero or negative values are ignored
- Malformed API responses are logged and skipped
Always test your API endpoint thoroughly before deploying to production. Ensure it returns valid data and handles edge cases properly.
## **Performance Considerations**
### **API Rate Limits**
- Snag will make sequential requests to your API
- Each request includes a 15-minute timeout
- Consider implementing appropriate rate limiting on your API
### **Data Size**
- Large datasets are handled through pagination
- Consider caching frequently accessed data
- Optimize your database queries for better performance
### **Reliability**
- Ensure your API has high uptime
- Implement proper error handling and logging
- Consider using a CDN for CSV files
## **Security Best Practices**
### **API Authentication**
- Use secure API keys with appropriate permissions
- Rotate keys regularly
- Implement IP whitelisting if possible
### **Data Validation**
- Validate all input data on your API
- Sanitize token IDs and reward values
- Implement proper error responses
Your custom rewards API is ready when:
- API returns valid JSON with required fields
- Pagination works correctly
- Authentication is properly configured
- Error handling is implemented
---
## External rule
*This recipe explains the concept of an external rule, how to configure it with Snag and how to issue rewards to your users.*
import FeedbackForm from '/snippets/FeedbackForm.mdx'
import AiAssistantCallout from '/snippets/ai-assistant-callout.mdx'
## What is an external loyalty rule?
You may wish to track user behaviour in contexts that Snag is unable to obtain a data source for (e.g. in a non-web/blockchain based game). For this and similar cases, Snag provides the concept of external rules.
An external rule is mostly the same as a standard loyalty rule and differs only in that it contains no built-in execution and evaluation logic.
Instead, this is done application-side (e.g. in-game) and, when a rewardable action is identified, Snag's [`POST /api/loyalty/rules/{id}/complete`](https://docs.snagsolutions.io/api-reference/loyalty/complete-loyalty-rule) endpoint can be hit.
This tells Snag that a given rule was completed by the given user(s) and issues the associated reward.
You can get more details on how to complete a rule in the [Submit and verify loyalty rule completion](/loyalty/verifying-rule-completion) recipe.
The id returned in the response will be used to complete the rule when a user performs the required action.
## Evaluating external rules
Unlike built-in rules, the evaluation of external rules happens within your own system. This means you must track the user behavior (e.g., game milestones or external API interactions) and determine when a rule is completed.
Once a rewardable action is identified, you can programmatically notify Snag by completing the external rule using the API (see next step).
## How to issue points for users who complete an external rule
To issue points for a user who completes an external rule, use the [`POST /api/loyalty/rules/{id}/complete`](https://docs.snagsolutions.io/api-reference/loyalty/complete-loyalty-rule) endpoint. This endpoint informs Snag that the rule has been satisfied for a specific user and triggers the reward.
You can get more details on how to complete a rule in the [Submit and verify loyalty rule completion](/loyalty/verifying-rule-completion) recipe.
Once the request is processed, Snag will issue the reward to the user based on the rule’s configuration.
## Summary Workflow
1. Create the External Rule: Use our admin dashboard to create the rule.
2. Track Behavior: Use your application to monitor user actions that meet the rule’s criteria.
3. Complete the Rule: When a user completes the action, submit the rule completion to Snag using the API, more details can be found in the [Submit and verify loyalty rule completion](/loyalty/verifying-rule-completion) recipe.
---
## Handling images and asset uploads
*How Snag validates image sources, when external URLs are allowed, and how to upload assets for use in objects like Users, Loyalty Rules, Pages, and Badges etc.*
This page explains Snag's image/asset handling policy, supported sources, file
size limits, and the recommended upload flow for use across admin and API. Use
this when attaching images to objects such as Users, Loyalty Rules, Pages, and
Badges.
### What you can and cannot link
- **Internal assets are required (most fields)**: For images that are uploaded from admin and linked to Snag objects (for example: `UserMetadata.logoUrl`, `LoyaltyRule.mediaUrl`, page builder images, and `LoyaltyBadge.imageUrl`), you must first upload the file using Snag's asset endpoint and then reference the returned asset `url` in your create/update request.
- **External URLs allowed (specific fields only)**:
- **`UserMetadata.logoUrl`**: May reference external sources (e.g., Twitter).
- **`LoyaltyPost.mediaUrl`**: May reference external sources (e.g., Twitter and other social platforms).
For fields that require internal assets, Snag rejects external URLs. Always
upload via the asset API first and then pass the returned `url`.
### File size limits and validation
- **Images**: up to 1 MB
- **Videos**: up to 10 MB
- You must provide the exact `fileSize` (in bytes) when requesting the presigned URL, and the uploaded file must match that size.
Supported paths include: `announcements`, `images`, `videos`, `banners`,
`fonts`, `posts`, `profiles`, `minting`, `loyalty`, `tokenClaims`.
### The upload flow (recommended)
Use the Asset Upload endpoint to obtain a presigned URL, upload your file to that URL, and then reference the returned public `url` in your object mutation.
Call the Asset Upload API with the file name, exact file size (in bytes), and desired `filePath`. You'll receive a `signedUrl` (for PUT upload) and a public `url` (to store on your object). See the Asset upload endpoint documentation.
Perform a `PUT` to `signedUrl` with the file as the request body. Ensure
`Content-Type` matches the file type and upload before the URL expires.
Include the returned `url` in the appropriate field. This is required for fields that do not accept external URLs.
### Common validation errors and fixes
- **Likely cause**: `fileSize` mismatch between the presigned request and actual uploaded bytes.
- **Fix**: Recompute the exact file size in bytes and request a new `signedUrl` with the corrected `fileSize`.
- **Likely causes**: Expired `signedUrl` or incorrect `Content-Type`. -
**Fix**: Request a new `signedUrl` and upload immediately; ensure
`Content-Type` matches the file type (e.g., `image/png`).
- **Cause**: The target field only accepts Snag-hosted assets. - **Fix**: Use
the upload flow above and pass the returned `url` instead of an external link.
- **Allowed**: `UserMetadata.logoUrl`, `LoyaltyPost.mediaUrl` (can reference external sources like Twitter and other social platforms).
- **Not allowed**: Most admin-uploadable fields such as `UserMetadata.logoUrl`, `LoyaltyRule.mediaUrl`, page builder images, and `LoyaltyBadge.imageUrl`.
### Admin UI behavior
When you upload images from the admin UI for fields that require internal assets, Snag uses the same flow under the hood: it requests a presigned URL, uploads your file, and stores the resulting asset `url` on the object. Pasting external URLs into these fields is blocked.
### API reference
See the official endpoint for complete parameters, supported paths, and responses:
- **Asset Upload**: Get Asset upload URL
You now know when external URLs are allowed and how to upload images to Snag
and attach them to your objects safely.
---
## Internal currency integration
*Learn how to create external loyalty rules with progress tracking and multiple reward tiers*
### Overview
If you have an existing loyalty system, or want to interact with an off-chain points system we allow partners to integrate existing loyalty points to be distributed as a part of the Snag Loyalty Program to be used as your all-in-one destination for points & points management moving forward.
### Configuration
In our self-serve dashboard we will ask for the following:
- Get balance URL
- Update balance URL
- API key
### Authentication
Please include API key authentication on your server. Allow us to pass in the API key as a header
```sql
# we will send the following header on all relevant requests
x-api-key: YOUR_API_KEY
```
### Get loyalty point balance
- This endpoint will be called many times from our frontend, please ensure it is performant
- If we query for a wallet address which is not present in your system then please either don't include that wallet in the response array or ensure the balance value is set to `0`.
- For multi-currency setups, please include separate account balance objects in the array for each currency, and map your currency to the loyaltyCurrencyId provided in the snag admin.
```jsx
GET
Query parameters:
{
walletAddress: string | string[], // case-insensitive
startingAfter?: string // optional, only if pagination is required.
}
// no pagination
Status code: 2**
Response body:
{
"data": [{
"id": string // optional, only if pagination is required
walletAddress: string
amount: string // important: amount must be a string
loyaltyCurrencyId: string
}],
hasNextPage: boolean // if pagination is required
}
```
### Update loyalty point balance
- This endpoint should process all updates transactionally (all succeed, or all fail), or return a 400 error.
- It is the APIs responsibility to balance check prior to executing the balance updates.
- For multi-currency setups please specify a separate path for each currency.
- Example: if one wallet has insufficient balance, then return a 400 and a proper error message. We will surface the error message to the frontend.
```jsx
POST
Body parameters:
{
entries: [
{
walletAddress: string // case-insensitive
direction: 'debit' | 'credit'
amount: string // important: amount must be string.
idempotencyKey: string | undefined // external client responsible for saving only once
metadata: Json | undefined
}
],
description: "description of event",
loyaltyRuleId: uuid | undefined
}
Response:
Success:
Status code: 2**
Failure:
Satus code: 4** or 5**
Response body:
{
message: string
}
```
### Get loyalty account history
- Returns the list of balance changes for a wallet address
- This endpoint will be called many times from our frontend, so make sure it is performant and can handle the load
```jsx
GET
Query parameters:
{
walletAddress: string, // case-insensitive
// If passed in then filter by this value
// return only the most recent per each rule.
userCompletedLoyaltyRuleId: string | string[],
// id from previous request, optional, if pagination is required.
startingAfter?: string
// number of elements to return
limit?: int
}
// sorted by createdAt desc
Status code: 2**
Response body:
{
"data": [{
"id": string // optional, only if pagination is required
walletAddress: string
direction: 'debit' | 'credit'
// we need this to determine if rule was completed
idempotencyKey: string | undefined
metadata: Json | undefined
loyaltyRuleId: string | undefined
amount: string // important: amount must be a string
loyaltyCurrencyId: string
description: string
createdAt: datetime
}],
hasNextPage: boolean // if pagination is required
}
```
### API Structure & Guidelines for Loyalty Rules
We expect specific standards for an API endpoint that our loyalty rules interact with (e.g. to get off-chain data from your system that somehow impacts rule evaluation and/or rewards).
### Configuration
For our system to be able to call your endpoint(s) during rule execution we ask for the following:
- Endpoint URL
- API key
- Standard pagination & sorting parameters
---
## Leaderboards
*Build dynamic leaderboards with the Snag API. Fetch sorted user balances, paginate large datasets, and display rankings with profile metadata.*
import FeedbackForm from '/snippets/FeedbackForm.mdx'
## Overview
The [`GET /api/loyalty/accounts`](https://docs.snagsolutions.io/api-reference/loyalty/get-loyalty-accounts) endpoint enables you to fetch loyalty points balances for your users, which can be used to build features like a leaderboard in your application. With sorting and pagination options, the endpoint allows you to manage large datasets and customize the display order, making it easy to highlight the top users based on their points. Below is an example and detailed instructions for constructing a leaderboard.
## Steps to Build a Leaderboard
### Fetching the data
The [`GET /api/loyalty/accounts`](https://docs.snagsolutions.io/api-reference/loyalty/get-loyalty-accounts) endpoint allows for fetching of loyalty points balances for your users. This endpoints provides various filters to allow sorting, pagination and other filters.
- Note: if your loyalty program contains multiple loyalty currencies then use the `loyaltyCurrencyId` filter to query for the specific currency you wish to target.
### Sorting & pagination
The [`GET /api/loyalty/accounts`](https://docs.snagsolutions.io/api-reference/loyalty/get-loyalty-accounts) endpoint supports various query parameters that allow you to retrieve and manage data efficiently:
1. Sorting: Use the orderBy filter to sort users by their points. For example, setting orderBy[amount]=desc ensures users with the highest point balances appear at the top of your leaderboard.
2. Pagination:
- The limit filter determines how many entries are fetched per request. The default value is 10, but it supports up to 1,000 entries per call.
- If your dataset contains more than the specified limit (e.g., over 1,000 users), the API response will include a hasNextPage boolean property. When this is true, you’ll need to fetch additional pages.
- To fetch the next set of data, take the id of the last entry in your previous result and pass it to the startingAfter query filter in your next request.
### Example Request
Request
```
{
"organizationId": "your-organization-id",
"websiteId": "your-website-id",
"loyaltyCurrencyId": "your-loyalty-currency-id",
"sortDir": "desc",
"limit": 1000
}
```
- Note: for paginated requests be sure to add the `startingAfter` property to subsequent requests.
### Example Response
```
{
"data": [
{
"id": "0abcd123-abcd-1234-5678-abcd12345678",
"userId": "01234abc-abcd-1234-5678-abcd12345678",
"loyaltyCurrencyId": "your-loyalty-currency-id",
"amount": "1500",
"lockVersion": "10",
"organizationId": "your-organization-id",
"websiteId": "your-website-id",
"createdAt": "2024-11-26T17:21:29.688Z",
"updatedAt": "2024-11-27T17:44:35.881Z",
"user": {
"id": "01234abc-abcd-1234-5678-abcd12345678",
"walletAddress": "0x1...abc",
"userMetadata": [
{
"walletGroupIdentifier": null,
"twitterUser": "user1",
"discordUser": "user1",
"logoUrl": "https://...png",
"displayName": "user1"
}
]
}
},
{
"id": "abcd1234-abcd-1234-5678-abcd12345678",
"userId": "abcd1234-abcd-1234-5678-abcd12345678",
"loyaltyCurrencyId": "your-loyalty-currency-id",
"amount": "1250",
"lockVersion": "10",
"organizationId": "your-organization-id",
"websiteId": "your-website-id",
"createdAt": "2024-11-26T17:21:29.688Z",
"updatedAt": "2024-11-27T17:44:35.881Z",
"user": {
"id": "abcd1234-abcd-1234-5678-abcd12345678",
"walletAddress": "0x2...abc",
"userMetadata": [
{
"walletGroupIdentifier": null,
"twitterUser": "user2",
"discordUser": "user2",
"logoUrl": "https://...png",
"displayName": "user2"
}
]
}
}],
"hasNextPage": "false"
}
```
### Example Usage
With the above data, you can construct a leaderboard that highlights your top users. The amount field provides the points total, while the userMetadata object contains user details like their display name, social media profiles, and profile picture. Displaying these details can make the leaderboard more engaging and personalized.
## Accessing Full Leaderboard
If your organization has **Looker Studio** access enabled through Snag, you can view the **full leaderboard**, including all users and their loyalty point balances, directly within your Looker Studio dashboard.
If Looker Studio is **not yet set up** for your organization, [contact us](https://www.snagsolutions.io/contact) to request access or an extension of your existing Looker Studio dashboard. The Snag team can help you enable the integration and ensure your leaderboard data is automatically synced for full visibility.
## Summary
By leveraging the /api/loyalty/accounts endpoint, you can create a dynamic points leaderboard for your application. Sorting and pagination options ensure scalability, even with large datasets, while additional user metadata allows you to create a rich and engaging user experience.
## Related pages
Access Looker dashboards for detailed loyalty analytics.
Create achievement badges based on points and rule completion.
Configure user profile fields displayed on leaderboards.
Full REST API documentation for loyalty endpoints.
---
## Loyalty overview
*Set up your Web3 loyalty program with Snag. Configure currencies, create reward rules across 50+ rule types, and track user engagement onchain and offchain.*

## What is Snag loyalty?
Snag loyalty is a white-label loyalty solution which allows you to track, evaluate and reward user behaviour across on- and off-chain platforms (e.g. social media, holding tokens, custom smart contract interactions, etc).
This comes with an admin dashboard for configuration of your loyalty program, a REST API for integration with your existing system(s) and a no-code white-label Loyalty UI for your end users. Snag supports 50+ loyalty rule types across 10+ social platforms, 15+ blockchain networks, and integrations with DeFi protocols like [Uniswap](https://docs.uniswap.org/) and governance tools like [Snapshot](https://docs.snapshot.org/).
## Core concepts
- **Accounts:** A loyalty account represents one end-user identified by their wallet address.
- **Currencies:** A loyalty currency will be used to track loyalty points and rewards.
- **Rules:** A loyalty rule allows you to track a specific user behaviour and issue a reward in loyalty points.
- **Ledger:** The loyalty ledger is a chronological account of all loyalty points distribution and spending activity across your loyalty program.
## Admin dashboard
The admin dashboard is a web interface which allows for configuration of your loyalty program, management of loyalty rules and the end-user loyalty UI. We use wallet authentication for this dashboard and allow permissions to be issued to your team wallets (for collaborative management of your loyalty program).
- Access dashboard here: https://admin.snagsolutions.io
## Loyalty Program
To get started with your loyalty program, please contact us at [support@snagsolutions.io](mailto:support@snagsolutions.io).
## Related pages
Step-by-step guide to setting up your first loyalty rule.
Browse 50+ rule types for social, onchain, and custom engagement.
Let users redeem points for NFTs, auctions, and sweepstakes.
Build custom reward logic with serverless functions and relayers.
import FeedbackForm from '/snippets/FeedbackForm.mdx'
---
## Loyalty rule progress tracking
*Learn how to create external loyalty rules with progress tracking and multiple reward tiers*
External loyalty rules can be configured to track user progress and only allow completion when the progress reaches 100%. This feature enables you to create multi-step challenges or activities that users must complete incrementally.
## Overview
When you enable progress tracking on an external loyalty rule:
- Users can only claim the rule when their progress reaches 100%
- You can define multiple reward tiers at different progress levels
- Progress updates are submitted via the API
- The claim button only appears when progress is 100%
## Creating an External Rule with Progress Tracking
### Step 1: Enable Progress Tracking
When creating an external loyalty rule, you need to enable the **Track Progress** option:

1. Navigate to your loyalty rules section
2. Create a new external rule
3. Check the **Track Progress** checkbox
4. Configure your rule settings as needed
### Step 2: Configure Multiple Reward Tiers
With progress tracking enabled, you can define multiple reward ranges that trigger at different progress levels:

#### Reward Options
For each progress range, you can configure:
- **Points**: Award loyalty points at each milestone
- **Badges**: Enable the "Reward badge per range" toggle to award badges along with points
When "Reward badge per range" is enabled, users will receive both points and badges at each progress milestone they reach.

#### Example Reward Configuration
You can set up reward ranges like this:
- **10-10**: 20 points + badge (awarded at 10% progress)
- **20-20**: 20 points + badge (awarded at 20% progress)
- **50-90**: 20 points + badge (awarded when progress is between 50-90%)
- **100-100**: 20 points + badge (awarded at 100% progress)
This means users can earn up to 4 separate rewards (points + badges) as they progress through the activity.
## Updating User Progress
### API Integration
To update a user's progress, use the [Update Loyalty Rule Status](/api-reference/loyalty/update-loyalty-rule-status) endpoint:
[Update Loyalty Rule Status API Documentation](/api-reference/loyalty/update-loyalty-rule-status)
### Progress Values
- Progress values should be integers between 0-100
- When progress reaches 100%, the rule becomes claimable
- **Important**: Updating progress does not automatically reward users
- Users must go through the claim flow to receive rewards
## User Experience
### Progress Display
Users will see their current progress on the loyalty rule:

## Multiple Reward Tiers
### How Rewards Work
When you configure multiple progress ranges, users can earn rewards at different milestones:
1. **10% Progress**: First reward (20 points) - available for claim
2. **20% Progress**: Second reward (20 points) - available for claim
3. **50-90% Range**: Third reward (20 points) - available for claim when progress enters this range
4. **100% Progress**: Final reward (20 points) + rule completion - available for claim
**Note**: Progress updates only make rewards available for claiming. Users must actively claim their rewards through the [verification flow](/loyalty/verifying-rule-completion) to receive them.
After reward fulfillment (claim completion), the user's progress data is preserved for 6 days.
### Best Practices
- **Clear Milestones**: Use round numbers (10, 25, 50, 75, 100) for better user understanding
- **Meaningful Rewards**: Ensure each milestone provides value to encourage continued participation
- **Progress Feedback**: Provide clear feedback when users reach new milestones
- **Range Planning**: Consider user behavior when setting ranges (e.g., 50-90 allows for flexible completion)
## Implementation Steps
1. **Create External Rule**: Use the admin interface to create an external loyalty rule with progress tracking enabled
2. **Update Progress**: Use the [Update Loyalty Rule Status API](/api-reference/loyalty/update-loyalty-rule-status) to update user progress as they complete activities
3. **Enable Claiming**: When progress reaches 100%, users can claim the rule through the [verification flow](/loyalty/verifying-rule-completion)
## Troubleshooting
### Common Issues
1. **Progress not updating**: Ensure you're using the correct `loyaltyRuleId`
2. **Rewards not triggering**: Check that your progress ranges are correctly configured
3. **Claim button not appearing**: Verify progress has reached exactly 100%
### API Response
The update endpoint returns the current rule status:
```json
{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"websiteId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"organizationId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"userId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"loyaltyRuleId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"progress": 100,
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z"
}
```
---
## Manage rule-level points on your side
*Snag does not store or expose per-rule point totals. Learn how to track and query rule-level points in your own storage using webhooks and periodic sync.*
## Overview
Snag maintains an immutable loyalty ledger and accurate per-currency balances for each user. We do not store or expose the total points a user earned from a single loyalty rule ("rule-level points").
We don't provide any endpoint to fetch "points by rule" for one user or a list
of users. If you need rule-level totals, you should track and store them in
your own system.
What we do provide: a complete transaction stream and balance updates. You can
reliably derive per-rule totals by consuming transaction events and persisting
aggregates keyed by `(userId, loyaltyRuleId)` on your side.
## Recommended approach
At a high level, you should:
- Ingest new transactions in real time via subscriptions → webhooks
- Periodically reconcile using the transactions listing endpoint
- Maintain idempotent, append-only processing in your datastore
Enable a subscription for `LoyaltyTransactionEntry` and deliver events to your webhook endpoint. See [Subscriptions](/stratus/subscriptions) and [Webhooks](/stratus/webhooks).
`LoyaltyTransactionEntry` may not always include a `loyaltyRuleId` (e.g., manual adjustments). Skip entries without a rule when calculating rule-level totals.
Run a periodic job to fetch recent entries and backfill anything missed by
your webhook consumer. See [Get Loyalty Transaction
Entries](/api-reference/loyalty/get-loyalty-transaction-entries).
- Keep a lightweight aggregate keyed by `(userId, loyaltyRuleId)`
- Apply credits and debits accordingly
- Deduplicate using `entry.id` or `idempotencyKey`
## FAQ
No. Snag's source of truth is the ledger and account balances. Aggregations by rule are intentionally left to partner systems so you can shape them to your business logic and retention needs.
Use `entry.id` or `idempotencyKey` to deduplicate. Keep a simple record of
processed entries and make your updates idempotent.
Entries include a `direction` of `credit` or `debit`. Apply debits as negative
adjustments to your stored totals.
This is why the periodic reconciliation step exists. Use `startingAfter` with the last processed `entry.id` to fetch any missed entries and catch up exactly once.
## Related docs
- [Subscriptions](/stratus/subscriptions)
- [Webhooks](/stratus/webhooks)
- [Get loyalty transaction entries — API reference](/api-reference/loyalty/get-loyalty-transaction-entries)
- [Checking Rule Completion Status](/loyalty/check-rule-completion-status)
You now have a reliable pattern to calculate, store, and query rule-level
point totals using Snag's transaction stream.
---
## Managing social accounts
*This recipe explains how to use Snag social integration API to let your users connect their social accounts to their Snag profile.*
## Overview
This guide explains how to use Snag's social integration API to let your users connect their social accounts to their Snag profile. After creating a user in Snag (as explained in [Managing User Accounts](/loyalty/managing-user-accounts)), you can use the social authentication endpoints to connect various social platforms to that user's profile.
You must first create a user using the [Managing User
Accounts](/loyalty/managing-user-accounts) guide to obtain a `userId` before
connecting social accounts. The `userId` parameter required for all social
account connections comes from the user creation endpoint response.
## Supported Social Platforms
Snag supports connecting the following social platforms:
- Twitter
- Discord
- Telegram
- Epic Games
- Steam
- Google
- Tiktok
- Email
- Reddit
- Phone
## Connecting a Social Account
### Endpoint
```web/apps/docs/loyalty/managing-social-accounts.mdx
GET /api/{authType}/auth
```
Where `{authType}` is one of: `twitter`, `discord`, `telegram`, `epic`, `steam` , `google` , `Reddit` or `tiktok`.
### Query Parameters
| Parameter | Type | Required | Description |
| ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| userId | UUID | Yes | The ID of the user received from the [user creation endpoint](/loyalty/managing-user-accounts). This must be obtained by creating a user first. |
| websiteId | UUID | No | The ID of the website (if applicable) |
| redirect | String | Yes | The URL where the user will be redirected after completing authentication |
| responseType | String | No | The type of response to return (`redirect` or `json`). Defaults to `redirect` |
**Security Note regarding Redirect URIs**
To simplify integration, we allow unvalidated `redirect` URIs when using the API. This means you can set the redirect URL to any destination.
Since this endpoint is protected by your API key, only authorized requests can initiate the flow with a custom redirect. However, because the redirect URL can be set to anything, it is **your responsibility** to ensure that you do not include sensitive data in the URL parameters.
### Example Request
```
GET /api/twitter/auth?userId=123e4567-e89b-12d3-a456-426614174001&redirect=https://your-app.com/auth-callback
```
### Authentication Flow
1. Call the `/api/{authType}/auth` endpoint with the required parameters
2. The API will return a URL that you should redirect your user to
3. The user will authenticate with the social platform and grant permissions
4. After successful authentication, the user will be redirected to the URL specified in the `redirect` parameter
5. The social account is now connected to the user's Snag profile
## Connecting Email
Use this flow to associate and verify a user's email with their Snag profile.
### Endpoint
```
GET /api/email/auth
```
### Query Parameters
| Parameter | Type | Required | Description |
| ------------- | ------ | ----------- | ------------------------------------------------------------------------------------------------------ |
| emailAddress | String | Yes | The user's email address to verify. |
| redirect | String | No | URL to redirect the user to after verification. We append `status` and, on failure, optional `error`. |
| userId | UUID | Conditional | Required when calling server-to-server with an API key to target a specific user. |
| walletAddress | String | Conditional | Alternative to `userId` when calling server-to-server with an API key; creates/links a user by wallet. |
| websiteId | UUID | No | Your website identifier if applicable. |
### Headers
- `x-api-key`: Required.
### Flow
Call the endpoint with `emailAddress` (and optional `redirect`). This stores the pending email on the user's metadata and sends a verification email containing a secure link.
```bash cURL
curl -X GET 'https://admin.snagsolutions.io/api/email/auth' \
-H 'x-api-key: YOUR_SNAG_API_KEY' \
-G \
--data-urlencode 'emailAddress=user@example.com' \
--data-urlencode 'userId=123e4567-e89b-12d3-a456-426614174001' \
--data-urlencode 'redirect=https://your-app.com/email-verified'
```
```json Success
{ "message": "Verification email sent.", "userId": "123e4567-e89b-12d3-a456-426614174001" }
```
The email contains a link to `GET /api/email/auth/connect?token=...`. Snag
validates the token and verifies the email.
After verification, the user is redirected to your `redirect` URL (or a default verification page) with a status:
- `status=SUCCESS` on success
- `status=EXPIRED` if the link expired
- `status=INVALID` and `error=INVALID_CODE` for invalid tokens
Example: `https://your-app.com/email-verified?status=SUCCESS`
### Handling Email Account Conflicts
If the email is already verified for another user on the same website, the user is redirected to your `redirect` with `error=MAXIMUM_ACCOUNT_LINKED`. Handle this scenario the same way as described in [Handling Social Account Conflicts](#handling-social-account-conflicts).
## Connecting Phone
Use this flow to associate and verify a user's phone number with their Snag profile. Phone verification uses Twilio Verify to send an OTP code via SMS.
Phone numbers must be in [E.164
format](https://www.twilio.com/docs/glossary/what-e164) (e.g.,
`+14155552671`). This is the international phone number format required by
Twilio.
### Step 1: Send Verification Code
#### Endpoint
```
POST /api/phone/auth
```
#### Headers
- `x-api-key`: Required.
- `Content-Type`: `application/json`
#### Body Parameters
| Parameter | Type | Required | Description |
| ------------- | ------ | ----------- | --------------------------------------------------------------------------------- |
| phoneNumber | String | Yes | The phone number to verify in E.164 format (e.g., `+14155552671`). |
| userId | UUID | Conditional | Required when calling server-to-server with an API key to target a specific user. |
| walletAddress | String | Conditional | Alternative to `userId` when calling server-to-server with an API key. |
| websiteId | UUID | No | Your website identifier if applicable. |
### Step 2: Verify OTP Code
#### Endpoint
```
POST /api/phone/auth/connect
```
#### Headers
- `x-api-key`: Required.
- `Content-Type`: `application/json`
#### Body Parameters
| Parameter | Type | Required | Description |
| ------------- | ------ | ----------- | --------------------------------------------------------------------------------- |
| phoneNumber | String | Yes | The phone number being verified, in E.164 format. |
| code | String | Yes | The OTP code received via SMS. |
| userId | UUID | Conditional | Required when calling server-to-server with an API key to target a specific user. |
| walletAddress | String | Conditional | Alternative to `userId` when calling server-to-server with an API key. |
| websiteId | UUID | No | Your website identifier if applicable. |
### Flow
Call `POST /api/phone/auth` with the phone number. Snag sends an OTP code to the user via SMS using Twilio Verify.
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/phone/auth' \
-H 'x-api-key: YOUR_SNAG_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"phoneNumber": "+14155552671",
"userId": "123e4567-e89b-12d3-a456-426614174001"
}'
```
```json Success
{
"message": "Verification code sent.",
"userId": "123e4567-e89b-12d3-a456-426614174001"
}
```
The user receives a 6-digit OTP code via SMS on their phone.
Call `POST /api/phone/auth/connect` with the phone number and the OTP code. Snag verifies the code with Twilio and connects the phone number to the user's profile.
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/phone/auth/connect' \
-H 'x-api-key: YOUR_SNAG_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"phoneNumber": "+14155552671",
"code": "123456",
"userId": "123e4567-e89b-12d3-a456-426614174001"
}'
```
```json Success
{
"success": true,
"message": "Phone number verified."
}
```
### Disconnecting Phone
To disconnect a phone number from a user's profile:
```
POST /api/phone/auth/disconnect
```
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/phone/auth/disconnect' \
-H 'x-api-key: YOUR_SNAG_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"userId": "123e4567-e89b-12d3-a456-426614174001"
}'
```
```json Success
{
"message": "Phone disconnected."
}
```
### Handling Phone Account Conflicts
If the phone number is already verified for another user on the same website, the verify endpoint returns `error: 'MAXIMUM_ACCOUNT_LINKED'` in the response body:
```json
{
"success": false,
"error": "MAXIMUM_ACCOUNT_LINKED",
"message": "This phone number is already linked to another account."
}
```
Handle this scenario the same way as described in [Handling Social Account Conflicts](#handling-social-account-conflicts).
### Handling Social Account Conflicts
If the social account is already associated with a different user, the redirect URL will include two query parameters:
1. `error = 'MAXIMUM_ACCOUNT_LINKED'` - Indicates that the account is already linked to another user
2. `accountLinkData` - A JWT verification token that contains the necessary information to process the account transfer
In this case, you should:
1. Display a confirmation prompt to the user asking if they want to disconnect the account from the other user and link it to their current profile
2. If the user confirms, make a POST request to `/api/users/verify` with the following payload:
```json
{
"accountLinkData": "jwt_verification_token",
"userId": "current_user_id"
}
```
This will:
- Disconnect the social account from the previous user
- Connect it to the current user's profile
### Google OAuth YouTube Channel Errors
When using Google OAuth, if the user's YouTube channel does not exist, the API will return an error with `error = 'YOUTUBE_CHANNEL_NOT_FOUND'`. This typically happens when:
- The user has never created a YouTube channel
- The YouTube channel was deleted or suspended
- The user's Google account doesn't have YouTube access
To handle this error, you should:
1. Display a user-friendly message explaining that a YouTube channel is required
2. Prompt the user to create a YouTube channel first
3. Provide instructions on how to create a YouTube channel
4. Allow the user to retry the Google OAuth flow after creating their channel
YouTube channels are automatically created when users upload their first video
or customize their channel. Users can also manually create a channel by
visiting [youtube.com](https://youtube.com) and following the setup prompts.
### Google OAuth Requirements
When using Google OAuth integration, whether with Snag's default OAuth application or your own custom OAuth application, you need to ensure the following permissions (scopes) are configured:
- `https://www.googleapis.com/auth/youtube.readonly` - Required to read YouTube channel information
- `https://www.googleapis.com/auth/userinfo.email` - Required to access user email address
- `https://www.googleapis.com/auth/userinfo.profile` - Required to access user profile information
Google requires verification of OAuth applications that request sensitive scopes. If you're using your own custom OAuth application, you must complete the verification process in the Google Cloud Console before the OAuth flow will work properly. This verification process can take several days to complete.
Contact the Snag team if you need assistance with the Google OAuth verification process or if you're experiencing issues with Google authentication.
### Example Implementation with Conflict Handling
```javascript
// Server-side implementation (e.g., in Node.js/Express)
app.get('/auth-callback', async (req, res) => {
const { error, accountLinkData } = req.query
const userId = req.session.userId // Get from authenticated session
if (error === 'MAXIMUM_ACCOUNT_LINKED') {
// Render a confirmation page or return JSON response
return res.json({
requiresConfirmation: true,
accountLinkData,
})
}
if (error === 'YOUTUBE_CHANNEL_NOT_FOUND') {
// Handle YouTube channel not found error
return res.json({
error: 'YOUTUBE_CHANNEL_NOT_FOUND',
message:
'YouTube channel not found. Please create a YouTube channel first and then re-authenticate via Google OAuth.',
requiresChannelCreation: true,
})
}
// Handle successful connection
res.redirect('/profile')
})
// Handle confirmation
app.post('/confirm-account-transfer', async (req, res) => {
const { accountLinkData, userId } = req.body
try {
const response = await fetch(
'https://admin.snagsolutions.io/api/users/verify',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': `${process.env.SNAG_API_KEY}`, // API key stored securely on server
},
body: JSON.stringify({
accountLinkData,
userId,
}),
}
)
if (response.ok) {
res.redirect('/profile')
} else {
throw new Error('Failed to transfer account')
}
} catch (error) {
console.error('Error transferring account:', error)
res.status(500).send('Error transferring account')
}
})
```
### Example Tiktok Implementation
```javascript
// Server-side implementation (e.g., in Node.js/Express)
app.get('/connect-tiktok', async (req, res) => {
const userId = req.query.userId // Get from authenticated session
const redirectUrl = 'https://example.com/tiktok-redirect'
try {
// Step 1: Get state (JWT) from your server
// This initiates the TikTok OAuth flow and returns a JWT containing the verification code
const response = await fetch(
`https://admin.snagsolutions.io/api/tiktok/auth?userId=${userId}&redirect${redirect_uri}`,
{
headers: {
'x-api-key': `${process.env.SNAG_API_KEY}`, // API key stored securely on server
},
}
)
const data = await response.json()
// The response contains a `state` (JWT).
// Decode the JWT to extract the 8-digit code that the user must add to their TikTok bio.
res.json(data)
} catch (error) {
console.error('Error initiating TikTok connection:', error)
res.status(500).send('Error initiating TikTok connection')
}
})
app.get('/tiktok-redirect', async (req, res) => {
const { error, state, code } = req.query
if (error === 'INVALID_TIKTOK_CODE') {
// Step 1: Handle TikTok verification failure
// - show error to user
// - Log the failure for debugging
// - Optionally redirect to error page or show error message
}
// Step 2: If no error,
// - Call user endpoint
// - Verify the TikTok connection was successful (tiktokUser,tiktokUserId,tiktokVerifiedAt)
})
// Client-side code to initiate the flow
function decodeJwtForCode(stateJwt) {
if (!stateJwt || typeof stateJwt !== 'string') {
throw new Error('Invalid state JWT')
}
const decoded = jwt.decode(stateJwt)
if (decoded && typeof decoded === 'object' && 'generatedCode' in decoded) {
return decoded.generatedCode
}
throw new Error('generatedCode not found in JWT')
}
async function connectTiktokAccount(userId, tiktokProfileUrl) {
// Step 1: Get state (JWT) from your server
const response = await fetch(`/connect-tiktok?userId=${userId}`)
const { state } = await response.json()
// Decode JWT → extract the 8-digit code
const code = decodeJwtForCode(state)
// Show popup asking user to add code to TikTok bio
if (
window.confirm(
`Please add this code to your TikTok bio: ${code}\n\nClick OK once you have updated your bio.`
)
) {
// Step 2: Redirect user to TikTok connect with state and tiktokProfileUrl
// After verification, they'll be redirected back to your redirect URL
const connectUrl = `https://admin.snagsolutions.io/api/tiktok/auth/connect?state=${state}&tiktokProfileUrl=${encodeURIComponent(
tiktokProfileUrl
)}`
window.location.href = connectUrl
} else {
console.log('User cancelled TikTok bio update')
}
}
```
### Example Reddit Implementation
Reddit integration only supports JSON response type. The `redirect` parameter
and `responseType=redirect` are not supported for Reddit authentication.
```javascript
// Server-side implementation (e.g., in Node.js/Express)
app.get('/connect-reddit', async (req, res) => {
const userId = req.query.userId // Get from authenticated session
try {
// Step 1: Get state (JWT) from your server
// This initiates the Reddit verification flow and returns a JWT containing the verification code
const response = await fetch(
`https://admin.snagsolutions.io/api/reddit/auth?userId=${userId}&responseType=json`,
{
headers: {
'x-api-key': `${process.env.SNAG_API_KEY}`, // API key stored securely on server
},
}
)
const data = await response.json()
// The response contains a `state` (JWT).
// Decode the JWT to extract the 8-digit code that the user must add to their Reddit bio.
res.json(data)
} catch (error) {
console.error('Error initiating Reddit connection:', error)
res.status(500).send('Error initiating Reddit connection')
}
})
// Client-side code to initiate the flow
function decodeJwtForCode(stateJwt) {
if (!stateJwt || typeof stateJwt !== 'string') {
throw new Error('Invalid state JWT')
}
const decoded = jwt.decode(stateJwt)
if (decoded && typeof decoded === 'object' && 'generatedCode' in decoded) {
return decoded.generatedCode
}
throw new Error('generatedCode not found in JWT')
}
async function connectRedditAccount(userId, redditProfileUrl) {
// Step 1: Get state (JWT) from your server
const response = await fetch(`/connect-reddit?userId=${userId}`)
const { state } = await response.json()
// Decode JWT → extract the 8-digit code
const code = decodeJwtForCode(state)
// Show popup asking user to add code to Reddit bio
if (
window.confirm(
`Please add this code to your Reddit bio: ${code}\n\nClick OK once you have updated your bio.`
)
) {
// Step 2: Call Reddit connect API with state and redditProfileUrl
const connectUrl = `https://admin.snagsolutions.io/api/reddit/auth/connect?state=${state}&redditProfileUrl=${encodeURIComponent(
redditProfileUrl
)}`
const connectResponse = await fetch(connectUrl)
const result = await connectResponse.json()
if (result.success) {
alert('Reddit account connected successfully!')
} else {
alert(result.message || 'Failed to connect Reddit account')
}
} else {
console.log('User cancelled Reddit bio update')
}
}
```
### Example Implementation
```javascript
// Server-side implementation (e.g., in Node.js/Express)
app.get('/connect-twitter', async (req, res) => {
const userId = req.query.userId // Get from authenticated session
try {
const response = await fetch(
`https://admin.snagsolutions.io/api/twitter/auth?userId=${userId}&redirect=https://your-app.com/auth-callback`,
{
headers: {
'x-api-key': `${process.env.SNAG_API_KEY}`, // API key stored securely on server
},
}
)
const data = await response.json()
// Redirect the user to the authentication URL
res.redirect(data.url)
} catch (error) {
console.error('Error connecting Twitter account:', error)
res.status(500).send('Error connecting Twitter account')
}
})
// Client-side code to initiate the flow
function connectTwitterAccount(userId) {
// Redirect to your server endpoint
window.location.href = `/connect-twitter?userId=${userId}`
}
```
## Response Handling
### Success Response (200 OK)
```json
{
"url": "https://auth-provider.com/oauth/authorize?client_id=xxx&redirect_uri=xxx&state=xxx"
}
```
The `url` property contains the authentication URL that you should redirect your user to.
## Custom OAuth Applications
Snag allows you to use your own OAuth applications for **Twitter**, **Discord**, **Epic Games**, **Google**, and **Steam** integrations. This enables you to maintain your brand identity throughout the authentication flow and have more control over the user experience.
To configure your own OAuth credentials, navigate to the **Integrations** tab in your admin dashboard and enter your client ID and client secret for the desired platform. The changes take effect immediately for all new authentication requests.
You will also need to add the following redirect URLs to your OAuth application:
- `https://snag-render.com/api/twitter/auth/callback`
- `https://snag-render.com/api/discord/auth/callback`
- `https://snag-render.com/api/epic/auth/callback`
- `https://snag-render.com/api/steam/auth/callback`
- `https://snag-render.com/api/google/auth/callback`
### Google OAuth Application Setup
When configuring a custom Google OAuth application, ensure your OAuth consent screen includes the following required scopes:
- `https://www.googleapis.com/auth/youtube.readonly`
- `https://www.googleapis.com/auth/userinfo.email`
- `https://www.googleapis.com/auth/userinfo.profile`
Google requires verification of OAuth applications requesting sensitive
scopes. You must complete the verification process in the Google Cloud Console
before your custom OAuth application will work. The verification process
typically takes several days and may require additional documentation about
your application's use of these scopes.
## Troubleshooting
### 403 Forbidden Response
If you receive a `403 Forbidden` response when attempting to connect a social account, check the following:
Ensure you're including the `x-api-key` header in your request with a valid API key. The API key must be associated with your Snag account and have the necessary permissions.
```javascript
headers: {
'x-api-key': 'YOUR_SNAG_API_KEY'
}
```
Verify that the `userId` you're using:
- Was obtained from the [user creation endpoint](/loyalty/managing-user-accounts)
- Uses the `userId` field from the response, **not** the `id` field
- Is a valid UUID format
- Belongs to a user that exists in your Snag account
- Is associated with the same website/account as your API key
{' '}
Make sure to use `userId` from the user creation response, not `id`. Using
`id` instead of `userId` will result in a 403 Forbidden error.
If you haven't created the user yet, follow the [Managing User Accounts](/loyalty/managing-user-accounts) guide first.
Your API key may not have the required permissions to connect social accounts.
Contact the Snag team to verify your API key has the necessary scopes enabled.
If you're using a `websiteId` parameter, ensure it matches the website associated with your API key and the user's account. Mismatched website IDs can result in 403 errors.
## Next Steps
After connecting social accounts, you can use this information to enhance the user experience in your application and leverage it for loyalty program features.
---
## Managing user accounts
*This recipe explains how to create or update user accounts via the Snag API.*
## Overview
The [`POST /api/users/metadatas`](https://docs.snagsolutions.io/api-reference/identity/create-user-metadata) allows you to create or update user objects in Snag's system which can then be used to migrate users from your existing system. This also allows you to attach useful metadata to new and existing users, such as their social network handle, which can be used for a more seamless user experience in-app and for use in the loyalty program.
## Steps to create or update a user
### Example use-case
For the case where you are migrating your users from an existing system to Snag, you would loop over each of your users and, for each, hit the [`POST /api/users/metadatas`](https://docs.snagsolutions.io/api-reference/identity/create-user-metadata) endpoint with their wallet address and any metadata you wish to attach. Below is an example request which creates a user account for the given wallet address and attaches various social network handles.
### Example Request
```
{
"walletAddress": "0x1...",
"discordUser": "discord-user",
"twitterUser": "twitter-user",
"emailAddress": "email-address",
"telegramUsername": "telegram-user",
"displayName": "profile-name",
"logoUrl": "https://...png"
}
```
- Note: only the `walletAddress` property is required as this is the identifier for the user being created.
### Example Response
```
{
"id": "your-user-id",
"walletAddress": "0x1...",
"discordUser": "discord-user",
"twitterUser": "twitter-user",
"emailAddress": "email-address",
"telegramUsername": "telegram-user",
"displayName": "profile-name",
"logoUrl": "https://...png"
}
```
---
## Multi-wallet support
*Connect multiple wallets to aggregate points, earnings, and holdings to enhance your loyalty experience.*
## Overview
In blockchain-based loyalty programs, users often have multiple wallet addresses across different networks. Snag's multi-wallet support allows users to connect multiple wallets to a single user group, enabling:
- **Aggregated points display** - See combined point balances across all connected wallets
- **Group-based spending** - Spend points from the collective balance of all linked wallets
- **Pooled token holdings** - Combine NFT/token holdings for higher-tier multiplier qualification
- **Shared multiplier benefits** - Access the best multipliers earned by any wallet in the group
This feature creates a seamless experience for users with multiple wallets while maintaining individual tracking for each wallet address.
## How User Groups Work
### Points Aggregation
While Snag treats each wallet address as a separate user, connecting wallets through a user group enables points aggregation across all wallets in that group:
**Example:**
- User A has 100 points in their primary wallet
- User B has 300 points in their secondary wallet
- When connected in the same user group, both users will see a total of 400 points
### Shared Points Spending
All wallets in a user group share access to the combined point balance when redeeming rewards:
**Example:**
- User A (100 points) wants to redeem a reward costing 350 points
- Since User A's wallet is connected to User B's wallet (300 points) in a group
- User A can spend 350 points from the combined 400-point balance
- After redemption, the system intelligently deducts points from both wallets:
- User A's wallet: 0 points (depleted its 100 points)
- User B's wallet: 50 points (depleted 250 of its 300 points)
### Token Holdings Pooling for Multipliers
For multiplier rules based on token holdings, assets across all connected wallets are pooled to determine qualification level:
**Example:**
- A multiplier rule gives:
- 1.5x multiplier for holding 10-14 NFTs
- 2x multiplier for holding 15+ NFTs
- User A holds 10 NFTs and User B holds 5 NFTs
- When connected in the same group:
- Their combined holdings (15 NFTs) qualify for the 2x multiplier
- Both users receive the 2x multiplier benefit
**Important:** Token pooling only applies to multiplier-type rules, not to points-based token holding rules.
### Multiplier Selection Logic
When multiple wallets in a group earn different multipliers for the same rule type:
- The system selects the highest multiplier of each type
- Multipliers are not stacked within the same multiplier type
- Each multiplier type is counted once with the highest value available in the group
**Example:**
- User A has a 1.5x multiplier for completing Discord quests
- User B has a 2x multiplier for the same Discord quest type
- Both users will benefit from the 2x multiplier (the highest in the group)
## Setting Up Multi-wallet Support
### Technical Implementation
The multi-wallet functionality works through a `userGroupId` property in the user's account metadata, which connects different wallets to the same group.
### Enabling Multi-wallet Connections
To use multi-wallet functionality:
1. **Contact Support:** Multi-wallet support must be enabled by our team for your project
2. **Connection Limits:** Projects can be configured to allow:
- One additional wallet (total of 2 wallets in a group)
- Up to 3 additional wallets (total of 4 wallets in a group)
### User Experience
Once enabled, users can connect additional wallets to their account through the profile settings:
1. User signs in with their primary wallet
2. From the profile page, they select "Connect Additional Wallet"
3. User connects their additional wallet using standard wallet connection flow
4. The system verifies ownership and adds the wallet to the user group
5. Points and holdings are immediately aggregated across all wallets in the group
6. Each user in the group maintains their own User metadata that can be updated and tracked individually
### Disconnecting Wallets
Users can disconnect secondary wallets (additional wallets) from their account through profile settings:
- **Secondary wallets can be disconnected**: Users can remove additional wallets they've connected
- **Main wallet cannot be disconnected**: The primary wallet is the core account object and cannot be disconnected by users
- **Points transfer**: When disconnecting a secondary wallet, users can connect it to a new primary wallet, and the points from that wallet will transfer with it
The restriction on disconnecting the main wallet prevents users from gaming the leaderboard by rotating through wallets that share multipliers. If a user's main wallet is compromised, they should contact support for assistance transferring their balance to a new wallet.
## API Endpoints for Multi-wallet Support
### Updating User Metadata
Use the [`POST /api/users/metadatas`](https://docs.snagsolutions.io/api-reference/identity/create-user-metadata) endpoint to set or update user group connections. You can use either:
- `userGroupId` - Snag's internal identifier for the user group
- `userGroupExternalIdentifier` - Your custom external identifier for the group
Using `userGroupExternalIdentifier` allows you to maintain your own mapping between user groups and your external systems:
```json
{
"walletAddress": "0x123...",
"userGroupExternalIdentifier": "YOUR_SYSTEM_GROUP_ID_12345"
}
```
**Note:** When using `userGroupExternalIdentifier`, Snag will create a `userGroupId` internally and associate this external identifier with it. This allows you to reference user groups using your own identifiers in subsequent API calls.
### Fetching User Group Data
The [`GET /api/loyalty/accounts`](https://docs.snagsolutions.io/api-reference/loyalty/get-loyalty-accounts) endpoint allows you to fetch points balances and other data for all wallets in a group by providing either:
- `userGroupId` parameter for Snag's internal group ID
- `userGroupExternalIdentifier` parameter for your custom external identifier
## Best Practices
1. **Authenticate Wallet Ownership:** Always verify that a user owns all wallets they attempt to connect
2. **Clear User Communication:** Explain to users how points aggregation works across their connected wallets
3. **Visual Indicators:** Consider showing which wallet contributed what portion of the total points balance
## Limitations
- Maximum of 4 wallets per user group (your specific limit may be lower based on configuration)
- Token holding pooling only applies to multiplier rules, not to points-based token holding rules
- **Leaderboard Display:** Currently, leaderboards show each individual wallet as a separate entry.
## Related: Delegate.cash Integration
If your users want to keep valuable NFTs in cold storage while participating in your loyalty program, consider using delegate.cash instead of or alongside user groups.
Learn how delegate.cash provides one-way delegation from cold wallets to hot wallets for secure NFT verification.
---
## Multipliers
*Configure point multipliers to boost rewards for specific actions, time periods, or user segments.*
## Overview
Multipliers are a powerful feature in the loyalty system that allow you to amplify the points users earn. Unlike standard point rewards that grant a fixed number of points, multipliers increase the value of all point-based rewards a user receives, creating opportunities for enhanced engagement and more valuable rewards.
## Multipliers vs. Points
The loyalty system supports two primary reward types:
- **Points**: A straightforward reward where users receive a specific number of points in the designated currency upon completing a rule.
- **Multipliers**: A reward that increases the value of all point-based rewards a user receives.

## How Multipliers Work
When a user completes a rule configured with a multiplier reward, they receive a multiplier value (e.g., 2x, 3x, 4x) that will be applied to their future point earnings. Multipliers effectively act as point amplifiers, allowing users to earn points at an accelerated rate.
### Example
If a user has a 3x multiplier and completes an action worth 10 points, they will receive 30 points (10 × 3) instead of the base 10 points.
### Retroactive Multiplier Application
The loyalty system includes a flag that allows multipliers to be applied retroactively to a user's existing point balance. When you activate this toggle, please note that the selected multiplier will be applied retroactively to the user's existing balance. This means that the multiplier will increase the current balance by the specified factor, effectively multiplying the user's total rewards from previous wins. Ensure that this retroactive adjustment is communicated clearly to users.
For example, if a user has 500 points and earns a 2x multiplier with retroactive application enabled, their balance would immediately increase to 1,000 points.

### Excluding Rules from Multiplier Effects
For certain point-based rules, you can enable the "Skip Multiplier" flag. When this flag is turned on, points earned from that specific rule will not be multiplied by the user's multiplier value. This allows you to create baseline rewards that are always awarded at their face value, regardless of a user's accumulated multipliers.

## Multiplier Balance Logic
Users can accumulate multiple multipliers from different rules. When a user has multiple multipliers, they stack according to a specific balance logic:
1. The first multiplier is applied at full value
2. Each additional multiplier is reduced by 1 before being added to the total
### Formula
Total Multiplier = First Multiplier \+ Sum of (Additional Multipliers - 1)
### Example
If a user has the following multipliers:
- Hold BAYC NFT: 4x
- Hold PENGU NFT: 3x
- Hold ETH: 3x
- Have Discord Role Moderator: 2x
The total multiplier would be calculated as:
- First multiplier (4x) \+ (3x - 1) \+ (3x - 1) \+ (2x - 1)
- 4x \+ 2x \+ 2x \+ 1x = 9x
This means all point rewards for this user would be multiplied by 9.
## Multipliers and User Groups
When users connect multiple wallets through a user group, the system applies special logic for handling multipliers across the group. This ensures that all wallets in the group benefit from the highest available multipliers while maintaining a balanced reward economy.
### Multiplier Selection in User Groups
For each loyalty rule type, the system will:
1. Identify all multipliers earned by wallets in the same user group
2. Select the highest multiplier value for each rule type
3. Apply this highest multiplier to all wallets in the group
### Example
Consider a user group with two connected wallets:
- **Wallet A** has earned:
- 3x multiplier for holding BAYC NFT
- 2x multiplier for Discord role "Moderator"
- 1.5x multiplier for completing daily quests
- **Wallet B** has earned:
- 4x multiplier for holding BAYC NFT
- 1.5x multiplier for Discord role "Moderator"
- 2x multiplier for completing daily quests
The system will select the highest multiplier for each rule type:
- BAYC NFT holding: 4x (from Wallet B)
- Discord role "Moderator": 2x (from Wallet A)
- Daily quests: 2x (from Wallet B)
Both Wallet A and Wallet B will benefit from these highest multipliers, effectively:
- Both wallets get 4x multiplier for BAYC NFT holdings
- Both wallets get 2x multiplier for Discord role "Moderator"
- Both wallets get 2x multiplier for daily quests
Using the multiplier balance logic formula (First Multiplier \+ Sum of (Additional Multipliers - 1)):
- First multiplier (4x) \+ (2x - 1) \+ (2x - 1)
- 4x \+ 1x \+ 1x = 6x total multiplier
This means all point rewards for both wallets in the group would be multiplied by 6, unless the "Skip Multiplier" flag is enabled for specific rules.
### Important Notes
1. **No Stacking Within Groups**: Multipliers from different wallets in the same group do not stack. Only the highest multiplier for each rule type is applied.
2. **Individual Multiplier Tracking**: While the highest multiplier is applied to all wallets in the group, the system still tracks individual multiplier achievements for each wallet.
3. **Group Benefits**: This system ensures that all wallets in a group benefit from the best multipliers earned by any member, creating an incentive for users to maintain their connections within the group.
## Setting Up Multiplier Rewards
When creating a loyalty rule, you can select "Multiplier" as the reward type instead of "Points." You'll then need to specify the multiplier value (e.g., 2x, 3x, 4x) that users will receive upon completing the rule.
### Currency Selection for Multipliers

The loyalty system allows you to specify which currencies your multipliers will affect. Using a dropdown menu, you can select one or more currencies to which the multiplier will be applied. This gives you fine-grained control over your reward economy, as rules can be configured to reward points in different currencies, and multipliers can be targeted to specific currencies.
For example, you might create a multiplier that only applies to your "Premium Points" currency but not to your "Standard Points" currency, allowing for strategic differentiation in how users can accelerate their earnings across different point types.
### Rule Type Limitations
Note that multiplier rewards are not supported for all rule types. Specifically:
- Referral rules
- Smart contract event rules
These rule types only support point-based rewards.
## Strategic Uses for Multipliers
Multipliers can be strategically implemented to:
1. **Reward high-value actions**: Assign higher multipliers to actions that are more valuable to your platform
2. **Encourage collection ownership**: Reward users who hold specific NFTs or tokens with multipliers
3. **Create tiered loyalty systems**: Design a system where users can progressively increase their earning potential
4. **Drive engagement with key features**: Incentivize users to engage with important platform features
## Strategic Considerations
- Use multipliers sparingly for high-value actions to maintain their perceived value
- Clearly communicate to users how multipliers work and how they stack
- Balance your multiplier values to ensure the reward economy remains sustainable
## Direct Multiplier Rewards via API
In addition to rule-based multiplier rewards, you can also directly assign multipliers to specific users through the Create Loyalty Multiplier API. This allows for programmatic control over multiplier distribution from your own server, enabling custom reward logic or manual multiplier assignments.
Direct multiplier rewards are not available through the User Support interface
or admin UI. You must use the API endpoint to assign multipliers directly to
users.
To implement direct multiplier rewards, use the API endpoint documented at [Multiplier API Reference](/api-reference/loyalty/create-loyalty-multiplier).
This approach is particularly useful for:
- Custom reward campaigns not covered by standard rules
- Manual multiplier adjustments for specific users
- Integration with external systems or events
- Bulk multiplier assignments based on your own business logic
---
## Snag Template App
*A production-ready Next.js + Tailwind Web3 starter template with loyalty program and user profiles designed for Web3 projects*
import AiAssistantCallout from '/snippets/ai-assistant-callout.mdx'
Whether you're building a community rewards program, a gated Web3 experience, or a gamified platform, this template will get you up and running fast — fully integrated with the **Snag Solutions SDK**.
The starter kit is built with:
- **[Next.js](https://nextjs.org/docs) (App Router)** for modern, file-based routing
- **[Tailwind CSS](https://tailwindcss.com/docs)** for rapid styling
- **Snag Solutions SDK** for plug-and-play Web3 features
Explore the codebase, star the repo, and fork to start quickly. This is the canonical template repository.
## What’s Included
This template showcases a simple Web3 application with:
- **Web3 Authentication** (wallet-based login)
- **Loyalty Program & Leaderboard**
- **User Profile Page** with wallet info and loyalty point history
## Getting Started
### Prerequisites
Before you start, make sure you have:
- [Node.js](https://nodejs.org/)
- [PNPM](https://pnpm.io/)
### Installation
Clone this repository and install the dependencies:
```bash
pnpm install
```
### Environment Setup
Create a `.env` file in the project root with the following variables:
```env
NEXT_PUBLIC_API_KEY=your_api_key
NEXT_PUBLIC_WEBSITE_ID=your_website_uuid
NEXT_PUBLIC_ORGANIZATION_ID=your_organization_uuid
LOYALTY_CURRENCY_ID=your_loyalty_currency_id
ENABLE_TWITTER_RULES=boolean # Optional
ENABLE_TELEGRAM_RULES=boolean # Optional
RULES_COLLECTIONS='[{"address":"0x123","network":"mainnet"}]' # Optional
```
You can find or generate your API key and UUIDs via the [**Admin Dashboard**](https://admin.snagsolutions.io).
## Development
To start the development server:
```bash
pnpm dev
```
Visit http://localhost:3000 to see the app in action.
## Setup Scripts
### Add Initial Loyalty Rules
This sets up a base rule set for your website:
```bash
pnpm rules:create
```
> Tip: Make sure your loyalty currency is created first.
### Remove All Loyalty Rules
To clear all existing rules:
```bash
pnpm rules:remove
```
## Fork This Template
Want to create your own version of this template?
Create your own copy of the Next.js Template App to customize. After forking, follow the
Getting Started
section above.
## Project Structure
```bash
.
├── app/ # App Router pages
├── components/ # Reusable UI components
├── lib/ # Utility functions and SDK integrations
├── public/ # Static assets
├── scripts/ # Loyalty setup/teardown scripts
└── .env.example # Environment variable template
```
## Related Docs
- [Next.js App Router Docs](https://nextjs.org/docs/app/building-your-application/routing)
- [Tailwind CSS Docs](https://tailwindcss.com/docs)
---
## Program settings
*The Loyalty Builder tab in Snag's admin tool allows you to customize your loyalty program page, manage currencies and balances and configure advanced leaderboard settings. Use this tool to tailor your loyalty experience and drive deeper engagement with your community.*
This page contains three sections:
1. **Page Settings** - Customize the look and feel of your loyalty program.
2. **Currencies & Balances** - Manage loyalty program currencies and user balances.
3. **Advanced Settings** - Configure leaderboards and visibility options.
## Page Settings
This section lets you customize how your loyalty program appears to users.

- **Page Title & Description** - Set a name and short description for your loyalty program.
- **Page Background** - Upload a banner image to personalize the page.
Format: Upload a PNG or JPG. Recommended at least 2000 × 400.
- **Enable Loyalty First View** - Toggle this on to make the loyalty program the default landing page instead of the marketplace.
---
## Currencies & Balances
Manage the different currencies used in your loyalty program.

- **Add a New Currency** - Define custom reward types. You can hide, reorder, or remove currencies at any time.
- **Set Currency Symbols** - Assign unique identifiers for each currency.
- **Show in Header** - Choose up to 3 currencies to display at the top of your platform, appearing across all pages.
- **Reorder Currencies** - The top currency in the list is the primary leaderboard metric.
- **Update User Balances** - Manually adjust point distributions for individual users by uploading a CSV file.
Learn more in [Updating Balances](/loyalty/available-loyalty-rules).
---
## Advanced Settings
Expand your loyalty program with additional customization and control options.

- **Hide Social Links** - Toggle this to remove users' social links from the leaderboard page.
- **Leaderboard Views** - Create multiple leaderboard variations with custom filters.
- **Conditions** - Define rules for leaderboard inclusion.
---
## Quality Social Farming
*Spotlight and reward meaningful user-generated content by displaying posts from users who have completed relevant social quests.*
Quality Social Farming allows you to showcase and reward user-generated social content. The feed displays posts from users who have completed specific social quests, helping you highlight quality content and engage your community.
Quality Social Farming is a Pro plan feature. Ensure your plan has' ' Quality Social Farming
enabled.
## How Quality Social Farming works
- **Social Feed**: A curated feed of up to 1,000 top posts from users who completed relevant social quests
- **Automatic Ranking**: Posts are automatically ranked using AI based on engagement, quality, and relevance
- **Manual Ranking**: Admins can manually reorder posts by dragging and dropping, which locks them from automatic AI reordering
- **Filtering**: Filter posts by time period, rule type, X verified status, media content, and search terms
- **Individual Rewards**: Reward specific posts with loyalty points to incentivize quality content
- **Public Display**: Toggle to show the feed in the right column of your loyalty page
## How post quality is calculated
Posts are automatically analyzed using AI to determine their quality and sentiment. The quality calculation consists of two main components:
### Sentiment Score (1-100)
Each post receives a sentiment score from 1-100 that evaluates both positivity and content quality:
- **1-40**: Low-quality content including AI-generated spam, incentive farming, generic spam (vague praise, copy-paste content, irrelevant hashtags, engagement bait)
- **40-60**: Positive sentiment but low-effort content
- **75-100**: Positive sentiment with substantive, high-quality content
Posts must have a sentiment score of 75 or higher to be considered "quality posts" when the "Only reward quality posts" option is enabled on social quest rules.
### Content Quality Score (0-10)
The AI also calculates a content quality score (0-10) based on:
- Quality and depth of content
- Clarity and coherence
- Engagement potential
- Uniqueness and creativity
- Relevance to the quest topic
### Final Ranking Score
The final ranking score combines multiple factors with weighted importance:
- **Content Quality** (40%): Based on AI analysis of the post content
- **Likes** (15%): Number of likes received
- **Views** (15%): Number of views/impressions
- **Comments** (10%): Number of comments
- **Reposts** (10%): Number of reposts/shares
- **Followers** (10%): Author's follower count
- **Media** (0%): Currently not weighted in the calculation
This weighted scoring ensures that high-quality, engaging content ranks higher in the social feed, while still considering engagement metrics and author influence.
## Supported rule types
The social feed only displays posts from users who have completed these specific social quest rule types:
- [**Post on X**](/loyalty/rules/x-new-tweet) (`drip_x_new_tweet`) - Users create a new post on X, optionally with specific text
- [**Comment on an X Post with Text**](/loyalty/rules/x-text-in-comment) (`drip_x_text_in_comment`) - Users comment on any X post with specific text
- [**React to an X Post**](/loyalty/rules/x-post-reaction) (`drip_x_tweet`) - Users like, comment, or repost a specific X post
- **Post with Hashtag** (`twitter_post_hashtag`) - Users post on X containing a specific hashtag or mention
- **Comment on X Post** (`twitter_comment`) - Users comment on a specific X post
To populate your social feed, you need to create at least one of these rule types. Posts will only appear in the feed after users complete the corresponding quests.
## Configure the social feed
### Enable public feed display
To show the social feed on your loyalty page:
From the admin app, go to Loyalty → Quality Social Farming.
Enable the "Show social feed" toggle at the top of the page. This displays the top 1,000 posts in the right column of your loyalty page.
### Filter and search posts
The admin interface provides several filtering options:
- **Social Content Rules**: Filter by specific rule type or view all rules
- **Duration**: Filter by time period (Last 24 hours, 3 days, 7 days, 30 days, or All time)
- **More Filters**:
- **Contains media**: Show only posts with images or videos
- **X verified**: Show only posts from verified X accounts
- **Search**: Search posts by content, username, or other metadata
### Manual ranking
You can manually reorder posts to highlight specific content:
Clear any active filters (duration, rule type, search, etc.) to enable drag-and-drop ranking.
Click and hold the grip icon (⋮⋮) next to a post and drag it to your desired position.
The new rank is saved automatically. Note that manual ranking can take up to an hour to update on the public feed.
When you manually rank a post, it becomes locked and will not be automatically reordered by AI. Use "Unlock All" to clear all manual ranks and allow automatic AI reordering.
### Reward individual posts
You can reward specific posts to incentivize quality content:
Click on a post in the feed to view its details.
Enter the reward amount and select the loyalty currency.
The reward is processed immediately and credited to the user's account.
### Sort options
Posts can be sorted by:
- **Rank**: Default sorting by AI-calculated rank (shows top 10,000 posts with exact ranks)
- **Created at**: Sort by post creation date (newest or oldest first)
## API reference
### Get social posts
Fetch social posts with filtering and pagination options.
```bash cURL
curl -X GET 'https://admin.snagsolutions.io/api/loyalty/posts?websiteId=WEB_ID&organizationId=ORG_ID&limit=20&page=1&duration=all&sortBy=rank' \
-H 'X-API-KEY: YOUR_API_KEY'
```
**Query Parameters:**
- `websiteId` (required): Your website ID
- `organizationId` (required): Your organization ID
- `loyaltyRuleId` (optional): Filter by specific rule ID or omit to show all rules
- `duration` (optional): Time filter - `24h`, `3d`, `7d`, `30d`, or `all` (default)
- `xVerified` (optional): Filter for verified X accounts - `true` or `false`
- `containsMedia` (optional): Filter for posts with media - `true` or `false`
- `search` (optional): Search term to filter posts
- `sortBy` (optional): Sort order - `rank` (default) or `createdAt`
- `page` (optional): Page number for pagination (default: 1)
- `limit` (optional): Number of posts per page (default: 10, max: 1000)
- `isAdmin` (optional): Include admin-only fields - `true` or `false`
**Response:**
```json
{
"data": [
{
"id": "post-id",
"loyaltyRuleId": "rule-id",
"userId": "user-id",
"rank": 1,
"manualRank": null,
"createdAt": "2024-01-15T10:30:00Z",
"postContent": "User's post content...",
"postUrl": "https://x.com/user/status/123",
"xVerified": true,
"containsMedia": true,
"user": {
"walletAddress": "0x...",
"twitterUser": "username"
}
}
],
"hasNextPage": false,
"cursor": "next-page-cursor",
"stats": {
"hasManualRankedPosts": false
}
}
```
### Update post sort order
Manually set the rank of a post (locks it from AI reordering).
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/loyalty/posts/POST_ID' \
-H 'X-API-KEY: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"manualRank": 5,
"previousRank": 10
}'
```
**Body Parameters:**
- `manualRank` (required): The new manual rank position
- `previousRank` (optional): The previous rank if moving down in the list
### Reward a post
Reward a user for their social post.
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/loyalty/posts/POST_ID/reward' \
-H 'X-API-KEY: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"amount": "100",
"loyaltyCurrencyId": "currency-id"
}'
```
**Body Parameters:**
- `amount` (required): The reward amount as a string
- `loyaltyCurrencyId` (required): The ID of the loyalty currency to reward
### Unlock all posts
Clear all manual ranks to allow automatic AI reordering.
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/loyalty/posts?websiteId=WEB_ID&organizationId=ORG_ID&clearManualRanks=true' \
-H 'X-API-KEY: YOUR_API_KEY'
```
## Best practices
- **Create relevant quests**: Set up social quests that encourage meaningful content creation aligned with your community goals
- **Regular moderation**: Review and manually rank high-quality posts to highlight them in the feed
- **Reward quality**: Use individual post rewards to incentivize exceptional content
- **Monitor engagement**: Use filters to identify trending content and verified accounts
- **Balance automation**: Use manual ranking sparingly for truly exceptional content; let AI handle the majority of ranking
- **Clear communication**: Inform users about the social feed and how posts are selected
## Troubleshooting
- **No posts appearing**: Ensure you have created at least one supported social quest rule type and that users have completed those quests
- **Manual rank not updating**: Manual ranking changes can take up to an hour to reflect on the public feed
- **Posts not ranking correctly**: Check if posts have manual ranks set; use "Unlock All" to clear manual ranks and allow AI reordering
- **API errors**: Ensure your API key has the necessary permissions for `loyaltyPost` resources
---
## Quest chaining
*Create multi-step quest flows by chaining rules together with prerequisites and conditional logic.*
Quest chaining lets you combine multiple rules into a guided, multi-step experience. Users must complete prerequisite steps in sequence before they can complete and claim the final quest’s rewards. This is ideal for onboarding journeys, progressive learning paths, campaigns, or complex eligibility workflows.
## How quest chaining works
- **Chain**: A named container that links one "final" rule to one or more prerequisite conditions.
- **Final rule**: The quest users ultimately complete (and usually get the reward for). Represented by a `loyaltyRuleId`.
- **Conditions**: One or more requirements that must be fulfilled before the final rule can be completed. Conditions can reference other rules, counts, or other supported condition types.
- **Operator type**: Controls how multiple conditions are evaluated. Use `AND` (default) to require all conditions, or `OR` to require at least one condition.
- **Status**: You can fetch per-user (or per user group) progress across the chain to show locked/unlocked steps and completion.
Quest chaining is a feature flag. Ensure your plan has{' '}
Quest chaining enabled.
## Enable the feature
You need the `enableQuestChaining` feature flag enabled on your billing website.
- **UI**: Go to Admin → Billing → Plan features and enable "Quest chaining".
- **API/Provisioning**: Your website record should have `enableQuestChaining = true`.
If the feature is disabled, quest chain UI and APIs will be restricted.
## Typical use cases
- Onboarding series (e.g., connect wallet → join Discord → post on X → claim reward)
- Education paths (e.g., watch video → pass quiz → mint badge)
- Referral gating (e.g., referral reward releases only after the referee completes a quest)
- Seasonal campaigns with progressive unlocks
## Condition types
Quest chains support multiple condition types that define prerequisites for completing the final rule. You can mix and match different condition types within a single chain.
### Rule completion
Requires a user to complete a specific rule/quest before the final rule can be unlocked.
**Configuration:**
- Select a single rule that must be completed
- Optional: Set `repeatCount` to require the rule to be completed multiple times (e.g., complete 3 times)
**Example:** "Connect wallet" must be completed before unlocking "Join Discord"
### Multiple rule completions
Requires users to complete multiple rules, with flexible minimum requirements.
**Configuration:**
- Select multiple rules (e.g., 5 different quests)
- Set `requiredCount` to specify the minimum number that must be completed (e.g., complete at least 3 out of 5)
**Example:** Complete 3 out of 5 daily challenges to unlock the weekly bonus quest
### Achieve points
Requires a user to reach a specific points threshold in their loyalty account balance.
**Configuration:**
- Set `amount` to the minimum points required
**Example:** Achieve 1,000 points before unlocking the VIP badge quest
### Achieve badge
Requires a user to earn a specific badge before the final rule can be completed.
**Configuration:**
- Select a single badge that must be earned
**Example:** Earn the "Early Adopter" badge to unlock exclusive content quest
### Achieve multiple badges
Requires users to earn multiple badges, with flexible minimum requirements.
**Configuration:**
- Select multiple badges (e.g., 5 different badges)
- Set `requiredCount` to specify the minimum number that must be earned (e.g., earn at least 3 out of 5)
**Example:** Earn 3 out of 5 skill badges to unlock the "Master" certification quest
When using requiredCount for multiple rule completions or multiple badges, users can complete any combination of the selected items as long as the minimum count is met.
## Operator type
The `operatorType` field controls how multiple conditions in a chain are evaluated:
- **AND** (default): All conditions must be satisfied before the final rule can be completed. This is the most common use case for sequential quest flows.
- **OR**: At least one condition must be satisfied. Useful when you want to provide multiple paths to unlock the final quest.
**Examples:**
- **AND**: Complete "Connect wallet" AND "Join Discord" AND "Post on X" → unlock "Claim reward"
- **OR**: Complete “Connect wallet” OR “Join Discord” OR “Post on X” → unlock "Take quiz"
The operator type defaults to AND if not specified. This ensures backward compatibility with existing chains.
## Create a quest chain (UI)
From the admin app, navigate to Loyalty → Quest Chaining and click "Add new
chain".
Pick the quest that users will ultimately complete and receive rewards for.
Add one or more conditions. The most common is a Rule{' '}
condition pointing to another quest that must be completed first. Configure
the Operator type to control whether all conditions (AND)
or at least one condition (OR) must be satisfied.
The chain becomes active immediately once saved. You can edit or delete it
later.
### List
```bash cURL
curl -X GET 'https://admin.snagsolutions.io/api/loyalty/rule_chains?organizationId=ORG_ID&websiteId=WEB_ID&limit=10' \
-H 'X-API-KEY: YOUR_API_KEY'
```
### Fetch chain statuses
Get per-user or per user-group progress for a chain.
```bash cURL
curl -X GET 'https://admin.snagsolutions.io/api/loyalty/rule_chains/status?organizationId=ORG_ID&websiteId=WEB_ID&userId=USER_ID&loyaltyRuleChainId=CHAIN_ID' \
-H 'X-API-KEY: YOUR_API_KEY'
```
You must provide either userGroupId or userId{' '}
(single or array). Max 20 user IDs per request.
## Reward behavior and locking
- The final rule’s reward is typically locked until all prerequisite conditions are met.
- If a rule is part of a chain, internal reward handlers will suppress rewards until the chain is complete for that user.
### Referral-specific behavior
When you chain a referral rule to another quest, rewards for the referrer will only process after the referee completes the chained quest. This ensures referrals are only rewarded when the referee actually performs the intended action.
If quest chaining is enabled for a referral flow, rewards will not process
until the chained quest is completed by the referred user,
regardless of when the referral was recorded.
## Best practices
- Keep chains short and clear; show users exactly what’s required.
- Use descriptive chain names (shown in admin and analytics).
- Avoid circular dependencies; a rule should not indirectly depend on itself.
- Pair chains with badges or multipliers for meaningful progression.
- Monitor chain statuses to surface progress and nudge users.
## Troubleshooting
- **Final reward not unlocking**: Verify all prerequisite conditions are satisfied for the user and that the correct `loyaltyRuleId` is set on the chain.
- **Chain not visible**: Ensure `enableQuestChaining` is true for the website and your API key has permissions for Rule Chains.
- **Statuses endpoint returns empty**: Provide `userId` or `userGroupId` and check the API key/organization/website IDs.
- **Referral rewards not firing**: Confirm the chained quest is completed by the referee and that the referral rule is configured correctly.
---
## Rules configuration
*These fields are common across all loyalty rules and define the general properties of a rule.*

## Basic Details
- **Name & description**: Enter a unique name and a brief description for the rule.
- **Section**: Select where this rule will appear on the loyalty page.
- **Start & end time**: Select when the rule becomes active. It won't be shown before the start time. The end time is optional - if set, the rule will expire and disappear automatically.
- **Required rule**: Enable this to make the rule mandatory to complete before any other non-required rules can be completed.
- **Hide from UI**: Enable this to hide the rule from the loyalty page. Useful for background rewards or API-triggered flows.
## Contract Address Section
Only shown for rules that require it. Otherwise, these fields won’t appear.
- **Contract address**: Enter the smart contract address relevant to the rule you're creating, such as a token contract, collection contract or a contract tied to a specific event.
- **Network**: Select the blockchain network where the contract is deployed.
## Reward Section
### Type of Reward
Choose between Points, Multiplier, or Tokens as the reward mechanism for this rule.
1. **Reward**: Grants a fixed number of points when the rule is completed.
- **Points to reward**: Enter the amount and type of points the user will earn upon completing the rule.
2. **Multiplier Reward**: Multiply all future rewards the user earns from other rules.
- **Multiplier**: Enter the multiplier value applied to future rewards. This value must be greater than 1.
- **Currencies to apply**: Select which point types the multiplier will affect.
- **Apply multiplier to user’s existing balance**: Enable this to retroactively increase the user’s existing point balance based on the set multiplier.
3. **Tokens**: Pays an on-chain token (a custom ERC-20 or the chain native token) directly into the user’s wallet on completion. Requires the Pro plan and a funded relayer on a supported chain. Multipliers do not apply to token rewards. See [Token Rewards](/loyalty/token-rewards) for setup and requirements.
- **Update cadence**: Select how often we’ll validate user completion and update their balance.
- Immediately: Completion is checked as soon as the user performs the action (e.g. answering a quiz, clicking a link).
- Daily / Weekly / Monthly: Completion is checked at the end of each period and balances are updated if completed (e.g. Twitter follower rules).
- Note: Some rules have default cadences that can't be changed due to their logic.
- **User reward frequency**: Select how often a user can receive this reward.
- One time: The rule will disappear after the user completes it once.
- Daily / Weekly / Monthly: Users can complete the rule again in the next period.
- Daily resets at midnight UTC.
- Weekly resets at midnight on Monday.
- Monthly resets at midnight on the 1st of the month.
- Note: Some rules have default frequencies that can't be changed due to their logic.
## Custom Settings Section
Appears only for rules that require specific configurations. This section includes unique settings tailored to the rule type.
---
## Set up loyalty currency
*After you have enabled loyalty program on your admin page, you will need to create a loyalty currency.*
## Overview
Configuring a loyalty currency is the first step to creating a loyalty program. This is the currency that will be used to track loyalty points and rewards. Moreover, you can configure multiple loyalty currencies to your admin page.
1. Navigate to the Loyalty builder tab in the admin page.
2. Click on the "Currencies and Balances" section.
3. Click on the "Add new currency" button.
4. Fill in the required fields and click "Save".
Congratulations! 🎉
You have created a loyalty currency and can now create your first loyalty rule.
---
## Smart contract event rule
*This recipe explains what the smart contract event rule is, how it can be used to track complex on-chain user behaviour and includes a simple example for how to set it up.*
TODO: DF to revisit after changes to smart contract event rule are published week of 18th Nov 24
## Context
In blockchain-based systems, on-chain events serve as a dependable way to track user actions and interactions. These events can be used to capture activity across DeFi protocols, blockchain games, and other decentralized applications.
The Smart Contract Event Rule extends the Snag Loyalty rule engine, allowing you to create tailored triggers and rewards for specific events. This integration enables you to seamlessly incentivize user behavior and drive engagement within your ecosystem.
## Use cases
Here are some common scenarios where you can use the Smart Contract Event Rule:
- **Game Achievements:** Reward players who reach specific milestones in a blockchain-based game by tracking events emitted from your game’s contract.
- **DeFi Incentives:** Encourage users to provide liquidity or stake tokens by monitoring liquidity pool or staking contract events.
- **Cross-Chain Bridging Rewards:** Reward users for successfully bridging assets across chains by tracking events from bridging contracts.
These use cases showcase how you can leverage Snag Loyalty to build engaging rules that track complex on-chain user behavior.
## Example set-up
TODO: TBD (whats the best example here that we can come up with which may serve as a template for some of our clients?)
---
## Token Rewards
*Reward users with on-chain tokens — an ERC-20 or the chain native token — when they complete loyalty rules.*
## Overview
Token rewards let a loyalty rule pay an on-chain token amount directly into the user's wallet on rule completion. The token is either:
- A **custom ERC-20** — you pick the token contract (e.g. FLOW, USDC, your project token).
- The **chain native token** — ETH on Ethereum/Base/Arbitrum/Optimism, MATIC on Polygon, BNB on BNB Chain, FTM on Fantom.
Examples:
- A "mint NFT" rule that grants $50 in FLOW (ERC-20).
- A "10k impressions" rule that grants `0.01` ETH (native).
- A quest chain that grants $5 in USDC on the final step (ERC-20).
The reward type lives next to **Points**, **Multiplier**, and **Badge** in the loyalty rule form. Picking it surfaces a relayer + token picker.
## Requirements at a glance
Before a token-reward rule can pay out, all of these must hold:
| Requirement | Why |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Pro plan** | Token rewards are gated to Pro and above; rejected at create and at run time otherwise. |
| **A relayer on a supported chain** | Payouts go out from a relayer. Only chains in the table below are supported. |
| **For a custom token: the contract added on the relayer's chain** | The ERC-20 you pay out must be added as a contract on the same chain as the relayer. |
| **Relayer holds the reward token** | The relayer is the funding source — it must hold enough of the ERC-20 (or native token) to cover payouts. |
| **Relayer holds native gas** | Every payout is an on-chain transaction. The relayer needs the chain native token for gas, _on top of_ the reward balance. For native-token rewards the native balance must cover **both** the reward and the gas. |
## Multipliers do not apply to token rewards
Loyalty **multipliers** scale point-based earnings. They do **not** apply to token rewards. The amount you set on a token rule is the exact on-chain amount paid out; no multiplier inflates it. If you want a higher payout, set the higher amount directly on the rule.
## Supported chains
| Chain | Native token |
| ---------------- | ------------ |
| Ethereum mainnet | ETH |
| Optimism | ETH |
| BNB Chain | BNB |
| Polygon | MATIC |
| Fantom | FTM |
| Base | ETH |
| Arbitrum | ETH |
Picking a token reward filters the relayer and token pickers to these chains. A reward can't be configured on any other chain.
## Payout is asynchronous — it takes time
Token rewards are **on-chain**, so they are not instant. The user is credited the moment they complete the rule, but the actual transfer to their wallet is submitted shortly after as an on-chain transaction. The completion screen shows _"It may take a few minutes to appear in your wallet."_ — expect up to ~5 minutes under normal conditions, longer if the chain is congested or the relayer is low on gas.
Token rewards show up on leaderboards, in balances, and in reward history just like any other currency.
## Fractional amounts
You can reward fractional amounts (e.g. `0.5` USDC or `0.01` ETH). The amount you enter is the human value of the token — decimals are handled for you, so `0.5` pays out half a token, not half a smallest-unit.
## Configuration
In the rule form:
1. Pick **Tokens** in the reward-type toggle.
2. Pick a **Relayer** — filtered to relayers on a supported chain.
3. Pick a **Token** — either a custom contract (filtered to the relayer's chain) or the relayer chain's **native** token.
4. Set the amount per completion (`0.01` minimum). The form shows the relayer's live balance and the USD value of the entered amount, with a sufficient/insufficient indicator so you can confirm the relayer can fund the reward before saving.
---
## Updating & reseting balances
*This recipe explains how to manually update user loyalty points balances (i.e. not as part of a rule completion or spend action; an example use-case is to reset all user point balances to 0).*
import FeedbackForm from '/snippets/FeedbackForm.mdx'
## Overview
In some cases, you may need to manually update loyalty point balances outside the context of automated rules or spend actions. For example:
- Resetting all user balances to zero.
- Crediting or debiting points manually for a specific promotion or adjustment.
Snag provides two approaches for handling this: a no-code solution via the admin dashboard and a programmatic solution using the API.
---
## Approaches
### Admin Dashboard (recommended)
We provide a no-code solution for this via the admin dashboard. Note: this approach supports up to 250,000 balance changes per CSV upload.
#### Steps:
1. Go to the admin dashboard: https://admin.snagsolutions.io.
2. Navigate to the `Loyalty` page from the left-side menu.
3. Click `Update Balances` (this opens the modal for updating user balances and provides a CSV template for uploading balance changes).
4. Download and fill out the CSV template with the relevant balance changes.
5. Upload the completed CSV file in the modal.
6. Click `Add` to queue the balance changes. A rule will be created to represent the queued changes. If it does not appear immediately, refresh the page.
7. Verify execution:
- Click the eye icon on the created rule.
- Compare the `Rewards` count to the number of rows in your CSV file. If they match, execution is complete.
---
### API
For programmatic updates, use the endpoint to create a loyalty transaction and update balances.
#### Endpoint:
**Request Body:**
```json
{
"description": "An explanation for the manual balance adjustment",
"loyaltyCurrencyId": "string",
"entries": [
{
"walletAddress": "0x00",
"direction": "debit",
"amount": 0
},
{
"walletAddress": "0x01",
"direction": "credit",
"amount": 100
}
]
}
```
- `description`: Explanation for the adjustment (e.g., “Reset balances to zero”).
- `loyaltyCurrencyId`: The ID of the loyalty currency to update.
- `entries`: A list of transactions specifying:
- `walletAddress`: The wallet of the user whose balance is being adjusted.
- `direction`: Either credit to add points or debit to remove points.
- `amount`: The number of points to credit or debit.
**Example Response:**
```json
{
"code": 200,
"message": "Values updated successfully."
}
```
#### Steps
1. Prepare the JSON payload with your required adjustments.
2. Send a POST request to the endpoint with your payload.
3. Verify the response to ensure the transaction was processed successfully.
## Summary
For flexibility and simplicity:
- Use the Admin Dashboard for batch updates and non-technical workflows.
- Use the API for programmatic and highly customized updates.
Both approaches ensure your loyalty balances are kept up to date with minimal effort.
---
## Point Reset Options
Snag admins have access to a **Reset Balances** feature, which allows for resetting loyalty balances in different ways. This feature provides three reset options:
1. Reset all balances
- Reset all user balances to zero
- The currency itself, ledger data and existing rules remain unchanged
2. Reset all balances and ledger data
- Resets all user balances to zero and resets the ledger data
- The ledger reset is done by cloning the exisitng currency and all associated rules
3. Reset all balances, ledger data and rules
- Reset all user balances to zero
- Clones the currency and deletes all associated rules without recreating them
### Important Notes
- This feature is only available to **Snag admins**. If other users require access to this functionality, they should contact Snag support.
- Resetting balances cannot be undone, so proceed with cauting when using these options.
---
## Verifying rule completion
*This recipe explains how to submit and verify loyalty rule completion via the Snag API.*
## Prerequisites
Before submitting and verifying loyalty rule completion, you must have a user registered in Snag.
Depending on the social platform, you'll need the following user information:
- **Twitter**: User's Twitter handle and Twitter user ID
- **Telegram**: User's Telegram handle and Telegram user ID
- **Discord**: User's Discord handle and Discord user ID
For more information on user management, see:
- [Managing user accounts](/loyalty/managing-user-accounts)
## Overview
Use the Snag API to submit and verify loyalty rule completions from your own backend.
Because external verification to third-party services (e.g. Twitter, Telegram, etc.) may take up to a couple minutes,
we use an asynchronous process to verify rule completion.
> **Note:** The API-based verification process described in this recipe is only required for the following rule types:
>
> - Add an Item to Steam Wishlist
> - Add Text to X Bio
> - Add Text to X Username
> - Answer a Poll
> - Answer a Quiz
> - Become a GitHub Repository Collaborator
> - Check In
> - Click a Link
> - Comment on a Reddit Post
> - Comment on a YouTube Video
> - Comment on an X Post with Text
> - Complete Profile Details
> - Connect Discord
> - Connect Email
> - Connect Epic
> - Connect GitHub
> - Connect Instagram
> - Connect Phone
> - Connect Reddit
> - Connect Steam
> - Connect Telegram
> - Connect TikTok
> - Connect Wallet
> - Connect X
> - Connect Youtube
> - Enter a Code
> - External rule
> - Follow a TikTok Account
> - Follow an Instagram Account
> - Follow an X Account
> - Fork a GitHub Repository
> - Get Discord Role
> - Get X post impressions
> - Grant a Discord Role
> - Hold a Fungible Token
> - Hold an NFT (only for multiplier reward type) - this step is not required for Cadence based rules (e.g., Token Hold, Sold On ... etc), as these rules run automatically on the specified start time and on set cadence (daily, weekly, monthly).
> - Join Discord Server
> - Join Telegram Group
> - Points Airdrop
> - Post on Instagram
> - Post on TikTok
> - Post on X
> - React to an X Post
> - Reward dApps by Active Users
> - Reward dApps by Gas Spent
> - Reward dApps by New Users
> - Send Discord Messages
> - Send Telegram Messages
> - Spend in Shopify Store
> - Star a GitHub Repository
> - Stratus Function
> - Submit Text Input
> - Subscribe to a YouTube Channel
> - Swap a Token
Let's set up a rule to follow a specific Twitter account. The twitter handle or twitter user ID can be configured in the admin dashboard.
We can then use the loyalty rule completion endpoint to submit a request to verify the rule completion.
We can use the loyalty rule status endpoint to check the status of the rule completion request.
When verification is complete, our system then creates a loyalty transaction to reward the user.
The rule completion endpoint will return an error if re-called and the rule is already completed for the user.
Here is an example implementation in javascript on how you can use the following three endpoints to submit and verify loyalty rule completion:
- [`POST /api/loyalty/rules/{id}/complete`](https://docs.snagsolutions.io/api-reference/loyalty/complete-loyalty-rule)
- [`POST /api/loyalty/rules/status`](https://docs.snagsolutions.io/api-reference/loyalty/get-processing-status-and-progress-data-for-a-specific-user-on-loyalty-rules)
- [`GET /api/loyalty/transaction_entries`](https://docs.snagsolutions.io/api-reference/loyalty/get-loyalty-transaction-entries)
Note: `loyaltyRuleId` accepts a single ID or an array (up to 100). For larger sets, batch requests to avoid HTTP URL length limits.
```JavaScript
const yourServerBaseUrl = "YOUR_SERVER_BASE_URL"
const userId = "USER_ID"
const ruleId = "RULE_ID"
let isLoading = false
let success = false
// After the claim is successful, we store the transaction entries for the user in this array
let transactionEntries = []
async function postCommentOnTwitter() {
console.log("Posted comment on Twitter!")
}
async function callCompleteEndpoint() {
isLoading = true
try {
const response = await fetch(`${yourServerBaseUrl}/loyalty/complete`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, ruleId }),
})
console.log("Complete Endpoint Response:", await response.json())
pollStatus()
} catch (error) {
console.error("Error completing loyalty rule:", error)
isLoading = false
}
}
async function pollStatus() {
const statusUrl = `${yourServerBaseUrl}/loyalty/status?userId=${userId}`
let attempts = 0
async function poll() {
try {
const response = await fetch(statusUrl, {})
const statuses = await response.json()
console.log("Status Endpoint Response:", statuses)
const ruleStatus = statuses.find((item) => item.loyaltyRuleId === ruleId)
if (ruleStatus) {
if (ruleStatus.status === "completed") {
transactionEntries = await fetchTransactionEntries()
success = true
} else if (ruleStatus.status === "failed") {
success = false
} else if (attempts < 10) {
attempts++
setTimeout(poll, 1000)
} else {
success = false
}
} else if (attempts < 10) {
attempts++
setTimeout(poll, 1000)
} else {
success = false
}
} catch (error) {
console.error("Error polling status:", error)
success = false
} finally {
isLoading = false
button.textContent = "Claim"
}
}
poll()
}
async function fetchTransactionEntries() {
try {
const response = await fetch(
`${yourServerBaseUrl}/loyalty/transaction-entries?userId=${userId}&userCompletedLoyaltyRuleId=${ruleId}`,
{}
)
console.log("Transaction Entries Response:", await response.json())
} catch (error) {
console.error("Error fetching transaction entries:", error)
}
}
async function handleClaim() {
if (isLoading) return
await postCommentOnTwitter()
await callCompleteEndpoint()
}
```
---
## X rules logic
*Additional context for X (Twitter) rules logic*
We have two approaches to rewarding contributions on X (formerly Twitter) depending on partner requirements, and can support both out of the box:
### **Recommended Approach**
This approach is a X API ‘workaround’, allowing us to read the user’s completed actions without the additional cost associated with the X platform’s APIs and most of the functionality. Available logic includes:
1. Comment on specific X post.
2. Repost specific X post.
3. Follow X account for as long as the quest is live.
4. Use a specific phrase in a post or reply (ie. “Snag Solutions is 🐐’ed”).
5. Use a specific phrase in your bio.
6. Use a specific phrase in your username.
### **Requires X API**
This approach requires your X API key be inputted into your loyalty program builder by a Snag admin, and typically requires the X Pro plan which starts at \$5,000 to not miss any data. These quests query all actions associated with a specific rule vs. requiring a user to claim based on engaging with a specific post or account to capture all contributions. Available logic includes:
1. Repost on X.
2. Quote or Comment on an X post.
3. Like a post on X.
4. X post liked by project’s main account (Pacmoon style rewards):
- Add a multiplier for verified X accounts.
- Reward users based on the impression of their post.
5. Mention (@) or Hashtag (#) rewards (i.e 10 points / #winning / day).
---
## Customization
*Snag gives partners powerful tools to customize their platform's branding, layout, and theme—no code required. This guide walks through all available customization options to help you tailor your white-labeled experience to your brand and community.*
## Overview
The **Customization** tab is where you manage all visual and branding settings for your platform. It’s organized into three main sections:
1. **Platform Settings** - Define platform-wide identity like name, favicon and currency.
2. **Themes** - Set global colors, typography and custom CSS.
3. **Header** - Control your logo, home page, navigation links and social links.
---
## Platform Settings
These settings define your platform’s core identity and optional capabilities.

- **Platform Name**: Sets the name shown in the UI and browser.
- **Favicon**: Upload a 32x32 icon that appears in the browser tab.
- **Delegate Cash**: Toggle [Delegate.cash](http://Delegate.cash) support for secure delegated access.
- **Currency**: Select how prices are displayed - either in Crypto or Fiat.
---
## Themes
Set platform colors to align with your brand identity. Themes define the look and feel of your entire platform.

### Colors
Define your color system to ensure consistency across all UI components:
- **Primary Color**: Main brand color used for primary buttons and key highlights.
- **Secondary Color**: Applied to secondary buttons and container backgrounds.
- **Background Color**: Sets the default page background color.
- **Text Colors**: Define both **primary** and **secondary** text colors for clear readability.
- **Separator Color**: Used for lines, borders, and dividers between elements.
Make sure your colors have enough contrast for accessibility.
### Fonts
Choose a default font or upload your own to match your brand typography.
- You can **add multiple fonts**.
- Use **Custom CSS** to target specific elements or sections with your uploaded fonts.
### Custom CSS
Edit the `stylesheet.css` file to add advanced visual overrides or tweaks.
### Page Background
Upload a custom background image or texture - this will appear across **all pages**.
---
## Header
Use this section to personalize the top navigation area of your platform.

- **Header Logo**: Upload a PNG or JPG in any ratio. This will be shown in the top-left of your header.
- **Header Text**: Optional label (e.g. "Loyalty Program", "Ecosystem" or "Season 01").
- **Home Page**: Select the default landing page from your available pages.
- **Navigation Links**: Define custom nav items to guide users to key areas of your platform or to external pages.
- **Social Links**: Link social profiles by adding URLs for Twitter, Discord, Telegram and more.
---
## Hostname
*Configure a custom domain or hostname for your Snag-powered loyalty experience.*
### Activating your platform
Choose the domain name where you'd like to point your platform, e.g. `{yourwebsite}.com`.
Choose the subdomain for your platform, e.g. `loyalty.{yourwebsite}.com`.
Have a technical member on your team add a DNS record to your domain host. We will be adding a `CNAME record`.

Navigate to the **Hostname** section on admin.snagsolutions.io and input your new subdomain.
If you're having trouble with setting up your subdomain / CNAME, please refer to the following pages to resolve those issues:
- GoDaddy: https://www.godaddy.com/help/add-a-cname-record-19236
- Cloudflare: https://developers.cloudflare.com/dns/zone-setups/partial-setup/setup/
- NameCheap: https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain/
### Cloudflare Proxy Troubleshooting
If you're using Cloudflare and experiencing DNS resolution issues:
Sometimes you need to toggle the Cloudflare proxy off (or lower the TTL) to resolve DNS propagation issues. After turning the proxy back on, it should continue to work properly.
1. Log into your Cloudflare dashboard
2. Navigate to DNS settings for your domain
3. Find your CNAME record and click the orange cloud icon to disable the proxy
4. Wait for DNS propagation (usually 5-15 minutes)
1. Once DNS is working, click the gray cloud icon to re-enable the proxy
2. Your subdomain should now work with Cloudflare's proxy enabled
You can also try lowering the TTL (Time To Live) value in your DNS settings to speed up propagation.
---
Contact us if you have any questions or need additional help configuring your specific environment.
---
## Permissions
*Manage admin roles and team access by adding wallet addresses and setting permissions. Control who can manage your platform and receive important updates.*
## Overview
The **Permissions** tab allows you to manage team access and administrative roles for your platform. You can add team members by their wallet addresses, assign appropriate roles, and configure admin email notifications for important updates.

---
## Wallets
Add team members by adding their wallet addresses and assigning appropriate roles. This ensures only authorized users can access administrative functions.
### Adding Team Members
Enter the wallet address of the team member you want to add.
Select their role from the dropdown (Admin or Moderator).
Optionally add a display name for easier identification.
Click **Save** to apply changes.
### Role Types
**Admins** - Can manage user roles and permissions, view contracts, create token claim rules/pages (including ERC-20), and manage API keys. Each site can have up to 2 Admins.
**Moderators** - Can do everything else like posting in the feed, managing drops, updating pages, and handling content.
You can have a maximum of 2 website admins. If you need to add additional
admins beyond this limit, please [contact
Snag](https://www.snagsolutions.io/contact) for assistance.
---
## Admin Emails
Configure email addresses that will receive key platform updates and notifications.
These emails receive important updates like:
- Burned items notifications
- Collected addresses reports
- Other critical platform events
Admin emails are separate from wallet-based permissions and can be managed
independently.
---
## Best Practices
- **Start with Moderators**: Begin by adding team members as Moderators for most day-to-day tasks
- **Limit Admin Access**: Only promote trusted team members to Admin roles due to the 2-admin limit
- **Keep Emails Updated**: Ensure admin email addresses are current and monitored regularly
- **Use Display Names**: Add meaningful display names to make team management easier
---
## Set up overview
*Launch your Web3 loyalty platform with Snag. Follow the setup checklist: configure branding, install your hostname, build your program, and go live.*
import AiAssistantCallout from '/snippets/ai-assistant-callout.mdx'
Snag lets you launch your all-in-one branded engagement platform using our no-code tools. Use the Snag admin dashboard to create a loyalty program, set up a rewards shop, build custom pages and more.
This section walks through the key steps to go live, from setup to launch.
## Launch checklist
When you first open the Snag admin dashboard, whether from the “Get Started” button on our website or the top-right link here, you’ll be prompted to enter some basic info about your project. You can always update this later under **Customization → Platform Settings**.
Make it yours. Head to the **Customization** tab to set your theme colors, upload fonts and fine-tune your platform’s visual style. Learn more [here](/setup/customization).
Before going live, you’ll need to sign our partner agreement. You’ll find it on the **Snag Admin Home** page.
Launch under your own domain by following [these steps](/setup/hostname). This ensures your platform feels fully branded end to end.
Now you’re all set to start configuring your loyalty program with different types of rules. Begin by [creating your first loyalty rule →](/loyalty/create-your-first-rule)
We test constantly, but it’s still smart to run your own QA. A few key checks:
- Cross-device experience
- Cross-wallet compatibility
- Rule completion
Post it on X or wherever you connect with your community.
## Related pages
Set up your loyalty program with 50+ rule types.
Integrate Snag programmatically with the REST API.
Configure wallet, email, and social login providers.
Create NFT contracts and set up a rewards marketplace.
---
## User profiles
*The following will guide you through creating a user profile and the functionality available within profiles.*
### Overview
User profiles are a powerful tool allowing projects to learn more about their audience, and build a CRM to engage more customers. Snag user profiles are complete, and allow you to collect social profile connections, associate multiple wallets under one account, and associate an email address with accounts to send email announcements (also available via the Snag platform).
### Set up
On the admin side you can choose what profile elements you'd like your users to have.

### User side
**Edit Profile**
Once a user connects their wallet they can edit their profile by accessing the 'Edit Profile' section in the user dropdown menu:

**About**
In the 'About' section of their profile the user can add information about themselves.

**Accounts & Wallets**
Under 'Accounts & Wallets' the user can associate social profiles and other wallets to their Snag profile, as defined by your preferences during set up. Email connection is on by default.

**Notifications**
Users can connect their emails, and via admin.snagsolutions.io you can send them email announcements. Users connect their email under 'Accounts & Wallets' and can Opt-in to 'Email Announcements' under the 'Notifications' section.

---
## Write a Stratus function for loyalty rule completion
*Learn the input/output contract and build a handler that awards points or badges when a loyalty rule is completed.*
## Overview
Use a Stratus function to decide if a user should be rewarded for a specific loyalty rule and return the reward details. This page explains when your function runs, the required input and output shapes, validation rules, and includes a copy‑paste handler template.
If you're new to Stratus functions, read the [Function
Syntax](https://docs.snagsolutions.io/stratus/syntax) first for required
export patterns, allowed modules, and validation behavior.
## When your function runs
Your function executes during a loyalty rule completion attempt for rules of type `stratus_function`. The platform calls your function with a structured `input` object containing the current user, userGroupUsers and the `loyaltyRule` being completed. Your function must return a parsed output describing the reward to grant.
## Input contract
Your handler receives two parameters `(input, output)` and must accept the following input shape. `input` may be a stringified JSON; parse it if needed.
```javascript Input shape
{
user: {
id: string,
walletAddress: string,
walletType: string,
},
loyaltyRule: {
id: string,
name: string,
type: string,
amount: number,
metadata: object,
startTime: string,
endTime: string,
interval: string,
rewardType: string,
}
}
```
`input` is passed exactly as shown above by the completion API for
`stratus_function` rules.
## Output contract
Set the result to an array of one reward object and return `output.buildOutput()`.
- **walletAddress**: string (required) – Address to reward
- **amount**: number (required if awarding fixed points) – Points to grant
- **idempotencyKey**: string (optional) – Only pass if you want to override the idempotency key for the rule that we set for managing how often a user can be rewarded for the same rule.
- **data**: string (optional) – A JSON.stringify()-ed object with extra context; must be valid JSON and under 1KB
- **loyaltyBadgeId**: string UUID (optional) – Badge to award instead of points or in addition to range-based rules
- **loyaltyMultiplierAmount**: number (optional) – Multiplier for range-based rules
For now, only a single reward item is allowed. If you return more than one,
validation will fail.
### Validation rules enforced by the platform
- Exactly one `handler` export; must call `output.setResult(...)` and then `return output.buildOutput()`
- Result must be an array with at most one reward object
- `data` must be stringified JSON and < 1KB
- Allowed modules: `axios`, `lodash`, `viem` and `viem/chains` only (see [Function Syntax](https://docs.snagsolutions.io/stratus/syntax))
## Copy‑paste handler template
Below is a ready‑to‑use template based on the attached snippet. Replace the eligibility logic and values with your own.
```javascript loyalty-rule.js
module.exports.handler = async (input, output) => {
try {
// Parse input if it was passed as a string
let parsed = typeof input === 'string' ? JSON.parse(input) : input
parsed = typeof parsed === 'string' ? JSON.parse(parsed) : parsed;
parsed = typeof parsed === 'string' ? JSON.parse(parsed) : parsed;
/**
* input = {
* user: { id, walletAddress, walletType },
* userGroupUsers?: { id, walletAddress, walletType }[],
* loyaltyRule: { id, name, type, amount?, ... }
* }
*/
const { user, loyaltyRule } = parsed
// Compute reward details
const amount = 2 // Example fixed points
const idempotencyKey = `rule:${loyaltyRule.id}:user:${user.id}` // Replace with a transaction hash or another unique key
// Optional structured context under 1KB
const data = JSON.stringify({
reason: 'eligible via custom function',
ruleType: loyaltyRule.type,
})
// Set result (single reward item only)
output.setResult([
{
walletAddress: user.walletAddress,
amount,
idempotencyKey,
data,
// loyaltyBadgeId: '00000000-0000-0000-0000-000000000000',
// loyaltyMultiplierAmount: 1.25,
},
])
return output.buildOutput()
} catch (error) {
console.error('Error in user code:', error)
throw error
}
}
```
## How to add and test your function
- Go to Admin → Loyalty → Rules
- Click "Create Rule" or edit an existing rule
- Set the rule type to `stratus_function` - In the function editor within the
rule form, paste the handler template and customize it - Save the rule; no
separate Stratus function is required
- From your site or via API, attempt to complete the rule for a user
- The platform invokes your function with the input shown above
- If your function returns a valid reward, the user is queued for fulfillment
Verify that your function run succeeded and a reward was recorded for the user.
## Troubleshooting
Return exactly one reward object in the array passed to `output.setResult`.
Ensure `data` is produced by `JSON.stringify({ ... })` and the string length is under 1024 characters.
Confirm your handler sets a result and returns `output.buildOutput()` as the
final statement.
Import only `axios`, `lodash`, `viem`, or `viem/chains`. Remove Node built-ins like `fs`.
## See also
- [Function Syntax](https://docs.snagsolutions.io/stratus/syntax)
- [Function Templates](https://docs.snagsolutions.io/stratus/function-templates)
- Loyalty rule API reference for response data: `https://docs.snagsolutions.io/api-reference/loyalty/get-loyalty-rules#response-data`
---
## Function templates
*Pre-built Stratus function templates for common use cases like airdrops and token gating.*
## Overview
Check out the [Function Syntax](https://docs.snagsolutions.io/stratus/syntax) page for more information on how to create and validate Stratus functions and the [Overview](https://docs.snagsolutions.io/stratus/functions) page before you start..
These templates are starting points. You should modify them based on your specific requirements and add appropriate error handling.
Here are some common function templates you can use as starting points for your Stratus functions. Simply copy the code and replace the placeholder values with your specific parameters.
## Track Relay.Link Bridge Activity
This function rewards users for bridging assets through [relay.link](https://relay.link). Replace the constants in the first lines of the function with your actual values.
There are two core steps to this function both of which can exist as part of any function:
1. It tracks all incoming transfers to the `DESTINATION_CHAIN_ID`.
2. It uses the Coingecko API for price tracking. It requires a minimum of 1\$ worth of coins to be bridged over and the dollar amount is multiplied by the REWARD_MULTIPLIER variable.
### Function Code
```javascript bridge-rewards.js
const axios = require('axios')
/**
* Required consts for the function
*/
const LOYALTY_API_KEY = 'REPLACE_WITH_YOUR_API_KEY'
const LOYALTY_RULE_ID = 'REPLACE_WITH_YOUR_LOYALTY_RULE_ID'
const REWARD_MULTIPLIER = '1' // USD value will be multiplied by 1
const DESTINATION_CHAIN_ID = 'REPLACE_WITH_CHAINID'
const GAS_TOKEN_ID = 'eth' // replace with the gas token ID of the destination chain
/**
* Additional logger functionality
*/
const logs = []
const errors = []
const log = Object.assign(
(message) => {
logs.push(message)
},
{
error: (errorMessage) => {
errors.push(errorMessage)
},
logs,
errors,
}
)
/**
* Get reward amount
*/
function rewardAmount(amount) {
const number = parseFloat(amount)
if (isNaN(number) || number < 1) return 0
return Math.floor(number * REWARD_MULTIPLIER)
}
/**
* Mark loyalty rule as completed to reward user
*/
async function completeLoyaltyRule(reward, log) {
try {
if (!LOYALTY_API_KEY) {
throw new Error('LOYALTY_API_KEY is not defined')
}
await axios.post(
`https://admin.snagsolutions.io/api/loyalty/rules/${LOYALTY_RULE_ID}/complete`,
{
verifyOnly: 'false',
amount: reward.reward,
walletAddress: reward.walletAddress,
idempotencyKey: reward.txHash,
},
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': LOYALTY_API_KEY,
},
}
)
log(
`${reward.walletAddress} rewarded with ${reward.reward} points for $${reward.amount} transaction, rule ${LOYALTY_RULE_ID}`
)
} catch (error) {
log.error(`Failed for ${reward.walletAddress}: ${error.message}`)
}
}
/**
* Get recent relay requests
*/
async function getRelayRequests() {
const limit = 20
const endTimestamp = Math.floor(Date.now() / 1000)
const startTimestamp = endTimestamp - 10 * 60
const allResults = []
let continuation = null
try {
if (!DESTINATION_CHAIN_ID) {
throw new Error('DESTINATION_CHAIN_ID is not defined')
}
do {
let baseUrl = 'https://api.relay.link/requests/v2'
const queryParams = [
`sortBy=createdAt`,
`destinationChainId=${DESTINATION_CHAIN_ID}`,
`startTimestamp=${startTimestamp}`,
`endTimestamp=${endTimestamp}`,
`limit=${limit}`,
]
if (continuation) {
queryParams.push(`continuation=${encodeURIComponent(continuation)}`)
}
const url = `${baseUrl}?${queryParams.join('&')}`
const response = await axios.get(url)
const data = response.data
if (Array.isArray(data.requests)) {
allResults.push(...data.requests)
}
continuation = data.continuation
} while (continuation)
return allResults
} catch (error) {
log.error(`[getRelayRequests] Error fetching relay requests`)
log.error(error)
return []
}
}
/**
* Get recent relay rewards
*/
async function getRecentRewards() {
const txArray = await getRelayRequests()
const rewards = []
try {
if (!txArray || txArray.length === 0) {
log('No new rewards found')
return rewards
}
for (const tx of txArray) {
if (tx?.status === 'success' && tx?.user) {
const currencyInChainId =
tx?.data?.metadata?.currencyIn?.currency?.chainId
if (currencyInChainId === DESTINATION_CHAIN_ID) {
continue
}
const amountUsd =
tx?.data?.metadata?.currencyOut?.amountUsdCurrent ||
tx?.data?.metadata?.currencyIn?.amountUsdCurrent ||
0
if (!amountUsd) continue
const currency = tx?.data?.currency
if (!currency || !['usdc', GAS_TOKEN_ID].includes(currency)) continue
const txHash = tx?.data?.outTxs?.[0]?.hash
const amountToReward = rewardAmount(amountUsd)
if (amountToReward > 0) {
rewards.push({
walletAddress: tx.recipient,
amount: amountUsd,
reward: amountToReward,
txHash: txHash,
})
}
}
}
} catch (e) {
log.error(`[getRecentRewards] Error fetching relay requests`)
log.error(e)
}
return rewards
}
/**
* Reward users
*/
async function rewardUsers(rewards, log) {
for (const reward of rewards) {
await completeLoyaltyRule(reward, log)
}
}
module.exports.handler = async (input, output) => {
try {
const rewards = await getRecentRewards()
if (rewards.length === 0) {
log('No rewards found')
} else {
log(`Found ${rewards.length} rewards`)
await rewardUsers(rewards, log)
}
// Set result
output.setResult({
logs,
rewards,
errors,
})
return output.buildOutput()
} catch (error) {
log.error('Error in user code:', error)
throw error
}
}
```
## Transaction Entries On-Chain
Some projects prefer to host points onchain to generate transaction volume and decentralize points data. This function listens to transaction entries and mints/burns tokens based on account balance changes. It can be adjusted to any token contract.
1. Click `Add New Function`
2. In the `Subscription` field, either select an existing Snag Subscription, or create a new one, as in the image below.

3. Finally, replace the `LOYALTY_CURRENCY_ID` with your loyalty currency ID and the `ERC20_CONTRACT_ADDRESS` with your token contract address in the first lines.
4. Click `Save` and you're good to go.
### Function Code
```javascript mint-burn.js
const { encodeFunctionData } = require('viem')
// Replace with your contract address
const ERC20_CONTRACT_ADDRESS = '0x0000000000000000000000000000000000000000'
// Replace with your loyalty currency ID
const LOYALTY_CURRENCY_ID = 'YOUR_LOYALTY_CURRENCY_ID'
module.exports.handler = async (input, output) => {
let operations = []
// Parse input if it's a string
if (typeof input === 'string') {
try {
let parsed = JSON.parse(input)
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed)
}
input = parsed
} catch (err) {
output.setResult({
message: 'Invalid JSON input',
error: err,
input: input,
operations,
})
return output.buildOutput()
}
}
if (!Array.isArray(input)) {
output.setResult({
message: 'Expected input to be an array of events',
input: input,
operations,
})
return output.buildOutput()
}
const mintABI = [
{
inputs: [
{ internalType: 'address', name: 'to', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'mint',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
]
const burnABI = [
{
inputs: [
{ internalType: 'address', name: 'from', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'burn',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
]
try {
for (const event of input) {
if (!event.data) continue
const dataItem = event.data
const tokenAmount = BigInt(dataItem.amount) * BigInt(10) ** BigInt(18)
// Replace with your loyalty currency ID
if (dataItem.loyaltyCurrencyId !== LOYALTY_CURRENCY_ID) {
continue
}
let operationType = null
if (dataItem.direction === 'credit') {
operationType = 'mint'
} else if (dataItem.direction === 'debit') {
operationType = 'burn'
} else {
continue
}
const walletAddress = dataItem.loyaltyAccount?.user?.walletAddress
if (!walletAddress) continue
let data
if (operationType === 'mint') {
data = encodeFunctionData({
abi: mintABI,
functionName: 'mint',
args: [walletAddress, tokenAmount.toString()],
})
} else if (operationType === 'burn') {
data = encodeFunctionData({
abi: burnABI,
functionName: 'burn',
args: [walletAddress, tokenAmount.toString()],
})
}
output.addTransaction({
to: ERC20_CONTRACT_ADDRESS,
data: data,
value: '0x0',
})
}
output.setResult({
message: 'Token transactions created.',
operations,
})
return output.buildOutput()
} catch (error) {
console.error('Error in user code:', error)
throw error
}
}
```
## DEX Trading Rewards
This function tracks and rewards users based on their trading volume, using Redis for persistence. It is based on a subscription to a USDC based uniswap pool, tracking the `Swap` event.
Replace the constants in the first lines of the function with your actual values.
### Function Code
```javascript trading-rewards.js
const { Redis } = require('ioredis')
const axios = require('axios')
const LOYALTY_RULE_ID = 'YOUR_LOYALTY_RULE_ID'
const LOYALTY_API_KEY = 'YOUR_API_KEY'
const REDIS_URL = 'YOUR_REDIS_URL'
const VOLUME_THRESHOLD = 10 // $10 USD threshold
const redis = new Redis(REDIS_URL)
/**
* Mark loyalty rule as completed to reward user
*/
async function completeLoyaltyRule(walletAddress) {
if (!LOYALTY_API_KEY) {
throw new Error('LOYALTY_API_KEY is not defined')
}
await axios.post(
`https://admin.snagsolutions.io/api/loyalty/rules/${LOYALTY_RULE_ID}/complete`,
{
verifyOnly: 'false',
walletAddress: walletAddress,
},
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': LOYALTY_API_KEY,
},
}
)
}
module.exports.handler = async (input, output) => {
const operations = []
try {
operations.push('Starting handler execution: parsing input')
if (typeof input === 'string') {
let parsed = JSON.parse(input)
if (typeof parsed === 'string') parsed = JSON.parse(parsed)
input = parsed
}
if (!Array.isArray(input) || input.length === 0) {
output.setResult({ message: 'No event data provided', input, operations })
return output.buildOutput()
}
const event = input[0]
if (!event.decodedEvent || !event.decodedEvent.args) {
throw new Error('Event data missing decodedEvent.args')
}
const args = event.decodedEvent.args
// Process swap events (assuming token0 is USDC with 6 decimals)
if (args.hasOwnProperty('amount0')) {
const { sender, amount0 } = args
const traderWallet = sender
const tradeDollarValue = Math.abs(Number(amount0)) / 1e6
const redisVolumeKey = `${traderWallet}-swapVolume`
const redisRewardedKey = `${traderWallet}-swapRewarded`
const alreadyRewarded = await redis.get(redisRewardedKey)
if (alreadyRewarded) {
output.setResult({
message: `User ${traderWallet} has already been rewarded for swap volume.`,
operations,
})
return output.buildOutput()
}
const storedVolumeStr = await redis.get(redisVolumeKey)
const storedVolume = storedVolumeStr ? parseFloat(storedVolumeStr) : 0
const newVolume = storedVolume + tradeDollarValue
await redis.set(redisVolumeKey, newVolume.toString())
if (storedVolume < redisRewardedKey && newVolume >= VOLUME_THRESHOLD) {
// Complete loyalty rule when threshold is reached
await completeLoyaltyRule(traderWallet)
await redis.set(redisRewardedKey, 'true')
output.setResult({
message: `User ${traderWallet} rewarded for reaching $${VOLUME_THRESHOLD} swap volume.`,
cumulativeSwapVolume: newVolume,
operations,
})
return output.buildOutput()
}
output.setResult({
message: `User ${traderWallet} cumulative swap volume updated to $${newVolume.toFixed(2)}.`,
cumulativeSwapVolume: newVolume,
operations,
})
return output.buildOutput()
}
output.setResult({ message: 'Unknown event type', operations })
return output.buildOutput()
} catch (error) {
operations.push(`Error encountered: ${error.message}`)
console.error('Error in handler:', error)
output.setError(error.message)
output.setResult({ operations })
return output.buildOutput()
}
}
```
## Usage Notes
1. Replace placeholder values (marked with `YOUR_*`) with your specific parameters
2. Update Redis URLs with your instance details
3. Adjust reward multipliers and thresholds as needed
4. Test thoroughly in development before deploying to production
Remember to handle your API keys and sensitive data securely. Never commit them directly in your code.
## Common Parameters to Replace
- `LOYALTY_RULE_ID`: Your specific loyalty rule identifier
- `REDIS_URL`: Your Redis instance connection string
- `ERC20_CONTRACT_ADDRESS`: The address of your token contract
- `VOLUME_THRESHOLD`: Minimum volume required for rewards
- `MULTIPLIER`: Reward calculation multiplier
These templates are starting points. You should modify them based on your specific requirements and add appropriate error handling.
---
## Functions
*Write and deploy serverless JavaScript functions on Snag Stratus. Schedule via cron, trigger via webhooks, or chain to onchain subscriptions for custom reward logic.*
import AiAssistantCallout from '/snippets/ai-assistant-callout.mdx'
## Overview
Stratus functions enable developers to write and execute JavaScript functions in a serverless environment. These functions:
- Scale horizontally
- Run in isolated VMs
- Have web access for external integrations
- Support complex logic combinations
Functions can interact with external services, allowing you to integrate with your own database, subgraphs, and other services.
## Invocation Types
Execute functions on a cron schedule with minimum 1-minute resolution
Invoke functions via HTTP POST requests using API keys
Trigger functions automatically when subscribed events occur
### Schedule Invocation
While there's no maximum schedule limit, the minimum resolution is 1 minute. Schedules with smaller intervals will fail.
### Webhook Invocation
To invoke functions via webhook:
1. Create a Stratus-scoped API key in the admin panel
2. Use the unique webhook URL assigned to your function
```bash Webhook Example
curl --request POST \
--url https://admin.snagsolutions.io/api/stratus/functions//invoke \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: ' \
--data '{
"organizationId": "org-1234",
"websiteId": "site-5678",
"input": {
"foo": 123,
"bar": "example value"
}
}'
```
### Subscription Invocation
Functions linked to subscriptions execute automatically when subscription events fire. Unlike webhooks, subscription-based invocations receive a single payload per event.
## Writing Functions
### Basic Structure
All Stratus functions must follow these rules:
Only one handler function can be exported per file
Handlers must be asynchronous
Functions must accept `input` and `output` parameters
```javascript Basic Template
module.exports.handler = async (input, output) => {
// Your function logic here
};
```
### The Output Class
The `output` parameter provides methods for managing function results and actions:
Sets the function result data (called once before buildOutput)
```typescript
setResult(resultObject: Record): void
```
Logs a failure and stops future executions
```typescript
setError(error: string): void
```
Adds a transaction to the execution queue (requires relayer connection)
```typescript
addTransaction(tx: Pick): void
```
Finalizes and returns the function output (required)
```typescript
buildOutput(): Record
```
## Executing Transactions
To execute transactions, your function must be connected to a relayer in your account.
### Transaction Rules
- Maximum 500 transactions per function run
- Transactions execute in the order they're added
- All transactions use the connected relayer
## Approved Modules
Only pre-approved npm modules can be used in Stratus functions. Contact Snag Solutions if you need additional modules.
Currently approved modules:
- [`axios`](https://axios-http.com/)
- [`lodash`](https://lodash.com/docs)
- [`viem`](https://viem.sh/)
- `ioRedis`
- `upstash/redis`
## Example Function
```javascript
const axios = require('axios');
module.exports.handler = async (input, output) => {
try {
// Make an external API call
const response = await axios.get('https://api.example.com/data');
// Set the result
output.setResult({
data: response.data,
timestamp: Date.now()
});
// Add a transaction if needed
output.addTransaction({
to: '0x123...',
data: '0x456...',
value: '0'
});
// Return the built output
return output.buildOutput();
} catch (error) {
output.setError(`Function failed: ${error.message}`);
return output.buildOutput();
}
};
```
Contact Snag Solutions if you need access to additional npm modules for your Stratus functions.
## Related pages
Reference guide for exports, parameters, and module imports.
Pre-built templates for airdrops, token gating, and rewards.
Trigger functions automatically from onchain events.
Configure the loyalty program that functions can interact with.
---
## Onchain points
*Deploy or import onchain points (ERC-20) through Stratus.*
## Overview
Onchain Points enable you to maintain a blockchain-based [ERC-20](https://eips.ethereum.org/EIPS/eip-20) representation of any loyalty currency in Snag. When enabled, user point balances are automatically synchronized on-chain at configurable intervals, allowing your loyalty program to integrate seamlessly with smart contracts, DEXs, and other on-chain applications.
You have two ways to get started:
- **Deploy New** — Snag deploys a `SnagHybridERC20` contract for you from your relayer address.
- **Import Existing** — Bring your own ERC-20 contract that already has `batchAdminMint` and `batchAdminBurn` functions. Snag verifies the relayer has the right permissions and then starts syncing.
Onchain points are perfect for projects that want to combine the flexibility of off-chain loyalty systems with the composability and transparency of blockchain technology.
## How It Works
Snag's onchain points system uses a **hybrid approach** that combines the best of both worlds:
1. **Off-chain tracking**: User balances are tracked in Snag's high-performance database
2. **Periodic synchronization**: Balance changes are batched and synced on-chain at regular intervals
3. **Efficient execution**: Batch operations minimize gas costs and maximize throughput
### Synchronization Process
Snag calculates the delta (difference) between off-chain balances and on-chain balances for all users since the last sync.
Users requiring mints or burns are grouped into optimized batches. If the dataset is too large for a single transaction, it's automatically split into multiple batches.
The relayer executes `batchMint` or `batchBurn` transactions to update balances on-chain.
Once confirmed on-chain, the sync is complete and balances are updated. The cycle repeats at your configured interval.
Setting a faster sync interval will result in more frequent on-chain updates but will also increase your overall gas costs proportionally.
## Contract Requirements
### Option 1: Deploy New (Recommended)
Select **Deploy New** in the onchain token modal and Snag will deploy a `SnagHybridERC20` contract for you. This is a fully-featured, upgradeable ERC20 token optimized for loyalty programs:
- **Batch operations**: `batchAdminMint` and `batchAdminBurn` for efficient multi-user updates
- **Permission system**: Granular control over who can send/receive tokens
- **Role-based access**: Separate roles for balance updates, admin, and upgrades
- **Upgradeable**: UUPS proxy pattern for future enhancements
No extra setup is needed — Snag deploys the contract from your relayer address and configures all permissions automatically.
The contract source code is available in our [GitHub repository](https://github.com/Snag-Solutions/hybrid-erc20).
### Option 2: Import Existing Contract
If you already have an ERC-20 contract deployed, you can import it by selecting **Import Existing** in the onchain token modal. Your contract must expose the following functions:
```solidity
// Required functions for Snag integration
function batchAdminMint(address[] calldata to, uint256[] calldata amounts) external;
function batchAdminBurn(address[] calldata from, uint256[] calldata amounts) external;
```
#### Prerequisites
Before importing, you must grant your Snag relayer address the role that permits calling `batchAdminMint` and `batchAdminBurn`. You can find your relayer's EOA address in the relayer settings.
If the relayer does not have mint and burn permissions on the contract, the import will fail during verification.
#### Verification Process
When you import a contract, Snag runs an automated verification to confirm everything is configured correctly:
Snag verifies that a contract exists at the address you provided on the correct chain.
A test mint of 1 wei is sent to the relayer address to confirm it has minting permissions.
A test burn of 1 wei is sent from the relayer address to confirm it has burning permissions.
Once both transactions succeed, the contract is linked to your loyalty currency and an initial balance sync is triggered.
The test mint and burn are for 1 wei (the smallest possible unit) and cancel each other out, so there is no net change to any balances.
#### Auto-detection
When importing, Snag will automatically read the contract's `name()` and `symbol()` functions and pre-fill those fields for you. If the contract doesn't expose `name()` or `symbol()` (some contracts don't), Snag falls back to the name and symbol of the loyalty currency you selected. You can always override either value before confirming.
## Permissions & Security
To enable Snag to update balances on your token contract, the relayer must have permission to call the batch mint and burn functions.
### Granting Permissions
- **Deploy New**: Permissions are configured automatically during deployment — no action required.
- **Import Existing**: You must grant your relayer address the appropriate role **before** importing. Your contract should implement role-based access control (such as OpenZeppelin's AccessControl).
For `SnagHybridERC20`, this is the `BALANCE_UPDATER_ROLE`. For custom contracts, grant whichever role allows calling your `batchAdminMint` and `batchAdminBurn` functions.
Relayers are standard EOAs (not contract wallets), making execution straightforward and fully compatible with your existing role system.
### Key Management
Snag uses enterprise-grade security for private key management:
- **GCP Cloud KMS**: All private keys are stored in Google Cloud's Key Management Service
- **Hardware Security Modules (HSM)**: Signing operations occur inside secure hardware enclaves
- **No key exposure**: Private keys never leave the cloud infrastructure
## V2 Relayers Only
Onchain points are exclusively supported on **V2 relayers**, which provide enhanced security with GCP Cloud KMS integration and improved transaction management.
V1 relayers (AWS-based) do not support onchain point synchronization. Please migrate to V2 relayers to use this feature.
## Gas & Funding
### Relayer Funding
**You are responsible for keeping your relayer funded** with native tokens (ETH, MATIC, etc.) to pay for gas fees.
Navigate to your relayer in Snag Admin and copy the EOA address.
Transfer native tokens from any wallet to the relayer address. Ensure you're on the correct network.
Regularly check your relayer balance to ensure uninterrupted operation.
### Fee Structure
**Snag takes zero fees per transaction.** You only pay network gas fees to the blockchain.
## Performance & Scalability
Snag's infrastructure is built to handle high-volume operations, supporting hundreds of thousands of accounts per sync cycle. The main constraint is the blockchain itself—specifically, one transaction per EOA per block.
Batching operations significantly improves both gas efficiency and throughput. Without batching, on-chain balances could never sync efficiently—each transaction would need to wait for the previous one to confirm.
For extremely high-volume use cases, we're developing a **relayer pool** system that distributes load across multiple EOAs using round-robin, effectively removing throughput limits.
## Transaction Reliability
All transaction management—including nonce sequencing, gas estimation, and error handling—is handled automatically by the relayer service. If issues arise (such as insufficient funds or permission errors), you'll be notified to take corrective action.
## Migration & Control
### Taking Full Control
If you want to manage token minting yourself in the future:
Set up your own signing infrastructure and relayer service.
Revoke the balance updater role from Snag's relayer and grant it to your relayer.
You can continue using Snag Admin to trigger syncs, or build your own synchronization solution.
When deploying new, the contract is created from your relayer and all configuration is handled for you. If you already have a contract deployed, use the **Import Existing** flow instead.
### Audits
- **Smart contracts**: Halborn is currently auditing our airdrop protocol; `SnagHybridERC20` audit is expected to follow
- **Infrastructure audit**: Completed 4-week white box security audit by Anvil Secure's lead security researcher
- **Regular audits**: Both smart contracts and application infrastructure undergo periodic security reviews
## Frequently Asked Questions
Any EVM-compatible chain where you have a V2 relayer. Popular chains include Ethereum, Arbitrum, Polygon, Base, Optimism, Berachain, and more. Solana support is coming soon (keys are managed in GCP but hardware signing is not yet available).
Yes! Select **Import Existing** when creating an onchain token and paste your contract address. Your contract must expose `batchAdminMint` and `batchAdminBurn` functions, and your relayer must already have permission to call them. Snag will verify this with a test mint and burn during import.
Transactions will fail to execute until you refund the relayer. Snag will alert you when balances are low.
This depends on your use case and budget. More frequent syncs provide near real-time updates but cost more in gas fees. Less frequent syncs (hourly or daily) are more cost-effective. You can adjust this anytime in Snag Admin.
V2 relayers use GCP Cloud KMS with hardware signing and support onchain point synchronization. V1 relayers use AWS KMS and do not support onchain points. All new projects should use V2 relayers.
Absolutely! All transactions are public on-chain. You can view them on explorers like Etherscan, Arbiscan, or Polygonscan by searching for your relayer's address.
Our relayers support EIP-7702 but we haven't integrated it for hybrid ERC20 yet, as this standard is still propagating and many partner blockchains don't support it. We're monitoring adoption and will integrate when appropriate.
## Next Steps
Set up a V2 relayer to get started with onchain points.
Learn more about Snag's cloud-native blockchain infrastructure.
Create a loyalty currency to sync on-chain.
Use Functions to build custom logic for your onchain points.
---
## Relayers
*Use Snag's transaction relayers for gasless transactions and sponsored onchain actions.*
# Relayers
Relayers are secure private keys managed by Snag Solutions that enable developers to sign messages and relay transactions on-chain. They are commonly used for:
- Creating gasless transactions
- Triggering admin actions
- Maintaining on-chain oracles
- And much more
Snag Solutions handles both key security and failed transaction management.
## Creating a Relayer
1. Navigate to the Relayers section
2. Provide a name for your relayer
3. Select a blockchain network from the dropdown
4. Click "Create Relayer"
The private key is generated and stored exclusively within [AWS KMS](https://aws.amazon.com/kms/) (Key Management Service). All signing operations take place inside a secure signing enclave.
## Funding Your Relayer
To pay for transactions, you'll need to deposit funds into your relayer:
1. Copy your relayer's address
2. Send funds from any wallet to this address
3. Ensure you're on the same blockchain network
API access for relayer operations is currently in development. We expect to provide full API access to all partners by the end of Q1 2025.
## Withdrawing Funds
To withdraw funds from your relayer:
1. Click the "Withdraw" button
2. Enter the destination address
3. Specify the amount to withdraw
All on-chain transactions are final. Triple-check the withdrawal address before confirming the transaction.
## Security
Our relayer system prioritizes security through:
- AWS KMS integration
- Secure signing enclaves
- Automated transaction monitoring
- Failed transaction handling
---
## Stratus overview
*Stratus is Snag's onchain cloud: serverless functions, event subscriptions, gasless relayers, and ERC-20 token sync. Build custom Web3 backend logic.*

Stratus is Snag's cloud-native solution designed to provide complete flexibility for building complex rule logic and onchain operations. With Stratus, you can seamlessly integrate one or all of its core services to develop the precise functionality you need. Each service is fully modular, so they can be used independently or combined within a function to suit your application's unique requirements.
## Core services
Write and deploy serverless JavaScript functions that execute custom onchain logic. Functions run in isolated VMs, scale horizontally, and support external API calls.
Receive real-time updates triggered by smart contract events or internal loyalty actions. Route notifications to webhooks or trigger functions automatically.
Secure private key management for signing messages and relaying transactions onchain. Enable gasless transactions and sponsored actions for your users.
Maintain [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token balances onchain for any Snag loyalty currency with automated synchronization between off-chain points and onchain tokens.
## How Stratus works
Stratus services are designed to work together or independently. A typical workflow combines multiple services:
1. **Subscribe** to onchain events (e.g., a token transfer or contract interaction) using Subscriptions
2. **Process** the event data with a serverless Function that applies your custom business logic
3. **Execute** onchain actions through a Relayer, such as minting rewards or updating token balances
Each service communicates through Snag's internal event system, so you can chain them together without managing infrastructure.
All Stratus services are available through the Snag admin dashboard. Functions and relayers also support programmatic access via the Snag API with a Stratus-scoped API key.
## Use cases
- **Custom loyalty rules** -- Write a Stratus function that checks onchain state and rewards users based on your own criteria using the [Stratus function loyalty rule](/stratus/function-loyalty-rule)
- **Gasless transactions** -- Use relayers to sponsor gas for your users when they claim rewards, mint NFTs, or interact with smart contracts
- **Onchain event tracking** -- Subscribe to smart contract events and automatically trigger reward distributions or update user balances
- **Token-backed points** -- Sync your off-chain loyalty currency to an onchain ERC-20 token so users can trade, stake, or bridge their points
- **Automated airdrops** -- Schedule a function on a cron to periodically check leaderboard rankings and distribute token rewards
## Getting started
Navigate to the **Stratus** section in the [Snag admin dashboard](https://admin.snagsolutions.io). You will see tabs for Functions, Subscriptions, Relayers, and Onchain Tokens.
Go to the **API Keys** page and create a new key with the **Stratus** scope. This key is used for webhook invocations and API access to Stratus services.
Stratus API keys are separate from Website API keys. Do not use Stratus keys with loyalty endpoints, and vice versa.
Head to [Functions](/stratus/functions) to write and deploy your first serverless function. Check out the [function templates](/stratus/function-templates) for common patterns.
## Related pages
Reference guide for writing valid Stratus functions with proper exports, parameters, and module imports.
Pre-built templates for common use cases like airdrops, token gating, and custom reward logic.
Configure webhook endpoints to receive real-time notifications for onchain and loyalty events.
Create custom loyalty rules powered by Stratus serverless functions.
---
## Subscriptions
*Managing Stratus Subscription*
## Overview
Subscriptions empower developers to receive real-time updates triggered by smart contract events or internal loyalty actions. These notifications can either be sent to a webhook endpoint or used to automatically invoke functions.
## Event Types
### Blockchain Events
Blockchain events notify you whenever a smart contract emits an event or a function is called. These events track on-chain state with minimal latency (typically one block), making them ideal for real-time applications.
Start by adding the desired smart contract to your admin panel.
Choose "EVENT" from the subscription options. All available events for the
contract will be populated automatically.
### Blockchain Function Events
The process mirrors blockchain events with a slight variation:
Ensure the contract is registered in your admin panel.
Choose "FUNCTION" after adding the contract. All smart contract functions
will be listed automatically.
### Snag Events
Snag events allow subscriptions to internal platform actions. These events
relate to loyalty account activities and may have a slight delay (up to 30
seconds) after the action is recorded.
## Event Types Reference
Triggered when a user's account balance is updated
Emitted when a user's profile is created or updated
Triggered when a user's loyalty rule progress is set to 100
### LoyaltyTransactionEntry
```json Sample Payload
{
"id": "uuid-123e4567-e89b-12d3-a456-426614174000",
"loyaltyAccountId": "uuid-account-001",
"loyaltyTransactionId": "uuid-transaction-001",
"loyaltyCurrencyId": "uuid-currency-001",
"amount": "100.00",
"direction": "credit",
"metadata": {},
"loyaltyAccountStartAmount": "500.00",
"loyaltyAccountEndAmount": "600.00",
"loyaltyAccountLockVersion": 1,
"hideInUi": false,
"idempotencyKey": "unique-key-001",
"idempotencyKeyExpiresAt": "2025-03-05T00:00:00Z",
"dismissedInUi": false,
"organizationId": "uuid-org-001",
"websiteId": "uuid-website-001",
"createdAt": "2025-03-04T12:00:00Z",
"updatedAt": "2025-03-04T12:00:00Z",
"loyaltyAccount": {
"user": {
"id": "uuid-user-001",
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678"
}
},
"loyaltyTransaction": {
"loyaltyRule": {
"id": "uuid-rule-001"
}
}
}
```
### UserMetadata
```json Sample Payload
{
"id": "uuid-234e4567-e89b-12d3-a456-426614174001",
"userId": "uuid-user-001",
"isBlocked": false,
"websiteId": "uuid-website-001",
"organizationId": "uuid-org-001",
"emailAddress": "user@example.com",
"twitterUser": "userTwitter",
"twitterUserId": "twitter-001",
"twitterUserFollowersCount": 150,
"twitterVerifiedAt": "2025-03-04T10:00:00Z",
"discordUser": "userDiscord",
"discordUserId": "discord-001",
"discordVerifiedAt": "2025-03-04T10:05:00Z",
"instagramUser": "userInstagram",
"instagramUserId": "insta-001",
"instagramVerifiedAt": "2025-03-04T10:10:00Z",
"logoUrl": "https://example.com/logo.png",
"displayName": "User Display Name",
"location": "New York, USA",
"bio": "Sample bio text.",
"portfolioUrl": "https://example.com/portfolio",
"createdAt": "2025-03-04T09:00:00Z",
"updatedAt": "2025-03-04T11:00:00Z",
"user": {
"walletAddress": "0x1234567890abcdef1234567890abcdef1234568"
},
"meta": {}
}
```
### LoyaltyRuleStatus
```json Sample Payload
{
"id": "uuid-456e4567-e89b-12d3-a456-426614174003",
"websiteId": "uuid-website-001",
"organizationId": "uuid-org-001",
"userId": "uuid-user-001",
"loyaltyRuleId": "uuid-rule-001",
"progress": 100,
"fulfilledAt": "2025-08-01T15:58:15.753Z",
"createdAt": "2025-08-01T15:58:15.741Z",
"updatedAt": "2025-08-01T15:58:15.754Z",
"loyaltyRule": {
"id": "uuid-rule-001",
"name": "Sample Loyalty Rule Name"
},
"user": {
"id": "uuid-user-001",
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678"
}
}
```
---
## Function syntax
*Reference guide for the Stratus function scripting syntax and available APIs.*
import AiAssistantCallout from '/snippets/ai-assistant-callout.mdx'
## Overview
To maintain consistency and security, all Stratus functions are validated before they are saved. This guide explains the required syntax, module restrictions, and best practices to help you create functions that pass validation.
## Module Export & Handler Function
### Single Handler Export
Your code must export exactly one handler function.
Exporting multiple handler definitions or not exporting a handler at all will cause validation to fail.
```javascript
// ❌ Incorrect – no exported handler:
const handler = async (input, output) => { ... };
```
### Proper Export Format
Always export your handler as a property of `module.exports`.
```javascript
// ✅ Correct:
module.exports.handler = async (input, output) => {
// Function body...
}
```
The exported handler must be a function. Assigning non-function values (e.g., a string) will trigger an error.
## Handler Function Parameters & Behavior
### Parameter Requirements
The handler function must accept exactly two parameters:
- `input` – holds input data
- `output` – provides methods for setting and building the function's output
```javascript
module.exports.handler = async (input, output) => { ... };
```
### Using output.setResult
Inside your handler, you must call `output.setResult` to set the result object.
```javascript
// ❌ Incorrect:
module.exports.handler = async (input, output) => {
// Missing call to output.setResult
return output.buildOutput()
}
```
### Calling output.buildOutput()
The handler must return the result of `output.buildOutput()`. Omitting this call is not allowed.
```javascript
// ✅ Correct:
module.exports.handler = async (input, output) => {
output.setResult({ message: 'Hello' })
return output.buildOutput()
}
```
If the handler does not accept exactly two parameters, validation will fail with a message similar to: "The 'handler' function must accept exactly two parameters: 'input' and 'output'."
## Module Import Requirements
### Allowed Modules
You may include external modules via `require()`, but only approved packages are allowed:
- `axios`
- `lodash`
- `viem` and its approved subpath `viem/chains`
```javascript
const axios = require('axios')
```
### Disallowed Modules
The following are not allowed:
- Modules not on the approved list (e.g., Node's `fs`)
- Unapproved subpaths from approved packages
- Dynamic requires (using variables for module names)
```javascript
// ❌ Incorrect:
const mod = 'axios'
const axios = require(mod) // Dynamic require not allowed
```
### Extra Exports
Adding extra properties to `module.exports` (aside from the handler) is allowed and will not affect validation:
```javascript
module.exports.extra = 'some extra value'
module.exports.handler = async (input, output) => {
output.setResult({ message: 'Hello' })
return output.buildOutput()
}
```
## JavaScript Syntax
### Valid JavaScript
Your code must be syntactically valid JavaScript. Common syntax errors include:
- Missing closing braces
- Unmatched parentheses
- Invalid syntax
```javascript
// ❌ Example of Syntax Error:
module.exports.handler = async (input, output) => {
output.setResult({ message: "Hello" })
return output.buildOutput(; // Error: Unmatched parenthesis
};
```
## Common Validation Error Messages
You may encounter the following validation errors:
```bash Common Errors
"Module 'unknown/chains' is not approved"
"User code must export exactly one handler function"
"Invalid dynamic require detected"
"Failed to validate user code: ..."
"The 'handler' property must be assigned to a function"
"Multiple handler definitions found"
"The 'handler' function must accept exactly two parameters: 'input' and 'output'"
"The 'handler' function must use output.setResult"
"The 'handler' function must call output.buildOutput()"
```
## Examples
### Valid Stratus Function with External Module
```javascript
const axios = require('axios')
module.exports.handler = async (input, output) => {
const response = await axios.get('https://example.com')
output.setResult({ data: response.data })
return output.buildOutput()
}
```
### Valid Function Without External Modules
```javascript
module.exports.handler = async (input, output) => {
output.setResult({ message: 'Hello World!' })
return output.buildOutput()
}
```
### Common Mistakes
```javascript
// ❌ Incorrect: Multiple handler definitions
module.exports.handler = async (input, output) => { ... };
module.exports.handler = async (input, output) => { ... };
```
```javascript
// ❌ Incorrect: Missing output.setResult
module.exports.handler = async (input, output) => {
return output.buildOutput();
};
```
```javascript
// ❌ Incorrect: Dynamic require
const mod = 'axios';
const axios = require(mod);
```
## Summary
To ensure your Stratus function passes syntax validation:
- Export one handler function using the correct module export syntax
- Ensure the handler is asynchronous and accepts exactly two parameters: `input` and `output`
- Call both `output.setResult` and `output.buildOutput()` within your handler
- Import only approved modules using string literals (no dynamic requires)
- Write valid JavaScript with correct syntax
---
## Webhooks
*Configure webhooks to receive real-time notifications for onchain events and loyalty actions.*
## Overview
Stratus subscriptions deliver events via webhooks, enabling your systems to respond immediately to key events. This guide explains how to set up and validate webhook notifications.
Webhooks are triggered by a scheduled job that runs **every minute**. During each run, Stratus processes all recent changes recorded since the previous run. Once updates are batched and applied, webhook events are dispatched to all destinations subscribed to the affected resources. This ensures near real-time delivery without missing intermediate updates.
## Getting Started
Toggle webhooks within your subscription settings
Provide the URL where your system will accept POST requests
Save the `Signing Key` from your subscription settings for payload validation
## Implementation Guide
### Webhook Validation
Each webhook payload includes a signature header (`x-signature`) that should be validated to ensure authenticity.
```javascript Next.js Example
// https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
export const config = {
api: {
bodyParser: false,
},
}
export default async function handler(req, res) {
try {
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST')
return res.status(405).json({ error: 'Method Not Allowed' })
}
const rawBody = (await getRawBody(req)).toString('utf-8')
const signature = req.headers['x-signature']
if (!signature) {
console.error('Missing signature')
return res.status(400).json({ error: 'Missing signature' })
}
const secret = process.env.WEBHOOK_SECRET
if (!secret) {
console.error('Server misconfiguration: Missing WEBHOOK_SECRET')
return res
.status(500)
.json({ error: 'Server misconfiguration: Missing WEBHOOK_SECRET' })
}
const computedSignature = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
if (
!crypto.timingSafeEqual(
new Uint8Array(Buffer.from(computedSignature)),
new Uint8Array(Buffer.from(signature))
)
) {
console.error('Invalid signature')
return res.status(401).json({ error: 'Invalid signature' })
}
const payload = JSON.parse(rawBody)
payload.forEach((event) => {
console.log('Received event:', event)
})
res.status(200).json({ status: 'Webhook processed successfully' })
} catch (error) {
console.error('Webhook processing error:', error)
res.status(500).json({ error: 'Internal Server Error' })
}
}
```
## Important Considerations
Multiple events may be batched into a single webhook payload
Failed webhooks are retried exponentially up to five times
Always validate webhook signatures using your WEBHOOK_SECRET
Never expose your WEBHOOK_SECRET in client-side code or public repositories. Always store it securely in environment variables.
---
## User Support Tool
*Get help with Snag including support channels, documentation, and contact information.*
The User Support tool is a powerful admin interface designed to help support staff and administrators quickly find, investigate, and manage user accounts. It provides comprehensive visibility into user profiles, loyalty balances, transaction history, and account management capabilities.
## Overview
The User Support tool enables you to:
- **Search for users** by multiple identifiers (wallet address, user ID, display name, social handles)
- **View complete user profiles** including metadata, social connections, and profile information
- **Manage loyalty balances** across multiple wallets and currencies
- **Review activity history** including transactions, multipliers, and badges
- **Take administrative actions** such as blocking users, updating balances, and merging wallets
- **Add internal notes** for tracking support interactions and user status
## Searching for Users
You can search for users using any of the following identifiers:
- **Wallet address** (e.g., `0x1234...5678`)
- **User ID** (unique identifier assigned to each user)
- **Display name** (user's profile name)
- **Social handles** including:
- Twitter/X username
- Discord username
- Telegram username
- Other connected social accounts
Search terms must be at least 5 characters long. The search is
case-insensitive and supports partial matches.
### Search Process
Type your search query in the search field. You can use wallet addresses, user IDs, display names, or social handles.
Click the Search button or press Enter to execute the search.
If multiple users match your search, you'll see a list of matching users. Click on any user to view their detailed profile.
## User Profile Overview
When you select a user, the tool displays a comprehensive profile card showing:
### Profile Information
- **Profile image** and display name
- **User ID** and **Loyalty Account ID** (with copy buttons)
- **Social connections** (Twitter, Discord, Telegram, etc.)
- **Location** and **Portfolio** links (if available)
- **Link to public profile** for viewing the user's public-facing profile page
### Balance Overview
The balance section displays:
- **Total multiplier** across all active multipliers
- **Total balances** for each loyalty currency (summed across all wallets)
- **Per-wallet balances** showing individual wallet addresses and their respective balances
If a user has multiple wallets linked to their account, each wallet's balance
is displayed separately. You can merge wallets to consolidate balances.
## Managing User Accounts
### Updating Balances
You can manually adjust a user's loyalty balance for any currency:
Click the "Update Balance" button next to the wallet you want to modify.
Enter the new balance amounts for each currency. The system will automatically
calculate the difference and create a transaction.
Review the changes and confirm. The system creates a credit or debit transaction to adjust the balance to your specified amount.
Balance updates create permanent transaction records. Always verify the
correct amounts before confirming changes.
### Merging Wallets
When a user has multiple wallets, you can merge balances from one wallet into another:
Click the "Merge Here" button next to the destination wallet (the wallet that will receive the merged balance).
Choose which wallets to merge from and which currencies to transfer.
The system shows you the planned transfers. Review to ensure accuracy.
Confirm the merge operation. All selected balances will be transferred to the destination wallet.
After merging wallets, balances from source wallets are debited and added to
the destination wallet. This operation creates transaction records for audit
purposes.
### Blocking and Unblocking Users
You can block users to prevent them from earning points or completing actions:
Click the "Block" or "Unblock" button next to the user's wallet address.
Enter a reason for blocking the user. This helps track why users were blocked.
Optionally mark the user as "block exempt" to prevent automatic blocking in
future block runs.
Confirm the block or unblock action.
Blocked users cannot earn points or complete loyalty actions. Unblocking
restores full access. Users marked as "block exempt" won't be automatically
blocked in future automated block runs.
### User Verification Status
Users can be marked as "Verified" to indicate manual verification. This status:
- Prevents automatic blocking in block runs
- Can be removed by clicking the X icon next to the "Verified" badge
- Helps track users who have been manually reviewed
## Activity History
The Activity section provides detailed history of user actions across three tabs:
### Transactions Tab
Shows all loyalty transactions including:
- **Timestamp** of each transaction
- **Rule name** that triggered the transaction
- **Wallet address** that received the points
- **Amount** and **currency** of the transaction
- **Currency image** for visual identification
### Multipliers Tab
Displays active multipliers including:
- **Timestamp** when multiplier was applied
- **Multiplier amount** (e.g., 1.5x, 2x)
- **Rule name** or description
- **Wallet address** associated with the multiplier
### Badges Tab
Shows earned badges with:
- **Timestamp** when badge was earned
- **Badge image** and **name**
- **Status** (active/inactive)
- **Wallet address** of the badge recipient
Activity history supports pagination. Use the scroll functionality to load
more historical data.
## Internal Notes
You can add internal notes to track support interactions, user status, or any relevant information:
Find the "Internal Notes" textarea in the user profile card (top right section).
Type your notes. Examples include: - "Likely Sybil account" - "Under review
for suspicious activity" - "Customer support ticket #12345" - "Verified
identity on 2024-01-15"
Notes are automatically saved after 1 second of inactivity (debounced). You'll see a success toast when notes are saved.
Internal notes are limited to 500 characters and are only visible to admin
users. They persist across sessions and help maintain context for support
interactions.
## Advanced Actions
### Handling Compromised Wallets
When a user reports that their wallet has been compromised, you have several options depending on the situation:
#### Understanding Wallet Restrictions
The main wallet is the core account object and **cannot be disconnected by users**. Users can only disconnect secondary wallets (additional wallets connected to their account). This restriction prevents users from gaming the leaderboard by rotating through wallets that share multipliers.
#### Option 1: Transfer Balances to New Wallet (Recommended)
If the user wants to preserve their loyalty progress, you can transfer their balance from the compromised wallet to a new wallet:
Search for the user using the compromised wallet address in the User Support tool.
Click "Update Balance" next to the compromised wallet and set all balances to zero, or use the balance update feature to debit the full amount.
Search for or create the user account with the new wallet address, then use "Update Balance" to credit the same amounts to the new wallet.
Add internal notes documenting the wallet transfer and reason (e.g., "Wallet compromised - balance transferred to 0x... on 2024-01-15").
After transferring balances, the user can connect their social accounts to the new wallet through profile settings. Their progress will be preserved on the new wallet.
#### Option 2: User Self-Service (If Multi-Wallet Enabled)
If the user has multiple wallets connected and multi-wallet support is enabled:
- Users can disconnect their secondary wallets from profile settings
- Users can connect those secondary wallets to a new primary wallet
- Points from the disconnected wallets will transfer with them to the new wallet
- **Note:** Users cannot disconnect their main wallet themselves
This option only works if the user already has multiple wallets connected. If they only have one wallet (the compromised one), they'll need admin assistance.
#### Option 3: Wipe User Metadata (Fresh Start)
If the user wants to start completely fresh with a new wallet:
This action is **irreversible** and will delete:
- Profile details and metadata
- Balances and transaction history
- All associated user data
Only use this action when the user explicitly wants to start from scratch or for GDPR compliance/account deletion requests.
Click the "Wipe User Metadata" button in the user profile card.
Read the warning message carefully and confirm that you want to proceed.
The wipe operation may take a few minutes to complete. You'll receive a notification when it's finished.
After wiping, the user can connect their social accounts to a new wallet through profile settings and complete rules again to earn points.
After wiping user metadata, the user will need to reconnect their social accounts and complete loyalty rules again to start earning points. All previous progress will be lost.
### Copying User Details
You can quickly copy user information:
- **Copy User ID**: Click the copy button next to the User ID
- **Copy Wallet Address**: Click the copy button next to the wallet address
- **Copy All User Details**: Use the copy button next to the user's name to copy a JSON representation of all user data including loyalty accounts
The "Copy All User Details" feature exports a complete JSON object that
includes user information and all associated loyalty accounts. This is useful
for debugging or exporting user data.
## Multiple User Results
When your search returns multiple users, you'll see a list view showing:
- **Profile images** and **display names**
- **User IDs** and **wallet addresses**
- **Current balances** for each loyalty currency
- **Social handles** (Twitter, Discord, Telegram)
Click on any user in the list to view their detailed profile and manage their account.
## Best Practices
Update balances when:
- Correcting errors in point calculations
- Compensating users for support issues
- Adjusting balances after rule changes
- Manual adjustments for special promotions
Always document the reason in internal notes.
Merge wallets when:
- Users have accidentally created multiple accounts
- Consolidating accounts after identity verification
- Cleaning up duplicate or test accounts
Verify wallet ownership before merging to prevent fraud.
When a user reports a compromised wallet:
**Preserve progress (recommended):**
- Debit balance from compromised wallet
- Credit balance to new wallet
- User connects social accounts to new wallet
**User self-service (if multi-wallet enabled):**
- User disconnects secondary wallets
- User connects secondary wallets to new primary wallet
- Points transfer automatically
**Fresh start:**
- Use "Wipe User Metadata" if user wants to start over
- User reconnects social accounts and completes rules again
Always verify wallet ownership before transferring balances.
Block users when:
- Detecting fraudulent or Sybil activity
- Violation of terms of service
- Suspicious behavior patterns
- Request from compliance or legal team
Always provide a clear reason for blocking.
Use internal notes to:
- Track support ticket numbers
- Document user verification status
- Note suspicious activity patterns
- Record important user interactions
- Maintain context for future support interactions
Keep notes concise and professional.
## Troubleshooting
### User Not Found
If a user doesn't appear in search results:
- Verify the search term is correct (minimum 5 characters)
- Try searching with different identifiers (wallet address vs. user ID)
- Check if the user account exists in the system
- Ensure you're searching within the correct website/organization context
### Balance Discrepancies
If balances don't match expected values:
- Review the Transactions tab to see all balance changes
- Check for failed transactions that may not have been applied
- Verify multipliers are correctly applied
- Look for transactions from multiple wallets
### Multiple Wallet Issues
If a user has multiple wallets causing confusion:
- Review all wallets in the balance section
- Consider merging wallets if they belong to the same user
- Verify wallet ownership before taking actions
- Check the Activity tab to see which wallet was used for each transaction
### Compromised Wallet Reports
If a user reports their wallet has been compromised:
- **Verify the request**: Confirm the user's identity before taking any action
- **Check for multiple wallets**: If they have secondary wallets, they can disconnect and reconnect them to a new primary wallet themselves
- **Transfer balances**: Use the balance update feature to debit from the old wallet and credit to the new wallet
- **Document the action**: Always add internal notes explaining the wallet transfer
- **Consider wiping**: Only use "Wipe User Metadata" if the user explicitly wants to start fresh
Always verify wallet ownership before transferring balances or taking administrative actions. Compromised wallet situations require careful verification to prevent fraud.
## Related Documentation
- [Managing User Accounts](/loyalty/managing-user-accounts) - General user account management
- [Updating Balances](/loyalty/updating-balances) - Programmatic balance updates
- [Blocking Users](/loyalty/blocking-users) - User blocking and moderation
- [Multi-Wallet Support](/loyalty/multi-wallet-support) - Understanding multi-wallet functionality
---
## User authentication overview
*Connect wallet, email, and social login to your Snag platform. Supports Dynamic, Thirdweb, Privy, Immutable, Sequence, and Auth0 providers.*

## Supported authentication providers
- [Dynamic (EVM, Solana, SUI)](/user-auth/dynamic)
- [Thirdweb](/user-auth/thirdweb)
- [Privy](/user-auth/privy)
- [Immutable Passport](/user-auth/immutable)
- [Sequence Wallet](/user-auth/sequence)
- [Auth0](/user-auth/auth0)
## How it works
Select one or more authentication providers that best fit your platform.
Each provider requires specific keys and configurations. Refer to the
separate provider pages for details on what to provide.
Once you have all the required details, share them with us. Our team will
set up authentication on your behalf.
## User identification across providers
- **Non-custodial wallets (e.g., MetaMask)**: Snag identifies users by `wallet_address`. If your app also identifies users by the same on-chain wallet, you can treat the user as the same across both systems and complete rules via that `wallet_address`.
- **Email/Social login via embedded wallet providers (e.g., Dynamic, Privy, Thirdweb)**: When a user logs in with email or social auth via an embedded wallet provider, an embedded wallet is issued. Snag uses that embedded wallet address as the user's identifier. If your backend uses a different provider or configuration than Snag, the same person may receive different embedded wallet addresses in your system vs. Snag.
Because different providers or configurations can produce different embedded wallets for the same email, do not assume the embedded wallet address is shared between systems. Always use the embedded wallet created within Snag when interacting with Snag.
Use the embedded wallet address issued by Snag's authentication flow for that user. Pass this address from your client to your backend (or fetch it server-side via your wallet provider SDK for the Snag session) and call Snag using that wallet_address to complete tasks or query user state.
The identifier is the on-chain wallet_address. You can use the exact same address across your app and Snag to reference the same user and complete external rules.
Best practice: Store a mapping in your backend between your primary user ID and the wallet_address that Snag recognizes (non-custodial or Snag-embedded). This guarantees you can reliably retrieve the correct user on Snag when initiating rewards or tasks.
If you maintain your own user IDs, set externalIdentifier when creating users in Snag. See the API docs: Create Users – External Identifier.
## Related pages
Complete setup checklist for launching your Snag platform.
Programmatically create and manage user accounts.
Link multiple wallets to a single user identity.
Authentication, endpoints, and base URL configuration.
---
## Auth0
*Integrate Auth0 as your authentication provider for enterprise-grade identity management.*

## Required information:
- Client ID
- Issuer Link
- Sign-in button text (optional)
## How to find these credentials:
1. Create an application in [Auth0](https://auth0.com/) ([docs](https://auth0.com/docs)).
2. Copy your **Client ID** and **Issuer Link**.
3. Send these details to us.
---
## Dynamic
*Integrate Dynamic as your authentication provider for embedded wallets and social login.*

## Required information:
- Dynamic Environment ID
- API Base URL
## How to find these credentials:
1. Sign up for [Dynamic](https://dynamic.xyz/) and log into your developer dashboard. See the [Dynamic docs](https://docs.dynamic.xyz/) for detailed setup guides.
2. Copy your **Environment ID**.
3. If you have the cookie based auth enabled in dynamic, you will need to provide the **API Base URL** as well.
You will be able to find it in https://app.dynamic.xyz/dashboard/developer/domains.
The api base url will be in the following format https://cookie-based-auth-domain/api/v0
4. Send these values to us.
---
## Immutable Passport
*Integrate Immutable Passport as your authentication provider for gaming-focused Web3 apps.*

## Required Information:
- Client ID
- Publishable Key
## How to find these credentials:
1. Log into your [Immutable Hub](https://hub.immutable.com/). See the [Immutable Passport docs](https://docs.immutable.com/docs/zkEVM/products/passport) for detailed configuration.
2. Copy your **Client ID** and **Publishable Key**.
3. Add the following redirect URLs to your Immutable dashboard:
- `https:///immutable/redirect`
- [`https://snag-web.onrender.com/immutable/redirect`](https://snag-web.onrender.com/immutable/redirect)
4. Send these details to us.
---
## Privy
*Integrate Privy as your authentication provider for embedded wallets and email login.*

## Required Information:
- Client ID
- App ID
## How to find these credentials:
1. Sign up for [Privy](https://privy.io/) and access your developer dashboard. See the [Privy docs](https://docs.privy.io/) for detailed setup instructions.
2. Copy your **Client ID** and **App ID**.
3. Send these values to us.
---
## Sequence Wallet
*Integrate Sequence as your authentication provider for smart wallet support.*

## Required Information:
- Sequence Project ID (Access Key)
- WAAS Configuration Key
- Google OAuth Client ID
- Client Secret
- WalletConnect Project ID
- Network (e.g. Arbitrum Sepolia)
## How to find these credentials:
1. Configure Google OAuth in the **Google Developer Console**:
- Create an OAuth application in the **Google Developer Console**.
- Ensure you allow `https:///`
2. Sign up for [WalletConnect](https://walletconnect.com/) and retrieve your **Project ID**.
3. Sign up for [Sequence](https://sequence.xyz/) ([docs](https://docs.sequence.xyz/)) and access your project dashboard.
4. Retrieve your **Project ID, WAAS Configuration Key**.
5. Deploy the parent contract in the Sequence **build dashboard**.
6. Send these details (**Project ID, WAAS Configuration Key, Google OAuth Client ID, Client Secret, WalletConnect Project ID**) to us.
---
## Thirdweb
*Integrate Thirdweb as your authentication provider for wallet and social login.*

## Required information:
- Client ID
- Sign-in button text (optional)
## How to find these credentials:
1. Go to the [**Thirdweb Dashboard**](https://playground.thirdweb.com/connect/sign-in/button). See the [Thirdweb docs](https://portal.thirdweb.com/) for full integration guides.
2. Generate a **Client ID** for your application.
3. Send this Client ID to us.
---
## Delegate.cash integration
*Enable delegate.cash support so users can participate in loyalty programs through delegated wallets.*
[Delegate.cash](https://delegate.xyz) is an on-chain registry that lets users delegate rights from one wallet to another without moving assets. Snag integrates with delegate.cash so users can prove ownership of NFTs in cold storage while interacting with your loyalty program using a hot wallet.
## How it works
When a user sets up a delegation on delegate.cash, they create an on-chain record that says "Wallet A delegates to Wallet B." In Snag's context:
- **Delegating wallet (cold wallet)**: The wallet holding valuable assets like NFTs
- **Delegated-to wallet (hot wallet)**: The wallet the user connects to your loyalty program
When the delegated-to wallet authenticates with your loyalty program, Snag checks the delegate.cash registry and combines the holdings from both wallets for rule evaluation.
## Enabling delegate.cash
To enable delegate.cash support for your loyalty program:
Navigate to **Customization > Platform Settings** in your Snag Admin dashboard and enable the delegate.cash integration toggle.
Once enabled, Snag indexes delegations for users when they authenticate. The system checks the delegate.cash on-chain registry for any delegations pointing to the user's connected wallet.
Assets from delegated wallets are automatically combined with the user's connected wallet for eligible loyalty rules.
If users don't see their delegated wallet holdings after enabling this feature, they need to **re-authenticate** (sign out and sign back in) to trigger the delegation indexing.
## Delegation vs user groups
Snag offers two ways to combine wallet holdings: **delegate.cash** and **user groups**. These work differently:
| Feature | Delegate.cash | User Groups |
|---------|--------------|-------------|
| **Relationship** | One-way (delegator → delegatee) | Bi-directional (all wallets equal) |
| **Control** | Only the delegated-to wallet controls the pool | All wallets in the group have equal control |
| **Setup** | On-chain via delegate.cash | In-app via Snag |
| **Use case** | Cold wallet security | Multiple personal wallets |
| **Points sharing** | No point balance sharing | Full point balance aggregation |
| **Rule scope** | Token hold rules only | All loyalty rules |
### One-way delegation explained
With delegate.cash, the delegation is **one-way**:
- The **delegated-to wallet** (hot wallet) can claim rewards based on combined holdings
- The **delegating wallet** (cold wallet) cannot independently claim rewards through this delegation
- If the cold wallet owner wants to interact directly, they must connect that wallet separately
This differs from user groups where all connected wallets are treated as equals and share the same identity, points balance, and social connections.
Use delegate.cash when users want to keep valuable NFTs in cold storage while participating in your loyalty program. Use user groups when users simply have multiple active wallets they want to combine.
## Supported rules
Delegate.cash integration currently applies to:
- **[Hold an NFT](/loyalty/rules/token-hold)**: NFT holdings from delegated cold wallets are aggregated with the hot wallet when evaluating token hold rules
Delegation-based holding checks apply only to the "Hold an NFT" rule. Other loyalty rules do not use delegated wallet holdings.
## Limitations
**Current limitations:**
- **Full-wallet delegations only**: Collection-level or token-level delegations are not supported. The delegation must grant full wallet rights.
- **Ethereum mainnet only**: Only delegations on Ethereum mainnet are tracked. Delegations on other networks (Polygon, Arbitrum, etc.) are ignored.
- **Token hold rules only**: Delegation aggregation only applies to NFT holding rules, not to other rule types like social connections or check-ins.
## How it works technically
When delegate.cash is enabled for your website:
1. **On authentication**: When a user connects their wallet and authenticates, Snag queries the delegate.cash registry for any delegations where the user's wallet is the delegatee
2. **Delegation storage**: Valid delegations are stored in the user's metadata (`delegatedTo` and `delegationsFrom` relationships)
3. **Rule evaluation**: When evaluating token hold rules, the system combines holdings from:
- The user's connected wallet
- Any wallets that have delegated to the user's wallet
- The wallet the user has delegated to (if applicable)
4. **On-chain sync**: Delegation data is synced from the on-chain state, so changes made on delegate.cash are reflected after the user re-authenticates
## User experience
For end users with delegate.cash set up:
Users visit [delegate.xyz](https://delegate.xyz) and create a full-wallet delegation from their cold wallet to their hot wallet.
Users connect their hot wallet (the delegated-to wallet) to your loyalty site.
Snag automatically detects the delegation and combines NFT holdings from both wallets when evaluating token hold rules.
Users do not need to connect their cold wallet directly. The delegation on delegate.cash is sufficient for Snag to recognize and aggregate their holdings.
## Troubleshooting
- **Re-authenticate**: Have the user sign out and sign back in to trigger delegation indexing
- **Check delegation type**: Ensure the delegation is a full-wallet delegation, not collection or token-level
- **Verify network**: Confirm the delegation was created on Ethereum mainnet
- **Check delegate.cash**: Verify the delegation is active on [delegate.xyz](https://delegate.xyz)
Changes to delegations on delegate.cash require the user to re-authenticate for Snag to pick up the updates. The system syncs delegation state at authentication time.
With one-way delegation, only the delegated-to wallet (hot wallet) receives the aggregated holdings. The delegating wallet (cold wallet) will not see combined holdings unless it's also connected and has its own delegations.
## Related resources
Learn how token hold rules work with delegated wallet holdings.
Compare with user groups for bi-directional multi-wallet support.
---
## Custom user attributes
*Define custom profile fields, let users edit them, fetch them from the Users API, and save updates.*
You can add custom fields to user profiles and use them across your website and APIs.
Custom attributes are ideal for capturing structured data like "Favorite Team"
or "External Profile Link" that isn't part of the default profile.
## Where custom attributes come from
- You can define attributes in Admin → Customization → User Profiles.
- Or you can create them via API.
If you're using the Admin UI, add a new field in the User Profiles section. To use the API, create attributes with a label that users will see.
- API: See [Create a new website user attribute](/api-reference/identity/create-a-new-website-user-attribute).
You should see the new attribute in Admin → Customization → User Profiles.
You must include a valid `X-API-KEY`. Missing or invalid keys result in 403 Forbidden.
In Snag's template, custom attributes appear as editable fields in the Edit Profile modal automatically. No extra code is required after you create the attributes.
Field order and labels come from how you configured attributes in Admin or via the API.
Use the Users API to retrieve users and their saved values for any defined attributes.
- API: See [Get users](/api-reference/identity/get-users).
Confirm the response includes each user and their stored values for the attributes you defined.
When a user edits their profile (or when you set values server-side), upsert attribute values using the attribute values endpoint.
- API: See [Create a new website user attribute value](/api-reference/identity/create-a-new-website-user-attribute-value).
Re-fetch the user and verify the updated value appears.
- **Cause**: Missing or invalid `X-API-KEY`, or insufficient permissions.
- **Fix**: Provide a valid API key with permission to write attributes/values for the given `websiteId` and `organizationId`.
- **Cause**: Missing required fields (`websiteId`, `organizationId`, `data`),
or invalid IDs. - **Fix**: Ensure all IDs are valid UUIDs and the `data` array
has the correct shape.
- **Cause**: Value upsert succeeded, but UI cache not refreshed.
- **Fix**: Re-fetch the user or invalidate client cache after saving.
## Related APIs
- [Create a new website user attribute — see API reference](/api-reference/identity/create-a-new-website-user-attribute).
- [Get users — see API reference](/api-reference/identity/get-users).
- [Create a new website user attribute value — see API reference](/api-reference/identity/create-a-new-website-user-attribute-value).
Use consistent labels and avoid storing secrets or sensitive PII in custom
attributes.
---
## Get Asset upload URL
`POST /api/assets`
`POST https://admin.snagsolutions.io/api/assets`
This endpoint enables you to upload a file to the resource. Returns a presigned URL and the asset's public URL. Call PUT with the file as the body to upload the asset using the presigned URL.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for uploading an asset with organization, website, and user details.
- `organizationId` (string, uuid) — UUID of the organization (optional, converted to lowercase)
- `websiteId` (string, uuid) — UUID of the website (optional, converted to lowercase)
- `userId` (string, uuid) — UUID of the user (optional, converted to lowercase)
- `filePath` (string, one of: images, videos, banners, fonts, posts, profiles, minting, loyalty, tokenClaims, dapps) — The file path, must be a valid enum value.
- `fileName` (string, required) — The name of the file, must match the regex pattern (converted to lowercase) and have a valid extension.
- `fileSize` (number, required) — Size of the file in bytes. Uploaded file must match this file size. 1mb for images and 10mb for videos.
## Response 200
200
- response (object) — Response returned upon successful generation of the signed URL for uploading the file.
- `signedUrl` (string, uri, required) — The pre-signed URL to be used for uploading the file.
- `url` (string, uri, required) — The URL of the uploaded file.
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get auction bids
`GET /api/auction_bids` — query parameters: `id`, `userId`, `walletAddress`, `startingAfter`, `limit`, `organizationId`, `websiteId`, `auctionId`, `status`, `orderBy`, `skip`, `includeCount`, `statuses`
`GET https://admin.snagsolutions.io/api/auction_bids`
Returns a list of auction bids with optional filtering and pagination.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `id` (string) — The bid ID(s) - accepts a single ID or an array of IDs
- `userId` (string) — ID of the user who made the bid
- `walletAddress` (string) — Wallet address of the bidder
- `startingAfter` (string) — Pagination cursor to start after a specific bid ID
- `limit` (integer) — Maximum number of bids to return
- `organizationId` (string) — ID of the organization
- `websiteId` (string) — ID of the website
- `auctionId` (string) — ID of the auction
- `status` (string) — Status of the bid
- `orderBy` (string) — Order bids by amount (ascending or descending). Defaults to desc.
- `skip` (integer) — Number of bids to skip (for offset-based pagination)
- `includeCount` (boolean) — Include total count of matching bids in response
- `statuses` (array) — Filter by multiple bid statuses (OR condition)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object) — Schema for a get auction bids response
- `id` (string, uuid, required)
- `status` (string, required, one of: submitted, accepted, rejected, fulfilled)
- `auctionId` (string, uuid, required)
- `auctionItemId` (string, uuid, required)
- `currencyAddress` (string, required)
- `currencyDecimals` (number, required)
- `updatedAt` (string, date-time, required)
- `auctionItem` (object, required)
- `id` (string, uuid, required)
- `quantity` (number, required)
- `mintingContractAsset` (object, required)
- id: … (nested further)
- isPhygitalItem: … (nested further)
- shippingPrice: … (nested further)
- tokenId: … (nested further)
- revealedAt: … (nested further)
- `user` (object, required)
- `id` (string, uuid, required)
- `walletAddress` (string, required)
- `loyaltyCurrency` (object, required)
- `id` (string, uuid, required)
- `name` (string, required)
- `symbol` (string, required)
- `decimals` (number, required)
- `imageUrl` (string, required)
- `apiGetUrl` (string)
- `apiGetTransactionEntryUrl` (string)
- `apiPostUrl` (string)
- `apiAuthType` (string, one of: none, basic, bearer, apiKey)
- `apiAuthKey` (string)
- `apiAuthValue` (string)
- `auction` (object, required)
- `id` (string, uuid, required)
- `name` (string, required)
- `hasNextPage` (boolean, required) — Indicates if there is a next page of results
- `count` (number) — Total count of matching bids (only present when includeCount=true)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get auction items
`GET /api/auctions/items` — query parameters: `auctionId`, `organizationId`, `websiteId`, `limit`
`GET https://admin.snagsolutions.io/api/auctions/items`
Returns a paginated list of auction items for the given auction ID(s).
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `auctionId` (string, required) — Comma-separated list of auction UUIDs to fetch items for (at least one required)
- `organizationId` (string) — Optional organization ID for scoping
- `websiteId` (string) — Optional website ID for scoping
- `limit` (integer) — Number of items to return (1-100, default 10)
## Response 200
200
- response (object) — Schema for the response when fetching auction items
- `data` (array, required)
- `items` (object) — Schema representing an auction item
- `id` (string, uuid, required)
- `sortId` (number, required)
- `quantity` (number, required)
- `mintingContractAsset` (object, required) — Schema representing a minting contract asset
- `id` (string, uuid, required)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get auctions
`GET /api/auctions`
`GET https://admin.snagsolutions.io/api/auctions`
Returns a list of auctions with optional filtering and pagination.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Response 200
200
- response (object) — Schema for the response when fetching auctions
- `data` (array, required)
- `items` (object) — Schema representing an auction
- `id` (string, uuid, required)
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `mintingContractId` (string, uuid, required)
- `userId` (string, uuid, required)
- `reservePrice` (string, required)
- `minBidIncrement` (string, required)
- `minBidAmount` (string, required)
- `currencyAddress` (string, required)
- `loyaltyCurrencyId` (string, uuid, required)
- `currencyDecimals` (number, required)
- `startsAt` (string, date-time, required)
- `endsAt` (string, date-time, required)
- `extendedDurationInSeconds` (number, required)
- `hideBids` (boolean, required)
- `type` (string, required, one of: first_price, second_price, english)
- `status` (string, required, one of: in_progress, completed, cancelled)
- `biddersOwnCollectionAddresses` (array, required)
- `items` (string)
- `name` (string, required)
- `description` (string, required)
- `network` (string, required, one of: ethereum, zksync)
- `signature` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `loyaltyCurrency` (object, required) — Schema representing a loyalty currency
- `id` (string, uuid, required)
- `name` (string, required)
- `symbol` (string, required)
- `decimals` (number, required)
- `imageUrl` (string, required)
- `auctionBids` (array, required)
- `items` (object) — Schema representing an auction bid
- id: … (nested further)
- amount: … (nested further)
- createdAt: … (nested further)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create new badge
`POST /api/loyalty/badges`
`POST https://admin.snagsolutions.io/api/loyalty/badges`
This endpoint allows you to configure a new badge
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for creating a new badge.
- `organizationId` (string, uuid, required) — UUID of the organization (optional, converted to lowercase)
- `websiteId` (string, uuid, required) — UUID of the website (optional, converted to lowercase)
- `name` (string, required) — The name of the badge
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `description` (string) — The description of the badge
- `imageUrl` (string, uri, required) — The URL of the badge image
- `rewards` (array) — Array of rewards granted when the badge is achieved
- `items` (object)
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of reward (points or multiplier)
- `loyaltyCurrencyId` (string, uuid) — UUID of the loyalty currency for this reward
- `loyaltyCurrencyIds` (array) — Array of loyalty currency IDs for multiplier rewards (empty means all currencies)
- `items` (string, uuid)
- `amount` (number) — Points amount to reward (for points reward type)
- `multiplier` (number) — Multiplier to reward (for multiplier reward type)
- `isRetroactive` (boolean, default: false) — Whether the multiplier should be applied to the user's existing balance
- `conditions` (array, required) — Array of conditions for the badge
- `items` (object)
- `type` (string, one of: rule, rules, points, section, level, api, badge, badges, csv) — The type of rule
- `description` (string) — The description of the rule
- `amount` (number) — The amount of the rule
- `repeatCount` (number) — The number of times the rule should be repeated to fulfill the condition
- `requiredCount` (number) — The number of rules that need to be fulfilled to satisfy the condition
- `loyaltyRuleChainId` (string, uuid) — UUID of the loyalty rule chain (optional, converted to lowercase)
- `loyaltyRuleIds` (array) — Array of UUIDs of the rules (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyRuleGroupIds` (array) — Array of UUIDs of the rule group (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyLeaderboardViewIds` (array) — Array of UUIDs of the Loyalty Leaderboard (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyCurrencyIds` (array) — Array of UUIDs of the currency (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyBadgeIds` (array) — Array of UUIDs of the badge (optional, converted to lowercase)
- `items` (string, uuid)
- `csvUrl` (string, uri) — URL of the CSV file (optional)
- `csvFiles` (array)
- `items` (object)
- `src` (string, uri, required)
- `name` (string)
- `description` (string)
## Response 200
200
- response (object) — Response returned upon successful creation of the badge.
- `id` (string, uuid, required) — UUID of the badge (converted to lowercase)
- `name` (string, required) — The name of the badge
- `description` (string, required) — The description of the badge
- `imageUrl` (string, uri, required) — The URL of the badge image
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `loyaltyConditions` (array, required) — Array of conditions of the badge
- `items` (object) — Schema for a loyalty condition rule.
- `id` (string, uuid, required) — UUID of the condition (converted to lowercase)
- `type` (string, required, one of: rule, rules, points, section, level, api, badge, badges, csv) — The type of the condition
- `description` (string, required) — The description of the condition
- `amount` (number, required) — The amount of the condition
- `repeatCount` (number, required) — The repeat count of the condition
- `requiredCount` (number, required) — The required count of the condition
- `loyaltyRuleGroupId` (string, uuid, required) — UUID of the rule group (converted to lowercase)
- `association` (array, required)
- `items` (object)
- loyaltyRule: … (nested further)
- loyaltyBadge: … (nested further)
- loyaltyRuleGroup: … (nested further)
- loyaltyCurrency: … (nested further)
- loyaltyLeaderboardView: … (nested further)
- `loyaltyCurrencyId` (string, uuid, required) — UUID of the currency (converted to lowercase)
- `csvUrl` (string, uri, required) — URL of the CSV file
- `csvFiles` (array) — Array of CSV files associated with the condition
- `items` (object)
- id: … (nested further)
- url: … (nested further)
- name: … (nested further)
- description: … (nested further)
- `rewards` (array) — Array of rewards granted when the badge is achieved
- `items` (object)
- `id` (string, uuid, required) — UUID of the badge reward
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of reward (points or multiplier)
- `loyaltyCurrencyId` (string, uuid, required) — UUID of the loyalty currency for this reward
- `loyaltyCurrencyIds` (array) — Array of loyalty currency IDs for multiplier rewards (empty means all currencies)
- `items` (string, uuid)
- `amount` (number, required) — Points amount to reward (for points reward type)
- `multiplier` (number, required) — Multiplier to reward (for multiplier reward type)
- `isRetroactive` (boolean, required) — Whether the multiplier should be applied to the user's existing balance
- `loyaltyBadgeUsers` (array) — Array of user badges
- `items` (object)
- `id` (string, uuid, required) — UUID of the user badge (converted to lowercase)
- `userId` (string, uuid, required) — UUID of the user (converted to lowercase)
- `websiteId` (string, uuid, required) — UUID of the website (converted to lowercase)
- `organizationId` (string, uuid, required) — UUID of the organization (converted to lowercase)
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `status` (string, required, one of: active, inactive, revoked) — The status of the badge
- `loyaltyBadgeId` (string, uuid, required) — UUID of the badge (converted to lowercase)
- `dismissedInUi` (boolean) — Whether the user has dismissed the badge in the UI
- `user` (object) — User associated with the user badge
- `id` (string, uuid, required) — UUID of the user (converted to lowercase)
- `walletAddress` (string, required) — Wallet address of the user
- `createdAt` (string, date-time, required) — The date and time the user badge was created
- `completedAt` (string, date-time) — The date and time the user badge was granted
- `updatedAt` (string, date-time, required) — The date and time the user badge was updated
- `loyaltyRules` (array) — Array of loyalty rules associated with the badge
- `items` (object)
- `id` (string, uuid, required) — UUID of the loyalty rule (converted to lowercase)
- `name` (string, required) — The name of the loyalty rule
- `dataJobs` (array) — Array of data jobs associated with the badge
- `items` (object)
- `id` (string, uuid, required) — UUID of the data job (converted to lowercase)
- `status` (string, required, one of: pending, processing, complete, failed, cancelled) — The status of the data job
- `message` (string) — The message associated with the data job
- `createdAt` (string, date-time, required) — The date and time the data job was created
- `updatedAt` (string, date-time, required) — The date and time the data job was updated
- `createdAt` (string, date-time, required) — The date and time the badge was created
- `updatedAt` (string, date-time, required) — The date and time the badge was updated
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Delete badge (by id)
`DELETE /api/loyalty/badges/{id}` — path parameters: `id`
`DELETE https://admin.snagsolutions.io/api/loyalty/badges/{id}`
This endpoint allows you to delete a badge.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required) — UUID of the badge to delete (converted to lowercase)
## Response 200
200
- response (object) — Response returned upon successful deletion of the badge.
- `id` (string, uuid, required) — UUID of the badge (converted to lowercase)
- `name` (string, required) — The name of the badge
- `description` (string, required) — The description of the badge
- `imageUrl` (string, uri, required) — The URL of the badge image
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `loyaltyConditions` (array, required) — Array of conditions of the badge
- `items` (object) — Schema for a loyalty condition rule.
- `id` (string, uuid, required) — UUID of the condition (converted to lowercase)
- `type` (string, required, one of: rule, rules, points, section, level, api, badge, badges, csv) — The type of the condition
- `description` (string, required) — The description of the condition
- `amount` (number, required) — The amount of the condition
- `repeatCount` (number, required) — The repeat count of the condition
- `requiredCount` (number, required) — The required count of the condition
- `loyaltyRuleGroupId` (string, uuid, required) — UUID of the rule group (converted to lowercase)
- `association` (array, required)
- `items` (object)
- loyaltyRule: … (nested further)
- loyaltyBadge: … (nested further)
- loyaltyRuleGroup: … (nested further)
- loyaltyCurrency: … (nested further)
- loyaltyLeaderboardView: … (nested further)
- `loyaltyCurrencyId` (string, uuid, required) — UUID of the currency (converted to lowercase)
- `csvUrl` (string, uri, required) — URL of the CSV file
- `csvFiles` (array) — Array of CSV files associated with the condition
- `items` (object)
- id: … (nested further)
- url: … (nested further)
- name: … (nested further)
- description: … (nested further)
- `rewards` (array) — Array of rewards granted when the badge is achieved
- `items` (object)
- `id` (string, uuid, required) — UUID of the badge reward
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of reward (points or multiplier)
- `loyaltyCurrencyId` (string, uuid, required) — UUID of the loyalty currency for this reward
- `loyaltyCurrencyIds` (array) — Array of loyalty currency IDs for multiplier rewards (empty means all currencies)
- `items` (string, uuid)
- `amount` (number, required) — Points amount to reward (for points reward type)
- `multiplier` (number, required) — Multiplier to reward (for multiplier reward type)
- `isRetroactive` (boolean, required) — Whether the multiplier should be applied to the user's existing balance
- `loyaltyBadgeUsers` (array) — Array of user badges
- `items` (object)
- `id` (string, uuid, required) — UUID of the user badge (converted to lowercase)
- `userId` (string, uuid, required) — UUID of the user (converted to lowercase)
- `websiteId` (string, uuid, required) — UUID of the website (converted to lowercase)
- `organizationId` (string, uuid, required) — UUID of the organization (converted to lowercase)
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `status` (string, required, one of: active, inactive, revoked) — The status of the badge
- `loyaltyBadgeId` (string, uuid, required) — UUID of the badge (converted to lowercase)
- `dismissedInUi` (boolean) — Whether the user has dismissed the badge in the UI
- `user` (object) — User associated with the user badge
- `id` (string, uuid, required) — UUID of the user (converted to lowercase)
- `walletAddress` (string, required) — Wallet address of the user
- `createdAt` (string, date-time, required) — The date and time the user badge was created
- `completedAt` (string, date-time) — The date and time the user badge was granted
- `updatedAt` (string, date-time, required) — The date and time the user badge was updated
- `loyaltyRules` (array) — Array of loyalty rules associated with the badge
- `items` (object)
- `id` (string, uuid, required) — UUID of the loyalty rule (converted to lowercase)
- `name` (string, required) — The name of the loyalty rule
- `dataJobs` (array) — Array of data jobs associated with the badge
- `items` (object)
- `id` (string, uuid, required) — UUID of the data job (converted to lowercase)
- `status` (string, required, one of: pending, processing, complete, failed, cancelled) — The status of the data job
- `message` (string) — The message associated with the data job
- `createdAt` (string, date-time, required) — The date and time the data job was created
- `updatedAt` (string, date-time, required) — The date and time the data job was updated
- `createdAt` (string, date-time, required) — The date and time the badge was created
- `updatedAt` (string, date-time, required) — The date and time the badge was updated
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get badges (all or by id)
`GET /api/loyalty/badges` — query parameters: `organizationId`, `websiteId`, `badgeIds`, `walletAddress`, `userId`, `userGroupId`, `userGroupExternalIdentifier`, `startingAfter`, `limit`, `includeDeleted`
`GET https://admin.snagsolutions.io/api/loyalty/badges`
This endpoint allows you to fetch badge(s) data.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string) — UUID of the organization (optional, converted to lowercase)
- `websiteId` (string) — UUID of the website (optional, converted to lowercase)
- `badgeIds` (string) — Array of badge IDs (converted to lowercase) to fetch
- `walletAddress` (string) — Wallet Address of the user, or array of wallet addresses.
- `userId` (string) — UUID of the user, or array of UUID.
- `userGroupId` (string) — UUID of the user group
- `userGroupExternalIdentifier` (string) — External identifier of the user group
- `startingAfter` (string) — UUID of the badge to start after (converted to lowercase)
- `limit` (integer) — Number of badges to fetch
- `includeDeleted` (boolean) — Include deleted badges in the response
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object) — Response returned upon successful fetching of the badges.
- `id` (string, uuid, required) — UUID of the badge (converted to lowercase)
- `name` (string, required) — The name of the badge
- `description` (string, required) — The description of the badge
- `imageUrl` (string, uri, required) — The URL of the badge image
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `loyaltyConditions` (array, required) — Array of conditions of the badge
- `items` (object) — Schema for a loyalty condition rule.
- id: … (nested further)
- type: … (nested further)
- description: … (nested further)
- amount: … (nested further)
- repeatCount: … (nested further)
- requiredCount: … (nested further)
- loyaltyRuleGroupId: … (nested further)
- association: … (nested further)
- loyaltyCurrencyId: … (nested further)
- csvUrl: … (nested further)
- csvFiles: … (nested further)
- `rewards` (array) — Array of rewards granted when the badge is achieved
- `items` (object)
- id: … (nested further)
- rewardType: … (nested further)
- loyaltyCurrencyId: … (nested further)
- loyaltyCurrencyIds: … (nested further)
- amount: … (nested further)
- multiplier: … (nested further)
- isRetroactive: … (nested further)
- `loyaltyBadgeUsers` (array) — Array of user badges
- `items` (object)
- id: … (nested further)
- userId: … (nested further)
- websiteId: … (nested further)
- organizationId: … (nested further)
- hideInUi: … (nested further)
- status: … (nested further)
- loyaltyBadgeId: … (nested further)
- progress: … (nested further)
- dismissedInUi: … (nested further)
- user: … (nested further)
- createdAt: … (nested further)
- completedAt: … (nested further)
- updatedAt: … (nested further)
- `loyaltyRules` (array) — Array of loyalty rules associated with the badge
- `items` (object)
- id: … (nested further)
- name: … (nested further)
- `dataJobs` (array) — Array of data jobs associated with the badge
- `items` (object)
- id: … (nested further)
- status: … (nested further)
- message: … (nested further)
- createdAt: … (nested further)
- updatedAt: … (nested further)
- `createdAt` (string, date-time, required) — The date and time the badge was created
- `updatedAt` (string, date-time, required) — The date and time the badge was updated
- `hasNextPage` (boolean, required) — Indicates if there is a next page of results
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get users for a badge
`GET /api/loyalty/badge_users` — query parameters: `loyaltyBadgeId`, `organizationId`, `websiteId`, `walletAddress`, `status`, `startingAfter`, `limit`
`GET https://admin.snagsolutions.io/api/loyalty/badge_users`
This endpoint allows you to fetch users for a specific badge.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `loyaltyBadgeId` (string, required) — UUID of the loyalty badge (converted to lowercase)
- `organizationId` (string) — UUID of the organization (optional, converted to lowercase)
- `websiteId` (string) — UUID of the website (optional, converted to lowercase)
- `walletAddress` (string) — Wallet Address of the user, or array of wallet addresses.
- `status` (string) — Status of the badge
- `startingAfter` (string) — UUID of the badge to start after (converted to lowercase)
- `limit` (integer) — Number of badges to fetch
## Response 200
200
- response (object) — Response returned upon successful fetching of the badge users.
- `id` (string, uuid, required) — UUID of the user badge (converted to lowercase)
- `userId` (string, uuid, required) — UUID of the user (converted to lowercase)
- `loyaltyBadgeId` (string, uuid, required) — UUID of the badge (converted to lowercase)
- `websiteId` (string, uuid, required) — UUID of the website (converted to lowercase)
- `organizationId` (string, uuid, required) — UUID of the organization (converted to lowercase)
- `createdAt` (string, date-time, required) — The date and time the user badge was created
- `updatedAt` (string, date-time, required) — The date and time the user badge was updated
- `deletedAt` (string, date-time, required) — The date and time the user badge was deleted
- `status` (string, required, one of: active, inactive, revoked) — The status of the badge
- `progress` (array, required) — Array of progress for the badge
- `dismissedInUi` (boolean) — Whether the user has dismissed the badge in the UI
- `user` (object) — User associated with the user badge
- `walletAddress` (string, required) — Wallet address of the user
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Revoke badge from account
`POST /api/loyalty/badges/{id}/revoke` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/badges/{id}/revoke`
This endpoint allows you to revoke a badge from an account
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required) — UUID of the badge (converted to lowercase)
## Request body
- Content type: `application/json`
- request body (object) — Schema for a update user badge schema
- `userId` (string, uuid) — UUID of the user (converted to lowercase)
- `walletAddress` (string) — Validated and formatted wallet address
- `status` (string, one of: active, inactive, revoked) — Status of the badge
## Response 200
200
- response (object) — Response returned upon successful update of the user badge
- `message` (string, required) — The message returned upon update of the badge
- `data` (object, required)
- `id` (string, uuid, required)
- `userId` (string, uuid, required)
- `loyaltyBadgeId` (string, uuid, required)
- `status` (string, required, one of: active, inactive, revoked)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Reward badge for account
`POST /api/loyalty/badges/{id}/reward` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/badges/{id}/reward`
This endpoint allows you to reward a badge for an account
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required) — UUID of the badge (converted to lowercase)
## Request body
- Content type: `application/json`
- request body (object) — Schema for a update user badge schema
- `userId` (string, uuid) — UUID of the user (converted to lowercase)
- `walletAddress` (string) — Validated and formatted wallet address
- `status` (string, one of: active, inactive, revoked) — Status of the badge
## Response 200
200
- response (object) — Response returned upon successful update of the user badge
- `message` (string, required) — The message returned upon update of the badge
- `data` (object, required)
- `id` (string, uuid, required)
- `userId` (string, uuid, required)
- `loyaltyBadgeId` (string, uuid, required)
- `status` (string, required, one of: active, inactive, revoked)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Update badge (by id)
`POST /api/loyalty/badges/{id}` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/badges/{id}`
This endpoint allows you to update an existing badge.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required) — UUID of the badge to update (converted to lowercase)
## Request body
- Content type: `application/json`
- request body (object) — Schema for updating an existing badge.
- `name` (string) — The name of the badge
- `description` (string) — The description of the badge
- `imageUrl` (string, uri) — The URL of the badge image
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `rewards` (array) — Array of rewards granted when the badge is achieved
- `items` (object)
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of reward (points or multiplier)
- `loyaltyCurrencyId` (string, uuid) — UUID of the loyalty currency for this reward
- `loyaltyCurrencyIds` (array) — Array of loyalty currency IDs for multiplier rewards (empty means all currencies)
- `items` (string, uuid)
- `amount` (number) — Points amount to reward (for points reward type)
- `multiplier` (number) — Multiplier to reward (for multiplier reward type)
- `isRetroactive` (boolean, default: false) — Whether the multiplier should be applied to the user's existing balance
- `csvFiles` (array)
- `items` (object)
- `src` (string, uri, required)
- `name` (string)
- `description` (string)
## Response 200
200
- response (object) — Response returned upon successful update of the badge.
- `id` (string, uuid, required) — UUID of the badge (converted to lowercase)
- `name` (string, required) — The name of the badge
- `description` (string, required) — The description of the badge
- `imageUrl` (string, uri, required) — The URL of the badge image
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `loyaltyConditions` (array, required) — Array of conditions of the badge
- `items` (object) — Schema for a loyalty condition rule.
- `id` (string, uuid, required) — UUID of the condition (converted to lowercase)
- `type` (string, required, one of: rule, rules, points, section, level, api, badge, badges, csv) — The type of the condition
- `description` (string, required) — The description of the condition
- `amount` (number, required) — The amount of the condition
- `repeatCount` (number, required) — The repeat count of the condition
- `requiredCount` (number, required) — The required count of the condition
- `loyaltyRuleGroupId` (string, uuid, required) — UUID of the rule group (converted to lowercase)
- `association` (array, required)
- `items` (object)
- loyaltyRule: … (nested further)
- loyaltyBadge: … (nested further)
- loyaltyRuleGroup: … (nested further)
- loyaltyCurrency: … (nested further)
- loyaltyLeaderboardView: … (nested further)
- `loyaltyCurrencyId` (string, uuid, required) — UUID of the currency (converted to lowercase)
- `csvUrl` (string, uri, required) — URL of the CSV file
- `csvFiles` (array) — Array of CSV files associated with the condition
- `items` (object)
- id: … (nested further)
- url: … (nested further)
- name: … (nested further)
- description: … (nested further)
- `rewards` (array) — Array of rewards granted when the badge is achieved
- `items` (object)
- `id` (string, uuid, required) — UUID of the badge reward
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of reward (points or multiplier)
- `loyaltyCurrencyId` (string, uuid, required) — UUID of the loyalty currency for this reward
- `loyaltyCurrencyIds` (array) — Array of loyalty currency IDs for multiplier rewards (empty means all currencies)
- `items` (string, uuid)
- `amount` (number, required) — Points amount to reward (for points reward type)
- `multiplier` (number, required) — Multiplier to reward (for multiplier reward type)
- `isRetroactive` (boolean, required) — Whether the multiplier should be applied to the user's existing balance
- `loyaltyBadgeUsers` (array) — Array of user badges
- `items` (object)
- `id` (string, uuid, required) — UUID of the user badge (converted to lowercase)
- `userId` (string, uuid, required) — UUID of the user (converted to lowercase)
- `websiteId` (string, uuid, required) — UUID of the website (converted to lowercase)
- `organizationId` (string, uuid, required) — UUID of the organization (converted to lowercase)
- `hideInUi` (boolean, default: false) — Whether to hide this badge in the user interface
- `status` (string, required, one of: active, inactive, revoked) — The status of the badge
- `loyaltyBadgeId` (string, uuid, required) — UUID of the badge (converted to lowercase)
- `dismissedInUi` (boolean) — Whether the user has dismissed the badge in the UI
- `user` (object) — User associated with the user badge
- `id` (string, uuid, required) — UUID of the user (converted to lowercase)
- `walletAddress` (string, required) — Wallet address of the user
- `createdAt` (string, date-time, required) — The date and time the user badge was created
- `completedAt` (string, date-time) — The date and time the user badge was granted
- `updatedAt` (string, date-time, required) — The date and time the user badge was updated
- `loyaltyRules` (array) — Array of loyalty rules associated with the badge
- `items` (object)
- `id` (string, uuid, required) — UUID of the loyalty rule (converted to lowercase)
- `name` (string, required) — The name of the loyalty rule
- `dataJobs` (array) — Array of data jobs associated with the badge
- `items` (object)
- `id` (string, uuid, required) — UUID of the data job (converted to lowercase)
- `status` (string, required, one of: pending, processing, complete, failed, cancelled) — The status of the data job
- `message` (string) — The message associated with the data job
- `createdAt` (string, date-time, required) — The date and time the data job was created
- `updatedAt` (string, date-time, required) — The date and time the data job was updated
- `createdAt` (string, date-time, required) — The date and time the badge was created
- `updatedAt` (string, date-time, required) — The date and time the badge was updated
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create a contract
`POST /api/contracts`
`POST https://admin.snagsolutions.io/api/contracts`
Create a contract
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `name` (string, required)
- `address` (string, required) — The address of the contract
- `chainId` (number, required)
- `type` (string, required, one of: erc20, erc721, erc1155, custom)
- `organizationId` (string, uuid, required)
- `websiteId` (string, uuid, required)
- `iconUrl` (string, uri)
## Response 201
201
- response (object) — Schema for a contract
- `id` (string, uuid, required) — Unique identifier for the contract
- `name` (string, required) — Name of the contract
- `address` (string, required) — Contract address matching the required format
- `chainId` (integer, int64, required) — ID of the blockchain chain
- `type` (string, required, one of: erc20, erc721, erc1155, custom) — Type of the contract
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `iconUrl` (string, uri) — Optional icon image for the contract; used as the reward token icon
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
---
## Delete a contract
`DELETE /api/contracts/{id}` — path parameters: `id`
`DELETE https://admin.snagsolutions.io/api/contracts/{id}`
Delete a contract
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Response 200
200
- response (object)
- `message` (string, required)
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
---
## Get contracts
`GET /api/contracts` — query parameters: `search`, `organizationId`, `websiteId`, `cursor`, `includeAbi`, `id`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/contracts`
Get contracts
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `search` (string)
- `organizationId` (string, required)
- `websiteId` (string, required)
- `cursor` (string)
- `includeAbi` (boolean)
- `id` (string)
- `limit` (integer) — Number of contracts to return
- `startingAfter` (string) — Pagination cursor to start after a specific contract ID
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object) — Schema for a contract
- `id` (string, uuid, required) — Unique identifier for the contract
- `name` (string, required) — Name of the contract
- `address` (string, required) — Contract address matching the required format
- `chainId` (integer, int64, required) — ID of the blockchain chain
- `type` (string, required, one of: erc20, erc721, erc1155, custom) — Type of the contract
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `iconUrl` (string, uri) — Optional icon image for the contract; used as the reward token icon
- `hasNextPage` (boolean, required)
---
## Update a contract
`POST /api/contracts/{id}` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/contracts/{id}`
Update a contract
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `name` (string)
- `type` (string, one of: erc20, erc721, erc1155, custom)
- `iconUrl` (string, uri)
## Response 200
200
- response (object) — Schema for a contract
- `id` (string, uuid, required) — Unique identifier for the contract
- `name` (string, required) — Name of the contract
- `address` (string, required) — Contract address matching the required format
- `chainId` (integer, int64, required) — ID of the blockchain chain
- `type` (string, required, one of: erc20, erc721, erc1155, custom) — Type of the contract
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `iconUrl` (string, uri) — Optional icon image for the contract; used as the reward token icon
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
---
## Get page sections
`GET /api/page_sections` — query parameters: `websiteId`, `organizationId`, `placement`, `pageId`, `all`, `pageSectionType`
`GET https://admin.snagsolutions.io/api/page_sections`
This endpoint allows you to fetch page sections
## Authentication
No authentication required.
## Query parameters
- `websiteId` (string, required) — Unique identifier for the website to filter by
- `organizationId` (string, required) — Unique identifier for the organization to filter by
- `placement` (string, required) — Placement of the page section
- `pageId` (string) — Unique identifier for the page to filter by
- `all` (string) — Whether to get all sections regardless of duration
- `pageSectionType` (string) — Type of the page section to filter by
## Response 200
200
- response (object)
- `id` (string, uuid, required) — Unique identifier for the page section
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `title` (string) — Title of the page section
- `pageSectionType` (string, one of: all_items, discovery_item, discovery_items_grid) — Type of the page section
- `placement` (string, required, one of: global, page) — Placement of the page section
- `sortId` (number) — Order of the page section
- `pageId` (string, uuid) — ID of the page this section belongs to
- `durationStart` (string, date-time, required) — Start time of the page section visibility
- `durationEnd` (string, date-time) — End time of the page section visibility
- `banner` (object)
- `id` (string, uuid, required)
- `websiteCollectionBanners` (array, required)
- `items` (object)
- `id` (string, uuid, required)
- `carousel` (object)
- `id` (string, uuid, required)
- `carouselItems` (array, required)
- `items` (object)
- id: … (nested further)
- sortId: … (nested further)
- `itemsGrids` (array)
- `items` (object)
- `id` (string, uuid, required)
- `discoveryItems` (array)
- `items` (object)
- `id` (string, uuid, required)
- `title` (string, required)
- `description` (string)
- `layout` (string, one of: container_style, full_width_hero)
- `loyaltySection` (string)
- `pageMediaLink` (string)
- `mediaLink` (string)
- `website` (string)
- `twitter` (string)
- `loyaltyRuleGroupId` (string)
- `sortId` (number)
- `buttonText` (string)
- `externalLink` (string)
- `hideQuestInfo` (boolean, default: false)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Complete Loyalty Rule
`POST /api/loyalty/rules/{id}/complete` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/rules/{id}/complete`
Verify a loyalty rule based on user action and reward them if applicable. This endpoint currently only supports following rule types: code_entry, text_input, link_click, external_rule, connect_wallet, check_in, connected_twitter, drip_x_follow, drip_x_new_tweet, drip_x_text_in_bio, drip_x_text_in_name, drip_x_text_in_comment, drip_x_tweet, connected_phone, instagram_post, instagram_follow, connected_steam, steam_wishlist, connected_epic, connected_email, profile_completed, connected_telegram, telegram_messages, telegram_join, connected_discord, discord_member, DiscordMessages, discord_join, discord_role_grant, quiz, poll, TokenHold, token_hold_erc20, connected_github, github_repo_star, github_repo_fork, github_repo_collaborator, connected_youtube, youtube_subscribers, youtube_comment, connected_reddit, reddit_comment, connected_instagram, points_airdrop, shopify_spend, swap, connected_tiktok, tiktok_post, tiktok_follow, post_impressions, stratus_function, dapps_by_gas_spent, dapps_by_active_users, dapps_by_new_users
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object) — Schema for completing a loyalty rule
- `verificationCode` (string) — Optional verification code for completing the loyalty rule
- `userId` (string, uuid) — Unique identifier for the user
- `walletAddress` (string) — Wallet address of the user can only be used if userId is not provided
- `contentUrl` (string) — Link to the post/comment made by user
- `loyaltyQuestionChoiceId` (string) — ID of the choice selected by the user
- `verifyOnly` (string, default: "false") — Flag indicating if only verification is required, this will not create a transaction and reward the user.
- `rangeValue` (number) — Value to compare with the range
- `amount` (number) — Override amount for the reward (rounded to nearest whole number). This will override the rule amount and reward the integer passed.
- `idempotencyKey` (string) — Unique key to ensure idempotent requests.
- `skipChainOrRequiredCheck` (boolean) — Flag indicating if the chain or required check should be skipped.
## Response 200
200
- response (object)
- `message` (string, required, one of: Completion request added to queue, Link click being verified, come back later to check the status)
## Response 304
304
- response (object)
- `message` (string, required, one of: Completion request already in queue)
## Response 400
400
- response (object)
- `message` (string, required, one of: loyaltyRuleId must be a uuid, Loyalty rule {NAME} has expired, Loyalty rule {NAME} has not started yet, Only external rules can have amount override, Only external rules can have idempotency key override, Loyalty currency not found, Api key is required to skip chain or required check, Access restricted. Your account is blocked., Please complete other required rules to unlock this quest., Unfortunately, this quest is only available for new users, Unfortunately, we couldn't verify the code you entered. Please ensure it's correct and try again, Invalid Verification, try again, Quest not completed, You have already clicked the link, please wait for the reward to be processed, Check-in amount rule cannot be completed when amount is 0, Twitter not connected, Phone not connected or not verified, Instagram not connected, Telegram not connected, Discord not connected, Epic not connected, Email not verified, GitHub not connected, Youtube not connected, Reddit not connected, Comment link is required, TikTok not connected, Twitter not connected or content url is required, Steam not connected, Email not connected or not verified, No choice selected, Question not found, Invalid choice selected, Claim on token hold is not supported for Sui, Claim on token hold is not supported for paid full royalty or native purchases, Claim on token hold is only supported for multipliers, Rule is not setup, Quest not achieved, please try again., Invalid request) — Categorized examples of messages that may be returned for a loyalty rule's processing result. possible messages: 'loyaltyRuleId must be a uuid', 'Loyalty rule {NAME} has expired', 'Loyalty rule {NAME} has not started yet', 'Only external rules can have amount override', 'Only external rules can have idempotency key override', 'Loyalty currency not found', 'Api key is required to skip chain or required check', 'Access restricted. Your account is blocked.', 'Please complete other required rules to unlock this quest.', 'Unfortunately, this quest is only available for new users', "Unfortunately, we couldn't verify the code you entered. Please ensure it's correct and try again", 'Invalid Verification, try again', 'Quest not completed', 'You have already clicked the link, please wait for the reward to be processed', 'Check-in amount rule cannot be completed when amount is 0', 'Twitter not connected', 'Phone not connected or not verified', 'Instagram not connected', 'Telegram not connected', 'Discord not connected', 'Epic not connected', 'Email not verified', 'GitHub not connected', 'Youtube not connected', 'Reddit not connected', 'Comment link is required', 'TikTok not connected', 'Twitter not connected or content url is required', 'Steam not connected', 'Email not connected or not verified', 'No choice selected', 'Question not found', 'Invalid choice selected', 'Claim on token hold is not supported for Sui', 'Claim on token hold is not supported for paid full royalty or native purchases', 'Claim on token hold is only supported for multipliers', 'Rule is not setup', 'Quest not achieved, please try again.', 'Invalid request'
## Response 401
401
- response (object)
- `message` (string, required, one of: Unauthorized, Invalid token)
## Response 403
403
- response (object)
- `message` (string, required, one of: Failed to claim, limit reached for the website, The rewards program is temporarily paused, so claiming is currently unavailable. Please try again later.)
## Response 404
404
- response (object)
- `message` (string, required, one of: Loyalty rule not found, Loyalty currency not found, User not found)
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
## Response 500
500
- response (object)
- `message` (string, required, one of: An error occurred while verifying the link click, Internal server error)
---
## Create a loyalty currency
`POST /api/loyalty/currencies`
`POST https://admin.snagsolutions.io/api/loyalty/currencies`
Create a loyalty currency
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for creating a new loyalty currency
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `imageUrl` (string) — URL of the image; must start with AWS S3 asset URL base
- `name` (string, required) — Name of the loyalty currency
- `symbol` (string, required) — Symbol representing the loyalty currency
- `decimals` (number, required) — Number of decimal places for the currency
- `apiGetUrl` (string) — Optional API GET URL for fetching data
- `apiGetTransactionEntryUrl` (string) — Optional API GET URL for fetching transaction entry data
- `apiPostUrl` (string) — Optional API POST URL for sending data
- `sortId` (number) — Optional sorting identifier
- `apiAuthType` (string, one of: none, basic, bearer, apiKey) — Type of authentication used for API
- `apiAuthKey` (string) — Authentication key for API
- `apiAuthValue` (string) — Authentication value for API
## Response 200
200
- response (object) — Schema for a loyalty currency
- `id` (string, uuid, required) — Unique identifier for the loyalty currency
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `imageUrl` (string, required) — URL of the image
- `name` (string, required) — Name of the loyalty currency
- `symbol` (string, required) — Symbol representing the loyalty currency
- `decimals` (number, required) — Number of decimal places for the currency
- `createdById` (string, uuid, required) — ID of the user who created the currency
- `createdAt` (string, date-time, required) — Timestamp when the currency was created
- `updatedAt` (string, date-time, required) — Timestamp when the currency was last updated
- `apiGetUrl` (string, required) — API GET URL for fetching data
- `apiGetTransactionEntryUrl` (string, required) — API GET URL for fetching transaction entry data
- `apiPostUrl` (string, required) — API POST URL for sending data
- `apiAuthType` (string, required, one of: none, basic, bearer, apiKey) — Type of authentication used for API
- `apiAuthKey` (string, required) — Authentication key for API
- `apiAuthValue` (string, required) — Authentication value for API
- `sortId` (number, required) — Sorting identifier
- `deletedAt` (string, date-time, required) — Timestamp when the currency was deleted, if applicable
- `isTokenRewardCurrency` (boolean) — True for LoyaltyCurrency rows auto-created by resolveTokenReward (Option 1 hidden currency backing a token-reward rule). Excluded from list responses by default; opt in via excludeTokenRewardCurrencies=false on the list endpoint.
## Response 400
400
- response (object)
- `error` (string, required)
## Response 403
403
- response (object)
- `error` (string, required)
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create Loyalty Multiplier
`POST /api/loyalty/multipliers`
`POST https://admin.snagsolutions.io/api/loyalty/multipliers`
Create a new loyalty multiplier for a wallet.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for creating a loyalty multiplier
- `walletAddress` (string, required)
- `multiplier` (number, required)
- `title` (string)
- `description` (string)
- `externalIdentifier` (string)
## Response 200
200
- response (object) — Schema for loyalty multiplier
- `id` (string, uuid, required)
- `userId` (string, uuid, required)
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `multiplier` (number, required)
- `title` (string, required)
- `description` (string, required)
- `externalIdentifier` (string, required)
- `createdAt` (string, required)
- `updatedAt` (string, required)
- `deletedAt` (string, required)
- `user` (object, required) — Schema for a loyalty multiplier user
- `id` (string, uuid, required)
- `walletAddress` (string, required)
- `loyaltyRuleId` (string, uuid)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Create Loyalty Rule Chain
`POST /api/loyalty/rule_chains` — query parameters: `organizationId`, `websiteId`
`POST https://admin.snagsolutions.io/api/loyalty/rule_chains`
Create a new loyalty rule chain
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string, required)
- `websiteId` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `name` (string, required)
- `loyaltyRuleId` (string, uuid, required) — Unique identifier for the loyalty rule
- `operatorType` (string, one of: AND, OR, default: "AND") — Type of the condition. AND if all conditions must be met, OR if any condition can be met.
- `loyaltyConditions` (array, required) — Array of conditions for the rule chain
- `items` (object)
- `type` (string, one of: rule, rules, points, badge, badges) — The type of rule
- `description` (string) — The description of the rule
- `amount` (number) — The amount of the rule
- `repeatCount` (number) — The number of times the rule should be repeated to fulfill the condition
- `requiredCount` (number) — The number of rules that need to be fulfilled to satisfy the condition
- `loyaltyRuleChainId` (string, uuid) — UUID of the loyalty rule chain (optional, converted to lowercase)
- `loyaltyRuleIds` (array) — Array of UUIDs of the rules (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyRuleGroupIds` (array) — Array of UUIDs of the rule group (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyLeaderboardViewIds` (array) — Array of UUIDs of the Loyalty Leaderboard (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyCurrencyIds` (array) — Array of UUIDs of the currency (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyBadgeIds` (array) — Array of UUIDs of the badge (optional, converted to lowercase)
- `items` (string, uuid)
- `csvUrl` (string, uri) — URL of the CSV file (optional)
## Response 200
200
- response (object)
- `data` (object, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Create Loyalty Rule Groups
`POST /api/loyalty/rule_groups/create`
`POST https://admin.snagsolutions.io/api/loyalty/rule_groups/create`
Create a new loyalty rule groups
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for a create loyalty rule groups body
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `name` (string, required)
- `subTitle` (string)
- `isCollapsible` (boolean, default: false)
- `isRequired` (boolean, default: false)
- `sortId` (number)
## Response 200
200
- response (object)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `id` (string, required)
- `name` (string, required)
- `isRequired` (boolean, required)
- `sortId` (number, required)
- `isCollapsible` (boolean, required)
- `subTitle` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create Loyalty Rule
`POST /api/loyalty/rules`
`POST https://admin.snagsolutions.io/api/loyalty/rules`
Create a new Loyalty Rule
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for creating a new loyalty rule with detailed validations
- `organizationId` (string, uuid, required) — Unique identifier for the organization creating the rule
- `websiteId` (string, uuid, required) — Unique identifier for the associated website
- `backgroundAssetUrl` (string) — URL of the background asset to be displayed
- `name` (string, required) — Name of the loyalty rule
- `description` (string) — Optional detailed description of the rule
- `network` (string, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence, default: "mainnet") — Blockchain network where the rule will apply
- `type` (string, required, one of: Bonus, BoughtOn, BoughtWithRoyalties, DiscordMessages, Listing, Lodging, PoapClaim, SnagSocialsConnected, SnapshotFullset, SnapshotHold, SoldWithRoyalties, TwitterBio, TwitterHashTagPost, MintOn, SoldOn, TokenHold, Retweet, check_in, code_entry, connect_wallet, connected_discord, connected_email, connected_epic, connected_github, connected_steam, connected_telegram, connected_twitter, create_partner_account, discord_member, drip_x_follow, drip_x_new_tweet, drip_x_text_in_bio, drip_x_text_in_comment, drip_x_text_in_name, drip_x_tweet, external_rule, link_click, manual_upload, profile_completed, referred_user, smart_contract_event, snapshot_governance, telegram_join, telegram_messages, text_input, token_hold_erc20, tweet_liked_by_project, twitter_comment, twitter_follow, twitter_followers, twitter_like, twitter_post_hashtag, quiz, poll, steam_wishlist, liquidity_uniswap_v2, liquidity_uniswap_v3, points_airdrop, youtube_subscribers, youtube_comment, shopify_spend, swap, tiktok_post, post_impressions, discord_join, connected_youtube, stratus_function, connected_tiktok, tiktok_follow, github_repo_star, github_repo_fork, github_repo_collaborator, github_merge_PR, discord_role_grant, connected_reddit, reddit_comment, robinhood_token_hold, connected_instagram, instagram_post, instagram_follow, connected_phone, dapps_by_gas_spent, dapps_by_active_users, dapps_by_new_users, dapps_by_transaction_count, dapps_by_user_retention) — Type of loyalty rule being created
- `frequency` (string, required, one of: none, once, hourly, daily, weekly, monthly, immediately) — Execution frequency of the loyalty rule
- `interval` (string, required, one of: hourly, daily, weekly, monthly, once, custom, every_message, unlimited) — Interval between rule executions
- `startTime` (string, date-time, required) — When the rule becomes active
- `endTime` (string, date-time, required) — When the rule becomes inactive
- `effectiveStartTime` (required) — The effective start time of the rule
- (oneOf)
- value (string, date-time)
- value (string, null)
- `effectiveEndTime` (required) — The effective end time of the rule
- (oneOf)
- value (string, date-time)
- value (string, null)
- `amount` (required) — Reward amount associated with the rule
- `customRewardsCsvUrl` (string, uri) — URL for uploading custom rewards via CSV
- `customRewardsApiUrl` (string, uri) — URL for fetching custom rewards
- `subscriptionId` (string) — Optional stratus subscription id for the rule
- `functionId` (string) — Optional stratus function id for the rule
- `metadata` (object, required) — Additional metadata for the loyalty rule
- `isCheckInOnEveryTxn` (boolean) — If enabled, the first transaction done on the platform will complete this rule
- `hasPaidFullRoyalty` (boolean) — Indicates if the full royalty has been paid for items.
- `royaltyAddress` (string) — Wallet address of the user can only be used if userId is not provided
- `royaltyPercentage` (number) — Royalty percentage of the item.
- `hasVerifiedTwitter` (boolean) — Indicates if the user has a verified Twitter account.
- `onlyRewardExistingUser` (boolean) — Indicates if only existing users are rewarded.
- `hasNeverSold` (boolean) — Indicates if the item has never been sold.
- `onlyNative` (boolean) — Flag indicating whether to include only native tokens.
- `hasFillSource` (boolean) — Flag indicating whether the fill source is included.
- `fillSource` (string) — Fill source of the order for the token sale
- `liquidity` (object) — Liquidity pool details.
- `network` (string, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence) — Blockchain network of the liquidity pool.
- `protocol` (string) — Protocol of the liquidity pool.
- `onlyRewardInRangeLiquidity` (boolean) — Indicates if only in-range liquidity is rewarded.
- `liquidityPerDay` (number) — Liquidity provided per day in USD
- `customFunction` (string) — Custom function to calculate the the reward amount based on the liquidity provided per day. X is the reward amount, Y is the liquidity provided per day in USD.
- `calculationType` (string, one of: fixed, custom) — Calculation type of the liquidity pool.
- `pools` (array) — Array of liquidity pools associated with the rule.
- `items` (object)
- id: … (nested further)
- `twitterUsername` (string) — Twitter username of the user.
- `twitterUserId` (string) — Unique identifier of the Twitter user.
- `twitterHashtag` (string) — Hashtag associated with the Twitter post.
- `enableVerifiedMultiplier` (boolean) — Flag indicating whether the verified multiplier is enabled.
- `verifiedMultiplier` (number) — Multiplier applied to rewards for verified users.
- `isMultiplierApplied` (boolean) — Indicates if the multiplier has been applied to rewards.
- `onlyKnownUsers` (boolean) — Flag indicating whether to include only known users.
- `firstReferralReward` (number) — Percentage reward given to a user for their first referral.
- `referrerReward` (number) — Lump sum reward given to a referrer.
- `referrerRewardLoyaltyCurrencyId` (string) — Loyalty currency ID of the referrer reward.
- `secondReferralReward` (number) — Percentage reward given for a second-level referral.
- `enableStreaks` (boolean) — Flag indicating whether streaks are enabled.
- `promoCode` (string) — Promotional code associated with the rule.
- `promoCodeType` (string, one of: code, csv, generate) — Type of the promotional code.
- `promoCodeLength` (number) — Numbers of the promotional code to be generated.
- `promoCodeCsvUrl` (string) — URL of the CSV file containing promo codes.
- `enableJoinDiscordServers` (boolean) — Flag indicating whether joining Discord servers is required.
- `discordServersToJoin` (array) — Array of Discord servers, channels, and roles to join.
- `items` (object)
- `id` (string) — ID of the Discord server to join.
- `channels` (array) — Array of Discord channels to join.
- items: … (nested further)
- `roles` (array) — Array of roles to assign in the Discord server.
- items: … (nested further)
- `streakArray` (array) — Array of streak milestones and corresponding rewards.
- `items` (object)
- `streakMilestone` (number, required) — Milestone required to achieve the streak.
- `streakAmount` (number, required) — Reward amount for achieving the streak milestone.
- `dripQuestsToComplete` (array) — Array of drip quests required to complete the rule.
- `items` (object)
- `id` (string, required) — ID of the drip quest to complete.
- `collection` (array) — Array of collections associated with the rule.
- `items` (object)
- `address` (string) — Blockchain address of the collection.
- `network` (string, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence) — Blockchain network of the collection.
- `multiplier` (number) — Multiplier applied to the rewards for this collection.
- `range` (array) — Array defining ranges and corresponding rewards.
- `items` (object)
- `startRange` (number, required) — Start value of the range.
- `endRange` (number, required) — End value of the range.
- `amount` (required) — Reward amount for this range.
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `loyaltyBadgeId` (string, uuid) — ID of the loyalty badge for this range.
- `loyaltyMultiplierAmount` (number) — Amount of the loyalty multiplier for this range.
- `customRewardsApiKey` (string) — API key for custom rewards integration.
- `link` (string) — Optional link associated with the metadata.
- `socialPlatform` (string, one of: Custom, Discord, EpicGames, Instagram, Phone, Steam, Telegram, TikTok, Twitch, X(Twitter), YouTube, Google, GitHub, Reddit) — Social media platform associated with the rule.
- `socialPlatformName` (string) — Name of the social platform.
- `socialPlatformLogo` (string) — URL of the social platform's logo.
- `rewardPerImpression` (boolean) — Flag indicating if rewards are given per impression.
- `snapshotProposals` (array) — Array of snapshot proposals for the rule.
- `items` (object)
- `id` (string, required) — ID of the snapshot proposal.
- `space` (string, required) — Space associated with the snapshot proposal.
- `cta` (object) — Object containing details for the call-to-action.
- `label` (string) — Label for the call-to-action.
- `href` (string) — Link for the call-to-action.
- `hasSaleCurrency` (boolean) — Flag indicating if the sale currency is included.
- `isTokenHoldMultiplier` (boolean) — Flag indicating if the token hold multiplier is applied.
- `saleCurrency` (string) — Currency associated with sales.
- `isRetroactive` (boolean) — Flag indicating if rewards are applied retroactively.
- `timeDelayToVerifySeconds` — Time delay in seconds to verify actions.
- (oneOf)
- value (string)
- value (number)
- `referralRequirements` (object) — Object defining referral requirements.
- `points` (object)
- `amount` (number) — Points required for referral.
- `loyaltyCurrecyId` (string) — ID of the loyalty currency for referral.
- `achievePoints` (boolean) — Flag indicating if achieving points is required.
- `completeProfile` (boolean) — Flag indicating if completing the profile is required.
- `connectTwitter` (boolean) — Flag indicating if connecting Twitter is required.
- `connectDiscord` (boolean) — Flag indicating if connecting Discord is required.
- `connectEmail` (boolean) — Flag indicating if connecting email is required.
- `multiplierLoyaltyCurrencyIds` (array) — Array of loyalty currency IDs used for multipliers.
- `items` (string)
- `verifyPlaceHolderText` (string) — Placeholder text for verification input fields.
- `verificationTextMinimumLength` (integer) — Minimum length of the verification text.
- `buttonText` (string) — Text displayed on the action button.
- `telegramChannelId` (string) — ID of the Telegram channel.
- `twitterAccountUrl` (string) — URL of the associated Twitter account.
- `twitterPostUrl` (string) — URL of the associated Twitter post.
- `checkLike` (boolean) — Flag indicating if liking the post is required.
- `checkRepost` (boolean) — Flag indicating if reposting is required.
- `checkComment` (boolean) — Flag indicating if commenting is required.
- `maxLikes` (integer) — Reaction limit for likes. Once the post reaches this many likes, the Like requirement is hidden from users. Likes are never verified server-side, so this only controls display.
- `maxReposts` (integer) — Reaction limit for reposts. Once the post reaches this many reposts, the Repost requirement is hidden and no longer verified.
- `maxComments` (integer) — Reaction limit for comments. Once the post reaches this many comments, the Comment requirement is hidden and no longer verified.
- `reactionStats` (object) — Latest scraped public reaction counts for the post, written back from social-service (throttled). Drives which reaction requirements are hidden once they hit their configured limit. Not admin-editable.
- `likeCount` (number)
- `repostCount` (number)
- `commentCount` (number)
- `syncedAt` (string)
- `checkText` — Text to check in the Twitter post, username, or bio.
- (oneOf)
- value (string)
- value (array)
- items: … (nested further)
- `requirePostLink` (boolean) — Flag indicating if the post link is required.
- `requirePostMediaLink` (boolean) — Flag indicating if media metadata is required.
- `preGeneratedPostText` (string) — Pre-generated text template that will be used to prefill the post content.
- `rewardCriteria` (string, one of: IMPRESSIONS_COUNT, ELIGIBLE_POST) — Criteria to evaluate the reward.
- `smartContract` (object) — Object containing details of the associated smart contract.
- `contractId` (string) — ID of the smart contract.
- `event` (string) — Event emitted by the smart contract.
- `type` (string, one of: function, event, default: "event") — Type of the smart contract interaction.
- `criteria` (string, one of: everyEvent, byParameter) — Criteria to evaluate the smart contract event.
- `params` (array) — Array of parameters for the smart contract.
- `items` (object)
- name: … (nested further)
- condition: … (nested further)
- value: … (nested further)
- `addressMapping` (string) — Mapping of addresses for the smart contract.
- `amountMultiplier` (object) — Object containing details of the amount multiplier from the event.
- `valueMapping` (string) — Mapping of the value for the smart contract.
- `walletType` (string, one of: evm, solana, imx, sui, ton, cosmos, ultra, agw, flow_cadence, substrate) — Type of wallet associated with the rule.
- `imageUrl` (string) — URL of the image associated with the rule.
- `onlyRewardSingleTokenOwnership` (boolean) — give points for only one token ownership per contract
- `skipMultiplier` (boolean) — Flag indicating if the multiplier is skipped.
- `completeProfileConditions` (object) — Conditions for completing the profile.
- `steamAppId` (string) — ID of the Steam app.
- `githubRepoUrl` (string) — URL of the GitHub repository to check for star.
- `githubBranchName` (string) — Name of the GitHub branch to check for PR merge.
- `resetProgressOnClaim` (boolean) — If enabled, the progress tracking row will be deleted after a successful claim, allowing the user to re-earn the rule.
- `rewardPerAction` (boolean) — Flag indicating if the reward is rewarded per action.
- `rewardByBatch` (boolean) — Flag indicating if the reward is rewarded by batch.
- `batchSize` (number) — Number of tokens per batch.
- `youtubeVideoId` (string) — ID of the Youtube video.
- `youtubeChannelId` (string) — ID of the Youtube channel.
- `redditPostId` (string) — ID of the Reddit post.
- `isRestrictedToNewUsers` (boolean) — Flag indicating if the rule is restricted to new users.
- `allowMultipleRedemptions` (boolean) — Flag indicating if the rule allows multiple redemptions. Applies to Enter a Code only.
- `rewardBadgePerRange` (boolean) — Flag indicating if the rule can also reward badges per range.
- `swap` (object) — Metadata for swap loyalty rules
- `provider` (string, one of: any, relay, lifi)
- `relayReferrerId` (string)
- `from` (object)
- `mode` (string, one of: any, specific)
- `chain`
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- value: … (nested further)
- `tokens` (array)
- items: … (nested further)
- `trackAmount` (boolean)
- `to` (object)
- `mode` (string, one of: any, specific)
- `chain`
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- value: … (nested further)
- `tokens` (array)
- items: … (nested further)
- `trackAmount` (boolean)
- `requireCrossChainSwap` (boolean)
- `rewardPerValue` (boolean) — Flag indicating if the rule should reward based on value of traded tokens instead of count.
- `conditionDescription` (string) — Description of the external rule condition (only for external rules)
- `directRpc` (boolean) — Flag indicating if the rule should use direct RPC to get the balance of tokens.
- `erc20Type` (string, one of: erc20, native) — Type of ERC20 token for the loyalty rule.
- `rewardQualityPosts` (boolean) — Flag indicating if the rule should reward quality posts.
- `minimumFollowerCount` (number) — Minimum follower count for the rule. Accepts both number and string values.
- `autoCheckInRuleIds` (array) — Array of loyalty rule IDs that count as check-in when completed. If ["any"] then any rule completions count as check-in.
- `items`
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `robinhoodSymbols` (array)
- `items` (string)
- `collectionAddress` (string) — Blockchain address of the associated collection
- `collections` (array) — List of associated collections
- `items` (object)
- `address` (string, required) — Blockchain address of the collection
- `network` (string, required, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence) — Blockchain network for the collection
- `symbol` (string) — Symbol of the collection.
- `hideInUi` (boolean, default: false) — Whether to hide this rule in the user interface
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `showBeforeStart` (boolean, default: false) — Whether to show this rule before the start time
- `isRequired` (boolean, default: false) — Whether this rule is mandatory
- `oauthCredentialsId` (string, uuid) — OAuth credentials associated with the rule
- `loyaltyCurrencyId` (string, uuid) — Unique identifier for the loyalty currency. Optional when rewardType is token and tokenReward is provided; backend will find or create the currency from the selected contract.
- `tokenReward` (object) — When rewardType is token, provide relayer and either an ERC-20 contract or isNative=true to find or create loyalty currency and onchain token.
- `relayerId` (string, uuid, required) — Stratus relayer for token distribution (same network as contract)
- `contractId` (string, uuid) — ERC-20 contract to reward with; must match relayer network. Required unless isNative is true.
- `isNative` (boolean) — When true, reward is the chain native gas token (ETH, MATIC, BNB, …) and contractId is omitted.
- `loyaltyRuleGroupId` — Unique identifier for the loyalty rule group
- (oneOf)
- value (string, one of: no-section)
- value (string, uuid)
- `rewardType` (string, one of: points, multiplier, badge, token, default: "points") — Type of reward issued by the rule
- `loyaltyBadgeId` (string, uuid) — Unique identifier for the loyalty badge
- `mediaUrl` (string) — URL of the media to be displayed
- `loyaltyUserAllotmentCsvUrl` (string, uri) — URL for uploading loyalty user allotment via CSV
- `maxAmountPerInterval` (number) — The maximum amount of points a user can earn per interval. Available for the smart contract and external rules.
- `maxAmountInterval` (string, one of: daily, weekly, monthly, lifetime) — The interval for the max amount. Available for the smart contract and external rules.
- `shopifyStoreUrl` (string) — URL of the Shopify store
- `externalIntegrationId` (string, uuid) — ID of the external integration
- `duplicatedFromId` (string, uuid)
- `rewardLifetime` (string, one of: permanent, dynamic) — The lifetime of the reward
- `claimType` (string, one of: manual, auto) — The type of claim for the reward
- `contractIds` (array) — The IDs of the contracts
- `items` (string)
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `isDappRule` (boolean) — Routes rule rewards to the dApp owner address (for substitution-type rules: external_rule, smart_contract_event, stratus_function) or marks the rule as a dApp-attribution rule (for the dapps_by_* family). Defaults to true at the API for the dapps_by_* family when omitted, false otherwise. Immutable after creation.
## Response 200
200
- response
- (allOf)
- value (object)
- `id` (string, uuid, required)
- value (object) — Schema for creating a new loyalty rule with detailed validations
- `organizationId` (string, uuid, required) — Unique identifier for the organization creating the rule
- `websiteId` (string, uuid, required) — Unique identifier for the associated website
- `backgroundAssetUrl` (string) — URL of the background asset to be displayed
- `name` (string, required) — Name of the loyalty rule
- `description` (string) — Optional detailed description of the rule
- `network` (string, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence, default: "mainnet") — Blockchain network where the rule will apply
- `type` (string, required, one of: Bonus, BoughtOn, BoughtWithRoyalties, DiscordMessages, Listing, Lodging, PoapClaim, SnagSocialsConnected, SnapshotFullset, SnapshotHold, SoldWithRoyalties, TwitterBio, TwitterHashTagPost, MintOn, SoldOn, TokenHold, Retweet, check_in, code_entry, connect_wallet, connected_discord, connected_email, connected_epic, connected_github, connected_steam, connected_telegram, connected_twitter, create_partner_account, discord_member, drip_x_follow, drip_x_new_tweet, drip_x_text_in_bio, drip_x_text_in_comment, drip_x_text_in_name, drip_x_tweet, external_rule, link_click, manual_upload, profile_completed, referred_user, smart_contract_event, snapshot_governance, telegram_join, telegram_messages, text_input, token_hold_erc20, tweet_liked_by_project, twitter_comment, twitter_follow, twitter_followers, twitter_like, twitter_post_hashtag, quiz, poll, steam_wishlist, liquidity_uniswap_v2, liquidity_uniswap_v3, points_airdrop, youtube_subscribers, youtube_comment, shopify_spend, swap, tiktok_post, post_impressions, discord_join, connected_youtube, stratus_function, connected_tiktok, tiktok_follow, github_repo_star, github_repo_fork, github_repo_collaborator, github_merge_PR, discord_role_grant, connected_reddit, reddit_comment, robinhood_token_hold, connected_instagram, instagram_post, instagram_follow, connected_phone, dapps_by_gas_spent, dapps_by_active_users, dapps_by_new_users, dapps_by_transaction_count, dapps_by_user_retention) — Type of loyalty rule being created
- `frequency` (string, required, one of: none, once, hourly, daily, weekly, monthly, immediately) — Execution frequency of the loyalty rule
- `interval` (string, required, one of: hourly, daily, weekly, monthly, once, custom, every_message, unlimited) — Interval between rule executions
- `startTime` (string, date-time, required) — When the rule becomes active
- `endTime` (string, date-time, required) — When the rule becomes inactive
- `effectiveStartTime` (required) — The effective start time of the rule
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `effectiveEndTime` (required) — The effective end time of the rule
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `amount` (required) — Reward amount associated with the rule
- `customRewardsCsvUrl` (string, uri) — URL for uploading custom rewards via CSV
- `customRewardsApiUrl` (string, uri) — URL for fetching custom rewards
- `subscriptionId` (string) — Optional stratus subscription id for the rule
- `functionId` (string) — Optional stratus function id for the rule
- `metadata` (object, required) — Additional metadata for the loyalty rule
- `isCheckInOnEveryTxn` (boolean) — If enabled, the first transaction done on the platform will complete this rule
- `hasPaidFullRoyalty` (boolean) — Indicates if the full royalty has been paid for items.
- `royaltyAddress` (string) — Wallet address of the user can only be used if userId is not provided
- `royaltyPercentage` (number) — Royalty percentage of the item.
- `hasVerifiedTwitter` (boolean) — Indicates if the user has a verified Twitter account.
- `onlyRewardExistingUser` (boolean) — Indicates if only existing users are rewarded.
- `hasNeverSold` (boolean) — Indicates if the item has never been sold.
- `onlyNative` (boolean) — Flag indicating whether to include only native tokens.
- `hasFillSource` (boolean) — Flag indicating whether the fill source is included.
- `fillSource` (string) — Fill source of the order for the token sale
- `liquidity` (object) — Liquidity pool details.
- network: … (nested further)
- protocol: … (nested further)
- onlyRewardInRangeLiquidity: … (nested further)
- liquidityPerDay: … (nested further)
- customFunction: … (nested further)
- calculationType: … (nested further)
- pools: … (nested further)
- `twitterUsername` (string) — Twitter username of the user.
- `twitterUserId` (string) — Unique identifier of the Twitter user.
- `twitterHashtag` (string) — Hashtag associated with the Twitter post.
- `enableVerifiedMultiplier` (boolean) — Flag indicating whether the verified multiplier is enabled.
- `verifiedMultiplier` (number) — Multiplier applied to rewards for verified users.
- `isMultiplierApplied` (boolean) — Indicates if the multiplier has been applied to rewards.
- `onlyKnownUsers` (boolean) — Flag indicating whether to include only known users.
- `firstReferralReward` (number) — Percentage reward given to a user for their first referral.
- `referrerReward` (number) — Lump sum reward given to a referrer.
- `referrerRewardLoyaltyCurrencyId` (string) — Loyalty currency ID of the referrer reward.
- `secondReferralReward` (number) — Percentage reward given for a second-level referral.
- `enableStreaks` (boolean) — Flag indicating whether streaks are enabled.
- `promoCode` (string) — Promotional code associated with the rule.
- `promoCodeType` (string, one of: code, csv, generate) — Type of the promotional code.
- `promoCodeLength` (number) — Numbers of the promotional code to be generated.
- `promoCodeCsvUrl` (string) — URL of the CSV file containing promo codes.
- `enableJoinDiscordServers` (boolean) — Flag indicating whether joining Discord servers is required.
- `discordServersToJoin` (array) — Array of Discord servers, channels, and roles to join.
- items: … (nested further)
- `streakArray` (array) — Array of streak milestones and corresponding rewards.
- items: … (nested further)
- `dripQuestsToComplete` (array) — Array of drip quests required to complete the rule.
- items: … (nested further)
- `collection` (array) — Array of collections associated with the rule.
- items: … (nested further)
- `range` (array) — Array defining ranges and corresponding rewards.
- items: … (nested further)
- `customRewardsApiKey` (string) — API key for custom rewards integration.
- `link` (string) — Optional link associated with the metadata.
- `socialPlatform` (string, one of: Custom, Discord, EpicGames, Instagram, Phone, Steam, Telegram, TikTok, Twitch, X(Twitter), YouTube, Google, GitHub, Reddit) — Social media platform associated with the rule.
- `socialPlatformName` (string) — Name of the social platform.
- `socialPlatformLogo` (string) — URL of the social platform's logo.
- `rewardPerImpression` (boolean) — Flag indicating if rewards are given per impression.
- `snapshotProposals` (array) — Array of snapshot proposals for the rule.
- items: … (nested further)
- `cta` (object) — Object containing details for the call-to-action.
- label: … (nested further)
- href: … (nested further)
- `hasSaleCurrency` (boolean) — Flag indicating if the sale currency is included.
- `isTokenHoldMultiplier` (boolean) — Flag indicating if the token hold multiplier is applied.
- `saleCurrency` (string) — Currency associated with sales.
- `isRetroactive` (boolean) — Flag indicating if rewards are applied retroactively.
- `timeDelayToVerifySeconds` (number) — Time delay in seconds to verify actions.
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `referralRequirements` (object) — Object defining referral requirements.
- points: … (nested further)
- achievePoints: … (nested further)
- completeProfile: … (nested further)
- connectTwitter: … (nested further)
- connectDiscord: … (nested further)
- connectEmail: … (nested further)
- `multiplierLoyaltyCurrencyIds` (array) — Array of loyalty currency IDs used for multipliers.
- items: … (nested further)
- `verifyPlaceHolderText` (string) — Placeholder text for verification input fields.
- `verificationTextMinimumLength` (integer) — Minimum length of the verification text.
- `buttonText` (string) — Text displayed on the action button.
- `telegramChannelId` (string) — ID of the Telegram channel.
- `twitterAccountUrl` (string) — URL of the associated Twitter account.
- `twitterPostUrl` (string) — URL of the associated Twitter post.
- `checkLike` (boolean) — Flag indicating if liking the post is required.
- `checkRepost` (boolean) — Flag indicating if reposting is required.
- `checkComment` (boolean) — Flag indicating if commenting is required.
- `maxLikes` (integer) — Reaction limit for likes. Once the post reaches this many likes, the Like requirement is hidden from users. Likes are never verified server-side, so this only controls display.
- `maxReposts` (integer) — Reaction limit for reposts. Once the post reaches this many reposts, the Repost requirement is hidden and no longer verified.
- `maxComments` (integer) — Reaction limit for comments. Once the post reaches this many comments, the Comment requirement is hidden and no longer verified.
- `reactionStats` (object) — Latest scraped public reaction counts for the post, written back from social-service (throttled). Drives which reaction requirements are hidden once they hit their configured limit. Not admin-editable.
- likeCount: … (nested further)
- repostCount: … (nested further)
- commentCount: … (nested further)
- syncedAt: … (nested further)
- `checkText` — Text to check in the Twitter post, username, or bio.
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `requirePostLink` (boolean) — Flag indicating if the post link is required.
- `requirePostMediaLink` (boolean) — Flag indicating if media metadata is required.
- `preGeneratedPostText` (string) — Pre-generated text template that will be used to prefill the post content.
- `rewardCriteria` (string, one of: IMPRESSIONS_COUNT, ELIGIBLE_POST) — Criteria to evaluate the reward.
- `smartContract` (object) — Object containing details of the associated smart contract.
- contractId: … (nested further)
- event: … (nested further)
- type: … (nested further)
- criteria: … (nested further)
- params: … (nested further)
- addressMapping: … (nested further)
- amountMultiplier: … (nested further)
- `walletType` (string, one of: evm, solana, imx, sui, ton, cosmos, ultra, agw, flow_cadence, substrate) — Type of wallet associated with the rule.
- `imageUrl` (string) — URL of the image associated with the rule.
- `onlyRewardSingleTokenOwnership` (boolean) — give points for only one token ownership per contract
- `skipMultiplier` (boolean) — Flag indicating if the multiplier is skipped.
- `completeProfileConditions` (object) — Conditions for completing the profile.
- `steamAppId` (string) — ID of the Steam app.
- `githubRepoUrl` (string) — URL of the GitHub repository to check for star.
- `githubBranchName` (string) — Name of the GitHub branch to check for PR merge.
- `resetProgressOnClaim` (boolean) — If enabled, the progress tracking row will be deleted after a successful claim, allowing the user to re-earn the rule.
- `rewardPerAction` (boolean) — Flag indicating if the reward is rewarded per action.
- `rewardByBatch` (boolean) — Flag indicating if the reward is rewarded by batch.
- `batchSize` (number) — Number of tokens per batch.
- `youtubeVideoId` (string) — ID of the Youtube video.
- `youtubeChannelId` (string) — ID of the Youtube channel.
- `redditPostId` (string) — ID of the Reddit post.
- `isRestrictedToNewUsers` (boolean) — Flag indicating if the rule is restricted to new users.
- `allowMultipleRedemptions` (boolean) — Flag indicating if the rule allows multiple redemptions. Applies to Enter a Code only.
- `rewardBadgePerRange` (boolean) — Flag indicating if the rule can also reward badges per range.
- `swap` (object) — Metadata for swap loyalty rules
- provider: … (nested further)
- relayReferrerId: … (nested further)
- from: … (nested further)
- to: … (nested further)
- requireCrossChainSwap: … (nested further)
- `rewardPerValue` (boolean) — Flag indicating if the rule should reward based on value of traded tokens instead of count.
- `conditionDescription` (string) — Description of the external rule condition (only for external rules)
- `directRpc` (boolean) — Flag indicating if the rule should use direct RPC to get the balance of tokens.
- `erc20Type` (string, one of: erc20, native) — Type of ERC20 token for the loyalty rule.
- `rewardQualityPosts` (boolean) — Flag indicating if the rule should reward quality posts.
- `minimumFollowerCount` (number) — Minimum follower count for the rule. Accepts both number and string values.
- `autoCheckInRuleIds` (array) — Array of loyalty rule IDs that count as check-in when completed. If ["any"] then any rule completions count as check-in.
- items: … (nested further)
- `robinhoodSymbols` (array)
- items: … (nested further)
- `collectionAddress` (string) — Blockchain address of the associated collection
- `collections` (array) — List of associated collections
- `items` (object)
- address: … (nested further)
- network: … (nested further)
- symbol: … (nested further)
- `hideInUi` (boolean, default: false) — Whether to hide this rule in the user interface
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `showBeforeStart` (boolean, default: false) — Whether to show this rule before the start time
- `isRequired` (boolean, default: false) — Whether this rule is mandatory
- `oauthCredentialsId` (string, uuid) — OAuth credentials associated with the rule
- `loyaltyCurrencyId` (string, uuid) — Unique identifier for the loyalty currency. Optional when rewardType is token and tokenReward is provided; backend will find or create the currency from the selected contract.
- `tokenReward` (object) — When rewardType is token, provide relayer and either an ERC-20 contract or isNative=true to find or create loyalty currency and onchain token.
- `relayerId` (string, uuid, required) — Stratus relayer for token distribution (same network as contract)
- `contractId` (string, uuid) — ERC-20 contract to reward with; must match relayer network. Required unless isNative is true.
- `isNative` (boolean) — When true, reward is the chain native gas token (ETH, MATIC, BNB, …) and contractId is omitted.
- `loyaltyRuleGroupId` — Unique identifier for the loyalty rule group
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `rewardType` (string, one of: points, multiplier, badge, token, default: "points") — Type of reward issued by the rule
- `loyaltyBadgeId` (string, uuid) — Unique identifier for the loyalty badge
- `mediaUrl` (string) — URL of the media to be displayed
- `loyaltyUserAllotmentCsvUrl` (string, uri) — URL for uploading loyalty user allotment via CSV
- `maxAmountPerInterval` (number) — The maximum amount of points a user can earn per interval. Available for the smart contract and external rules.
- `maxAmountInterval` (string, one of: daily, weekly, monthly, lifetime) — The interval for the max amount. Available for the smart contract and external rules.
- `shopifyStoreUrl` (string) — URL of the Shopify store
- `externalIntegrationId` (string, uuid) — ID of the external integration
- `duplicatedFromId` (string, uuid)
- `rewardLifetime` (string, one of: permanent, dynamic) — The lifetime of the reward
- `claimType` (string, one of: manual, auto) — The type of claim for the reward
- `contractIds` (array) — The IDs of the contracts
- `items` (string)
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `isDappRule` (boolean) — Routes rule rewards to the dApp owner address (for substitution-type rules: external_rule, smart_contract_event, stratus_function) or marks the rule as a dApp-attribution rule (for the dapps_by_* family). Defaults to true at the API for the dapps_by_* family when omitted, false otherwise. Immutable after creation.
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Create Loyalty Transaction
`POST /api/loyalty/transactions`
`POST https://admin.snagsolutions.io/api/loyalty/transactions`
Create a loyalty transaction to update account balances.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for creating a loyalty transaction
- `description` (string, required) — Description of the loyalty transaction
- `entries` (array, required) — Array of transaction entries
- `items` (object)
- `walletAddress` (string) — The wallet address of the user
- `direction` (string, one of: credit, debit) — Direction of the transaction entry for this wallet address
- `amount` (number, required) — Transaction entry amount.
- `idempotencyKey` (string) — Idempotency key for the transaction entry. Creation will fail if already exists for this wallet address.
- `loyaltyCurrencyId` (string, uuid) — Unique identifier for the loyalty currency for the wallet. Can only use entry-level currency or transaction-level currency.
- `loyaltyCurrencyId` (string, uuid) — Unique identifier for the loyalty currency
- `websiteId` (string, uuid)
- `organizationId` (string, uuid)
## Response 200
200
- response (object)
- `id` (string, uuid, required)
- `description` (string, required)
- `entries` (array, required)
- `items` (object)
- `walletAddress` (string, required)
- `amount` (number, required)
- `direction` (string, required, one of: credit, debit) — Direction of the transaction entry
- `idempotencyKey` (string)
- `createdAt` (string, required)
- `updatedAt` (string, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Delete a loyalty currency
`DELETE /api/loyalty/currencies/{id}` — path parameters: `id`
`DELETE https://admin.snagsolutions.io/api/loyalty/currencies/{id}`
Delete a loyalty currency
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Response 200
200
- response (object) — Schema for a loyalty currency
- `id` (string, uuid, required) — Unique identifier for the loyalty currency
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `imageUrl` (string, required) — URL of the image
- `name` (string, required) — Name of the loyalty currency
- `symbol` (string, required) — Symbol representing the loyalty currency
- `decimals` (number, required) — Number of decimal places for the currency
- `createdById` (string, uuid, required) — ID of the user who created the currency
- `createdAt` (string, date-time, required) — Timestamp when the currency was created
- `updatedAt` (string, date-time, required) — Timestamp when the currency was last updated
- `apiGetUrl` (string, required) — API GET URL for fetching data
- `apiGetTransactionEntryUrl` (string, required) — API GET URL for fetching transaction entry data
- `apiPostUrl` (string, required) — API POST URL for sending data
- `apiAuthType` (string, required, one of: none, basic, bearer, apiKey) — Type of authentication used for API
- `apiAuthKey` (string, required) — Authentication key for API
- `apiAuthValue` (string, required) — Authentication value for API
- `sortId` (number, required) — Sorting identifier
- `deletedAt` (string, date-time, required) — Timestamp when the currency was deleted, if applicable
- `isTokenRewardCurrency` (boolean) — True for LoyaltyCurrency rows auto-created by resolveTokenReward (Option 1 hidden currency backing a token-reward rule). Excluded from list responses by default; opt in via excludeTokenRewardCurrencies=false on the list endpoint.
## Response 400
400
- response (object)
- `error` (string, required)
## Response 403
403
- response (object)
- `error` (string, required)
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Delete Loyalty Multiplier by External Identifier
`DELETE /api/loyalty/multipliers`
`DELETE https://admin.snagsolutions.io/api/loyalty/multipliers`
Delete a loyalty multiplier by its external identifier.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for deleting a loyalty multiplier
- `externalIdentifier` (string, required)
## Response 200
200
- response (object) — Schema for loyalty multiplier
- `id` (string, uuid, required)
- `userId` (string, uuid, required)
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `multiplier` (number, required)
- `title` (string, required)
- `description` (string, required)
- `externalIdentifier` (string, required)
- `createdAt` (string, required)
- `updatedAt` (string, required)
- `deletedAt` (string, required)
- `user` (object, required) — Schema for a loyalty multiplier user
- `id` (string, uuid, required)
- `walletAddress` (string, required)
- `loyaltyRuleId` (string, uuid)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
---
## Delete Loyalty Multiplier by ID
`DELETE /api/loyalty/multipliers/{multiplierId}` — path parameters: `multiplierId`
`DELETE https://admin.snagsolutions.io/api/loyalty/multipliers/{multiplierId}`
Delete a loyalty multiplier by its ID.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `multiplierId` (string, required)
## Response 200
200
- response (object)
- `id` (string, uuid, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
---
## Delete Loyalty Rule Chain
`DELETE /api/loyalty/rule_chains/{loyaltyRuleChainId}` — path parameters: `loyaltyRuleChainId`; query parameters: `organizationId`, `websiteId`
`DELETE https://admin.snagsolutions.io/api/loyalty/rule_chains/{loyaltyRuleChainId}`
Delete a loyalty rule chain
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `loyaltyRuleChainId` (string, required)
## Query parameters
- `organizationId` (string, required)
- `websiteId` (string, required)
## Response 200
200
- response (object)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Delete Loyalty Rule Group
`DELETE /api/loyalty/rule_groups/{id}` — path parameters: `id`
`DELETE https://admin.snagsolutions.io/api/loyalty/rule_groups/{id}`
Delete loyalty rule group by ID
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Response 200
200
- response (object)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Delete Loyalty Rule
`DELETE /api/loyalty/rules/{id}` — path parameters: `id`; query parameters: `debitLoyaltyPoints`
`DELETE https://admin.snagsolutions.io/api/loyalty/rules/{id}`
Delete an existing Loyalty Rule
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Query parameters
- `debitLoyaltyPoints` (string) — Whether to debit loyalty points
## Response 200
200
- response (object)
- `id` (string, uuid, required)
- `deletedAt` (string, date-time, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get loyalty account streaks
`GET /api/loyalty/account_streaks` — query parameters: `websiteId`, `organizationId`, `userGroupId`, `userId`, `walletAddress`, `loyaltyRuleId`, `startingAfter`, `limit`
`GET https://admin.snagsolutions.io/api/loyalty/account_streaks`
Get loyalty account streaks
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string) — Unique identifier for the website
- `organizationId` (string) — Unique identifier for the organization
- `userGroupId` (string) — Unique identifier for the user group
- `userId` (string) — User id to filter by
- `walletAddress` (string) — Wallet address to filter by
- `loyaltyRuleId` (string, required) — Loyalty rule id(s) to filter by, array or single id
- `startingAfter` (string) — Pagination cursor to start after a specific resource ID
- `limit` (number) — Maximum number of records to return (max 1000)
## Response 200
200
- response (array) — Response schema for getting loyalty account streaks, null if no streak
- `items` (object) — Response schema for getting loyalty account streaks, null if no streak
- `id` (string, uuid) — ID of the loyalty account streak, null if no streak yet
- `createdAt` (string, required) — Date and time the streak was created, null if no streak yet
- `updatedAt` (string, required) — Date and time the streak was last updated, null if no streak yet
- `expiresAt` (string, required) — Date and time the streak expires, null if no streak yet
- `streakCount` (number, required) — Number of days in the streak, 0 if no streak
- `userId` (string, uuid, required)
- `loyaltyRuleId` (string, uuid, required)
- `walletAddress` (string, required)
- `milestone` (array, required)
- `items` (object)
- `streakMilestone` (number, required)
- `streakAmount` (number, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 405
405
- response (object) — Schema for method not allowed response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
---
## Get loyalty accounts
`GET /api/loyalty/accounts` — query parameters: `organizationId`, `websiteId`, `startingAfter`, `limit`, `walletAddress`, `userId`, `walletGroupIdentifier`, `sortDir`, `loyaltyCurrencyId`, `userGroupExternalIdentifier`, `userGroupId`, `externalIdentifier`, `excludeDeletedCurrency`
`GET https://admin.snagsolutions.io/api/loyalty/accounts`
Get loyalty accounts
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string) — Unique identifier for the organization to filter by
- `websiteId` (string) — Unique identifier for the website to filter by
- `startingAfter` (string) — Pagination cursor to start after a specific account ID
- `limit` (integer) — Maximum number of accounts to return
- `walletAddress` (string) — Wallet address(es) to filter accounts by
- `userId` (string) — User ID(s) to filter accounts by. Pass in null to get a currencys main accounts.
- `walletGroupIdentifier` (string) — Wallet group identifier to filter accounts by
- `sortDir` (string) — Order the accounts in ascending or descending order, sorted by amount
- `loyaltyCurrencyId` (string) — Loyalty currency ID(s) to filter accounts by. Accepts a single id or a list.
- `userGroupExternalIdentifier` (string) — External identifier for the user group controlled by you
- `userGroupId` (string) — Unique identifier for the user group
- `externalIdentifier` (string) — External identifier for the user
- `excludeDeletedCurrency` (string) — Pass true to drop accounts belonging to deleted loyalty currencies. Defaults to false, so they are included. Ignored when loyaltyCurrencyId names a currency — a deleted currency returns its accounts either way.
## Response 200
200
- response (object) — Schema for the response of getting loyalty accounts
- `data` (array, required) — Array of loyalty accounts
- `items` (object) — Schema for a loyalty account
- `id` (string, uuid, required) — Unique identifier for the loyalty account
- `loyaltyCurrencyId` (string, uuid, required) — Unique identifier for the loyalty currency
- `userId` (string, uuid, required) — Unique identifier for the user
- `user` (required) — User associated with the loyalty account
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `hasNextPage` (boolean, required) — Indicates if there is a next page of results
- `message` (string) — Optional message
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get loyalty currencies
`GET /api/loyalty/currencies` — query parameters: `organizationId`, `websiteId`, `startingAfter`, `includeDeleted`, `excludeTokenRewardCurrencies`, `limit`
`GET https://admin.snagsolutions.io/api/loyalty/currencies`
Get loyalty currencies
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string, required) — Unique identifier for the organization
- `websiteId` (string, required) — Unique identifier for the website
- `startingAfter` (string) — Cursor for pagination; start after this ID
- `includeDeleted` (string) — Whether to include deleted currencies
- `excludeTokenRewardCurrencies` (string) — Exclude LoyaltyCurrency rows auto-created by token-reward rules (isTokenRewardCurrency=true). Defaults to true so admin pickers never see them; surfaces that need to render an existing token-reward rule pass false.
- `limit` (integer) — Maximum number of records to return
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object) — Schema for a loyalty currency
- `id` (string, uuid, required) — Unique identifier for the loyalty currency
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `imageUrl` (string, required) — URL of the image
- `name` (string, required) — Name of the loyalty currency
- `symbol` (string, required) — Symbol representing the loyalty currency
- `decimals` (number, required) — Number of decimal places for the currency
- `createdById` (string, uuid, required) — ID of the user who created the currency
- `createdAt` (string, date-time, required) — Timestamp when the currency was created
- `updatedAt` (string, date-time, required) — Timestamp when the currency was last updated
- `apiGetUrl` (string, required) — API GET URL for fetching data
- `apiGetTransactionEntryUrl` (string, required) — API GET URL for fetching transaction entry data
- `apiPostUrl` (string, required) — API POST URL for sending data
- `apiAuthType` (string, required, one of: none, basic, bearer, apiKey) — Type of authentication used for API
- `apiAuthKey` (string, required) — Authentication key for API
- `apiAuthValue` (string, required) — Authentication value for API
- `sortId` (number, required) — Sorting identifier
- `deletedAt` (string, date-time, required) — Timestamp when the currency was deleted, if applicable
- `isTokenRewardCurrency` (boolean) — True for LoyaltyCurrency rows auto-created by resolveTokenReward (Option 1 hidden currency backing a token-reward rule). Excluded from list responses by default; opt in via excludeTokenRewardCurrencies=false on the list endpoint.
- `hasNextPage` (boolean, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get Loyalty Multipliers
`GET /api/loyalty/multipliers` — query parameters: `websiteId`, `organizationId`, `walletAddress`, `userId`, `startingAfter`, `limit`, `includeDeleted`, `walletGroupIdentifier`, `externalIdentifier`, `userGroupExternalIdentifier`, `userGroupId`
`GET https://admin.snagsolutions.io/api/loyalty/multipliers`
Get Loyalty Multipliers
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string)
- `organizationId` (string)
- `walletAddress` (string)
- `userId` (string)
- `startingAfter` (string)
- `limit` (integer)
- `includeDeleted` (string)
- `walletGroupIdentifier` (string)
- `externalIdentifier` (string)
- `userGroupExternalIdentifier` (string)
- `userGroupId` (string)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object) — Schema for loyalty multiplier
- `id` (string, uuid, required)
- `userId` (string, uuid, required)
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `multiplier` (number, required)
- `title` (string, required)
- `description` (string, required)
- `externalIdentifier` (string, required)
- `createdAt` (string, required)
- `updatedAt` (string, required)
- `deletedAt` (string, required)
- `user` (object, required) — Schema for a loyalty multiplier user
- `id` (string, uuid, required)
- `walletAddress` (string, required)
- `loyaltyRuleId` (string, uuid)
- `hasNextPage` (boolean, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get Loyalty Rule Chains Status
`GET /api/loyalty/rule_chains/status` — query parameters: `organizationId`, `websiteId`, `userGroupId`, `loyaltyRuleChainId`, `userId`
`GET https://admin.snagsolutions.io/api/loyalty/rule_chains/status`
Get the status of a loyalty rule chains
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string, required)
- `websiteId` (string, required)
- `userGroupId` (string)
- `loyaltyRuleChainId` (string, required)
- `userId` (string) — Unique identifier(s) for the user. Max 20.
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `userId` (string, uuid)
- `walletAddress` (string)
- `loyaltyRuleId` (string, uuid, required)
- `isCompleted` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get Loyalty Rule Chains
`GET /api/loyalty/rule_chains` — query parameters: `organizationId`, `websiteId`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/loyalty/rule_chains`
Retrieve configured loyalty rule chains
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string, required) — Unique identifier for the organization
- `websiteId` (string, required) — Unique identifier for the website
- `limit` (number) — Number of rule chains to return
- `startingAfter` (string) — Starting after the given rule chain ID
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `id` (string, required) — Unique identifier for the rule chain
- `name` (string, required) — Name of the rule chain
- `loyaltyRuleId` (string, required) — Unique identifier for the loyalty rule
- `operatorType` (string, one of: AND, OR, default: "AND") — Type of the operator. AND if all conditions must be met, OR if any condition can be met.
- `loyaltyConditions` (array, required)
- `items` (object)
- id: … (nested further)
- type: … (nested further)
- description: … (nested further)
- amount: … (nested further)
- repeatCount: … (nested further)
- requiredCount: … (nested further)
- csvUrl: … (nested further)
- association: … (nested further)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get Loyalty Rule Edits
`GET /api/loyalty/rule_edits` — query parameters: `loyaltyRuleId`, `organizationId`, `websiteId`, `startingAfter`, `limit`
`GET https://admin.snagsolutions.io/api/loyalty/rule_edits`
Retrieve configured loyalty rule edits with optional pagination and filters
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `loyaltyRuleId` (boolean, required) — ID of the loyalty rule
- `organizationId` (string) — Unique identifier for the organization to filter by
- `websiteId` (string) — Unique identifier for the website to filter by
- `startingAfter` (string) — Pagination cursor to start after a specific rule edit ID
- `limit` (integer) — Maximum number of edits to return
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `id` (string, uuid, required) — Unique identifier for the rule edit
- `loyaltyRuleId` (string, uuid, required) — ID of the loyalty rule
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `editedByUserId` (string, uuid, required) — User ID of the user who made the edit
- `editedAt` (string, date-time, required) — Timestamp of when the rule edit was made
- `action` (string, required, one of: create, update, delete, restore) — Type of action that was made on the rule
- `data` (object, required) — Data that was changed on the rule
- `id` (string, uuid, required) — Unique identifier for the loyalty rule
- `name` (string, required) — Name of the loyalty rule
- `description` (string, required) — Description of the loyalty rule
- `endTime` (string, required) — End time of the loyalty rule
- `startTime` (string, required) — Start time of the loyalty rule
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of the reward
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `collectionAddress` (string) — Optional address of the collection
- `type` (string, required) — Type of the loyalty rule
- `frequency` (string, required) — Frequency of the loyalty rule
- `amount` (number, required) — Amount associated with the loyalty rule
- `createdAt` (string, date-time, required) — Timestamp when the loyalty rule was created
- `updatedAt` (string, date-time, required) — Timestamp when the loyalty rule was last updated
- `deletedAt` (string, date-time, required) — Timestamp when the loyalty rule was deleted (if applicable)
- `mediaUrl` (string) — URL of the media associated with the loyalty rule
- `metadata` (object) — Optional metadata for the loyalty rule
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `previousData` (object) — Previous data of the rule before the edit
- `id` (string, uuid, required) — Unique identifier for the loyalty rule
- `name` (string, required) — Name of the loyalty rule
- `description` (string, required) — Description of the loyalty rule
- `endTime` (string, required) — End time of the loyalty rule
- `startTime` (string, required) — Start time of the loyalty rule
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of the reward
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `collectionAddress` (string) — Optional address of the collection
- `type` (string, required) — Type of the loyalty rule
- `frequency` (string, required) — Frequency of the loyalty rule
- `amount` (number, required) — Amount associated with the loyalty rule
- `createdAt` (string, date-time, required) — Timestamp when the loyalty rule was created
- `updatedAt` (string, date-time, required) — Timestamp when the loyalty rule was last updated
- `deletedAt` (string, date-time, required) — Timestamp when the loyalty rule was deleted (if applicable)
- `mediaUrl` (string) — URL of the media associated with the loyalty rule
- `metadata` (object) — Optional metadata for the loyalty rule
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `comment` (string) — Comment that was made on the edit
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get Loyalty Rule Groups
`GET /api/loyalty/rule_groups` — query parameters: `websiteId`, `organizationId`, `startingAfter`, `limit`, `excludeHidden`
`GET https://admin.snagsolutions.io/api/loyalty/rule_groups`
Retrieve configured loyalty rule groups
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string)
- `organizationId` (string)
- `startingAfter` (string)
- `limit` (integer)
- `excludeHidden` (string)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object) — Schema for a get loyalty rule groups response
- `id` (string, uuid, required)
- `name` (string, required)
- `isRequired` (boolean, required)
- `sortId` (number, required)
- `isCollapsible` (boolean, required)
- `subTitle` (string, required)
- `loyaltyGroupItems` (array, required)
- `items` (object)
- id: … (nested further)
- sortId: … (nested further)
- loyaltyRule: … (nested further)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get Loyalty Rule Statuses
`GET /api/loyalty/rule_statuses` — query parameters: `organizationId`, `websiteId`, `walletAddress`, `userId`, `userGroupId`, `userGroupExternalIdentifier`, `loyaltyRuleId`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/loyalty/rule_statuses`
Retrieve loyalty rule statuses
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string) — Unique identifier for the organization
- `websiteId` (string) — Unique identifier for the website
- `walletAddress` (string) — Wallet address(es) to filter accounts by
- `userId` (string) — User ID(s) to filter accounts by
- `userGroupId` (string)
- `userGroupExternalIdentifier` (string)
- `loyaltyRuleId` (string) — Unique identifier(s) for the loyalty rule(s). Accepts a single ID or an array (up to 100); for larger sets, batch requests to avoid URL length limits.
- `limit` (number) — Number of items to return
- `startingAfter` (string) — Starting after item
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `id` (string, uuid, required)
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `userId` (string, uuid, required)
- `loyaltyRuleId` (string, uuid, required)
- `progress` (number, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get Loyalty Rules
`GET /api/loyalty/rules` — query parameters: `organizationId`, `websiteId`, `collectionAddress`, `loyaltyRuleGroupId`, `startingAfter`, `limit`, `loyaltyRuleId`, `isActive`, `allotedToUserId`, `allotedUserGroupId`, `isSpecial`, `includeDeleted`
`GET https://admin.snagsolutions.io/api/loyalty/rules`
Retrieve configured loyalty rules with optional pagination and filters
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string) — Unique identifier for the organization to filter by
- `websiteId` (string) — Unique identifier for the website to filter by
- `collectionAddress` (string) — Address of the collection to filter by
- `loyaltyRuleGroupId` (string) — ID(s) of the loyalty rule group(s) to filter results. Single ID or array for multiple groups (shared cursor when multiple).
- `startingAfter` (string) — Pagination cursor to start after a specific resource ID
- `limit` (number) — Maximum number of records to return (max 100)
- `loyaltyRuleId` (string) — The IDs of the loyalty rule
- `isActive` (string) — If true this will only return active rules, the rules for which the startTime is in the past and the endTime is in the future
- `allotedToUserId` (string) — IDs of the users to filter results by
- `allotedUserGroupId` (string) — ID of the user group to filter results by
- `isSpecial` (string) — If true this will only return special rules, special rules are the rules that are used for anti sybil as honey pot
- `includeDeleted` (boolean) — Whether to include deleted/archived records
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `id` (string, uuid, required) — Unique identifier for the loyalty rule
- `name` (string, required) — Name of the loyalty rule
- `description` (string, required) — Description of the loyalty rule
- `endTime` (string, required) — End time of the loyalty rule
- `startTime` (string, required) — Start time of the loyalty rule
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of the reward
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `collectionAddress` (string) — Optional address of the collection
- `type` (string, required) — Type of the loyalty rule
- `frequency` (string, required) — Frequency of the loyalty rule
- `amount` (number, required) — Amount associated with the loyalty rule
- `createdAt` (string, date-time, required) — Timestamp when the loyalty rule was created
- `updatedAt` (string, date-time, required) — Timestamp when the loyalty rule was last updated
- `deletedAt` (string, date-time, required) — Timestamp when the loyalty rule was deleted (if applicable)
- `mediaUrl` (string) — URL of the media associated with the loyalty rule
- `metadata` (object) — Optional metadata for the loyalty rule
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `loyaltyRuleChain` (array, required) — Chains attached to the loyalty rule
- `items` (object)
- id: … (nested further)
- name: … (nested further)
- loyaltyRuleId: … (nested further)
- operatorType: … (nested further)
- loyaltyConditions: … (nested further)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get Loyalty Transaction Entries
`GET /api/loyalty/transaction_entries` — query parameters: `organizationId`, `websiteId`, `startingAfter`, `limit`, `walletAddress`, `userId`, `walletGroupIdentifier`, `loyaltyTransactionId`, `loyaltyRuleId`, `loyaltyRuleType`, `idempotencyKey`, `type`, `sortBy`, `sortDir`, `userCompletedLoyaltyRuleId`, `userGroupExternalIdentifier`, `userGroupId`, `createdAtStart`, `createdAtEnd`, `loyaltyRuleGroupId`, `loyaltyCurrencyId`
`GET https://admin.snagsolutions.io/api/loyalty/transaction_entries`
Fetch loyalty transaction entries for wallets or users, representing account balance changes.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string) — Unique identifier for the organization to filter by
- `websiteId` (string) — Unique identifier for the website to filter by
- `startingAfter` (string) — Pagination cursor to start after a specific resource ID
- `limit` (integer) — Maximum number of records to return (max 100)
- `walletAddress` (string) — Wallet address or array of wallet addresses to filter by
- `userId` (string) — User ID or array of User IDs to filter by
- `walletGroupIdentifier` (string) — Identifier for the wallet group to filter by
- `loyaltyTransactionId` (string) — Unique identifier for the loyalty transaction to filter by
- `loyaltyRuleId` (string) — Unique identifier for the loyalty rule to filter by
- `loyaltyRuleType` (string)
- `idempotencyKey` (string) — Key to ensure idempotency of the request
- `type` (string) — Type of the loyalty transaction to filter by
- `sortBy` (string) — Field to sort the results by
- `sortDir` (string) — Direction to sort the results by
- `userCompletedLoyaltyRuleId` (string) — When passed in return the list of fulfilled entries for loyalty rules. If a loyalty rule has a fulfilled entry this means the user cannot currently complete the rule. Must be passed in with userId or walletAddress.
- `userGroupExternalIdentifier` (string) — External identifier for the user group controlled by you.
- `userGroupId` (string) — Unique identifier for the user group
- `createdAtStart` (string) — Start date to filter by (inclusive, greater than or equal to)
- `createdAtEnd` (string) — End date to filter by (exclusive, less than)
- `loyaltyRuleGroupId` (string) — UUID of the rule group (converted to lowercase)
- `loyaltyCurrencyId` (string) — UUID of the loyalty currency IDs to filter by. Use this to exclude deleted currencies
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object) — Schema for a loyalty transaction entry
- `id` (string, uuid, required) — Unique identifier for the transaction entry
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `loyaltyAccountId` (string, uuid, required) — Unique identifier for the loyalty account
- `direction` (string, required, one of: credit, debit) — Direction of the transaction entry
- `type` (string, required, one of: auction_bid, direct_mint, loyalty_rule, api_key, order) — Type of the loyalty transaction
- `createdAt` (string, required) — Timestamp when the transaction entry was created
- `updatedAt` (string, required) — Timestamp when the transaction entry was last updated
- `amount` (number, required) — Amount involved in the transaction
- `metadata` (object) — Optional metadata for the transaction entry
- `loyaltyTransaction` (object, required) — Details of the loyalty transaction
- `id` (string, required) — Unique identifier for the loyalty transaction
- `type` (string, required, one of: auction_bid, direct_mint, loyalty_rule, api_key, order) — Type of the loyalty transaction
- `loyaltyRule` (object) — Details of the loyalty rule associated with the transaction
- id: … (nested further)
- name: … (nested further)
- description: … (nested further)
- type: … (nested further)
- metadata: … (nested further)
- loyaltyRuleGroupItem: … (nested further)
- `hasNextPage` (boolean, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
---
## Get loyalty website page sections
`GET /api/loyalty/website_page_sections` — query parameters: `websiteId`, `organizationId`
`GET https://admin.snagsolutions.io/api/loyalty/website_page_sections`
Get loyalty website page sections
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string, required)
- `organizationId` (string, required)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `id` (string, uuid, required)
- `type` (string, required, one of: badges, leaderboard, feed, history)
- `isEnabled` (boolean, required)
- `sortIdentifier` (integer, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get Processing status and progress data for a specific user on loyalty rules
`GET /api/loyalty/rules/status` — query parameters: `websiteId`, `organizationId`, `userId`, `userGroupId`, `loyaltyRuleId`, `walletAddress`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/loyalty/rules/status`
This will return the processing status of quests for a specific user and also the progress of the loyalty rule
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string)
- `organizationId` (string)
- `userId` (string)
- `userGroupId` (string)
- `loyaltyRuleId` (string) — Unique identifier for the loyalty rule[s]
- `walletAddress` (string)
- `limit` (number) — Number of items to return
- `startingAfter` (string) — Starting after item
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `loyaltyRuleId` (string, uuid, required)
- `userId` (string, uuid, required)
- `status` (string, one of: pending, processing, completed, failed) — Status of the queued job, this is only returned if the job is queued and being processed, this wil be retained for 10 minutes after the job is completed
- `message` (string) — Categorized examples of messages that may be returned for a loyalty rule's processing result. User Onboarding - Check In: "Streak successful" - Complete Profile Details: "Profile not complete" Connect Accounts - Connect TikTok: "TikTok not connected" - Connect Discord: "Discord not connected", "Discord request timeout" - Connect X: "Twitter not connected", "Twitter not connected or content url is required" - Connect Email: "Email address not connected" - Connect Telegram: "Telegram not connected", "user has not setup telegram (no telegramUsername), please setup username in telegram and reconnect.", "Telegram request rate-limited" - Connect Steam: "Steam not connected" - Connect Youtube: "Youtube not connected" Social Quests - Join Telegram Group: "Telegram Channel not joined", "Telegram Channel joined successfully" - Post on TikTok: "TikTok post not verified" - Comment on an X Post: "Quest Completed: You have commented on the tweet", "Quest Not Completed: Comment not found for the specified tweet" - Repost an X Post: "Quest Completed: You have retweeted the tweet", "Quest Not Completed: Retweet not found for the specified tweet", "Quest Completed: You have retweeted and commented on the tweet", "Quest Not Completed: You have not retweeted or commented on the tweet" - Follow an X Account: "Missing required parameters: twitterUserId, targetUsername", "Quest Completed: You are a follower of @${targetUsername}", "Quest Not Completed: Please follow @${targetUsername} and try again in few minutes" - React to an X Post: "Missing required parameters: texts, twitterUserId, userTweetId", "No text provided", "Quest Completed: You have posted {tweetId} a tweet with the text {text}", "Quest Not Completed: Your all previous {COUNT} posts are already claimed. Please post a new tweet and try again in a few minutes.", "Tweet is already claimed", "Tweet does not belong to the user", "Text not found in tweet", "Tweet is before the quest start time", "Tweet does not have media", "Text found in tweet" - Add Text to X Bio: "Missing required parameters: texts, twitterUserId", "User not found", "Text found in bio", "Text not found in bio" - Add Text to X Username: "Missing required parameters: texts, twitterUserId", "User not found", "Text found in username", "Text not found in username" - Comment on an X Post with Text: "Missing required parameters: texts, twitterUserId", "Comment is already claimed", "Text found in comment", "Text not found in comment", "Tweet not found or could not be retrieved" - Comment on a YouTube Video: "Youtube comment added", "Youtube comment not added" - Subscribe to a YouTube Channel: "Youtube channel subscribed", "Quest failed, please make sure you have made your subscriptions public in youtube" - Get X post impressions: "You've already claimed the reward for this impression range." - Add an Item to Steam Wishlist: "Steam Wishlist item added", "Steam Wishlist item not added" Complex Onchain Logic - Token Swap: "Swap rewards calculated successfully", "Swap rule failed" Other - Answer a Quiz: "Quiz question not configured", "No question response found", "Incorrect answer", "Correct answer" - Answer a Poll: "Poll question not configured", "Poll response recorded" - Spend in Shopify Store: "Shopify not connected" - Points Airdrop: "Seems like you're not eligible for this points airdrop." General - "Quest already completed" - "Quest already completed using the same social account (${socialAccountName}), with the different user." - "You've already claimed the reward for this quest." - "Quest not achieved, please try again." - "Rule is not setup" - "Request aborted due to timeout" - "Google account not connected" - "Claim failed: Multiplier is already active"
- `id` (string, uuid) — The ID of the loyalty rule status for the Progress object
- `websiteId` (string, uuid)
- `organizationId` (string, uuid)
- `progress` (number) — The progress of the loyalty rule from 0 to 100
- `createdAt` (string, date-time)
- `updatedAt` (string, date-time)
- `fulfilledAt` (string, date-time)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 405
405
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get the rank of a user based on their loyalty points
`GET /api/loyalty/accounts/{id}/rank` — path parameters: `id`; query parameters: `websiteId`, `organizationId`, `loyaltyCurrencyId`
`GET https://admin.snagsolutions.io/api/loyalty/accounts/{id}/rank`
This api can only return the rank upto 5000, after that it will return '5000+'. Returns the user's rank determined by their loyalty points for the specified loyalty currency. This can be a slow operation for large organizations. We recommend caching when possible.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required) — UUID of the account
## Query parameters
- `websiteId` (string, required) — UUID of the website
- `organizationId` (string, required) — UUID of the organization
- `loyaltyCurrencyId` (string, required) — UUID of the loyalty currency
## Response 200
200
- response (object) — Schema for a get account rank response
- `rank` (number, required) — Rank of the user. null means the user has no amount. This api can only return the rank upto 5000, after that it will return "5000+"
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Reset Loyalty Currency
`POST /api/loyalty/reset`
`POST https://admin.snagsolutions.io/api/loyalty/reset`
Reset Loyalty Currency of a website
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `type` (string, required, one of: loyalty_reset_balances, loyalty_reset_balances_data_and_rules, loyalty_reset_balances_data_not_rules)
- `loyaltyCurrencyId` (string, uuid, required) — UUID of the loyalty currency to reset
- `organizationId` (string, uuid) — UUID of the organization (optional, converted to lowercase)
- `websiteId` (string, uuid) — UUID of the website (optional, converted to lowercase)
## Response 200
200
- response (object)
- `success` (boolean, required)
## Response 403
403
- response (object)
- `error` (string, required)
---
## Restore Loyalty Rule Edit
`POST /api/loyalty/rule_edits/{id}/restore` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/rule_edits/{id}/restore`
Restore a loyalty rule from a rule edit
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
## Response 200
200
- response
- (allOf)
- value (object)
- `id` (string, uuid, required)
- value (object)
- `id` (string, uuid, required) — Unique identifier for the rule edit
- `loyaltyRuleId` (string, uuid, required) — ID of the loyalty rule
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `editedByUserId` (string, uuid, required) — User ID of the user who made the edit
- `editedAt` (string, date-time, required) — Timestamp of when the rule edit was made
- `action` (string, required, one of: create, update, delete, restore) — Type of action that was made on the rule
- `data` (object, required) — Data that was changed on the rule
- `id` (string, uuid, required) — Unique identifier for the loyalty rule
- `name` (string, required) — Name of the loyalty rule
- `description` (string, required) — Description of the loyalty rule
- `endTime` (string, required) — End time of the loyalty rule
- `startTime` (string, required) — Start time of the loyalty rule
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of the reward
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `collectionAddress` (string) — Optional address of the collection
- `type` (string, required) — Type of the loyalty rule
- `frequency` (string, required) — Frequency of the loyalty rule
- `amount` (number, required) — Amount associated with the loyalty rule
- `createdAt` (string, date-time, required) — Timestamp when the loyalty rule was created
- `updatedAt` (string, date-time, required) — Timestamp when the loyalty rule was last updated
- `deletedAt` (string, date-time, required) — Timestamp when the loyalty rule was deleted (if applicable)
- `mediaUrl` (string) — URL of the media associated with the loyalty rule
- `metadata` (object) — Optional metadata for the loyalty rule
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `previousData` (object) — Previous data of the rule before the edit
- `id` (string, uuid, required) — Unique identifier for the loyalty rule
- `name` (string, required) — Name of the loyalty rule
- `description` (string, required) — Description of the loyalty rule
- `endTime` (string, required) — End time of the loyalty rule
- `startTime` (string, required) — Start time of the loyalty rule
- `rewardType` (string, required, one of: points, multiplier, badge, token) — Type of the reward
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `collectionAddress` (string) — Optional address of the collection
- `type` (string, required) — Type of the loyalty rule
- `frequency` (string, required) — Frequency of the loyalty rule
- `amount` (number, required) — Amount associated with the loyalty rule
- `createdAt` (string, date-time, required) — Timestamp when the loyalty rule was created
- `updatedAt` (string, date-time, required) — Timestamp when the loyalty rule was last updated
- `deletedAt` (string, date-time, required) — Timestamp when the loyalty rule was deleted (if applicable)
- `mediaUrl` (string) — URL of the media associated with the loyalty rule
- `metadata` (object) — Optional metadata for the loyalty rule
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `comment` (string) — Comment that was made on the edit
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Update Loyalty Multiplier
`POST /api/loyalty/multipliers/{multiplierId}` — path parameters: `multiplierId`
`POST https://admin.snagsolutions.io/api/loyalty/multipliers/{multiplierId}`
Update an existing loyalty multiplier.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `multiplierId` (string, required)
## Request body
- Content type: `application/json`
- request body (object) — Schema for updating a loyalty multiplier
- `multiplier` (number)
- `title` (string)
- `description` (string)
## Response 200
200
- response (object) — Schema for loyalty multiplier
- `id` (string, uuid, required)
- `userId` (string, uuid, required)
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `multiplier` (number, required)
- `title` (string, required)
- `description` (string, required)
- `externalIdentifier` (string, required)
- `createdAt` (string, required)
- `updatedAt` (string, required)
- `deletedAt` (string, required)
- `user` (object, required) — Schema for a loyalty multiplier user
- `id` (string, uuid, required)
- `walletAddress` (string, required)
- `loyaltyRuleId` (string, uuid)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
---
## Update Loyalty Rule Chain
`POST /api/loyalty/rule_chains/{loyaltyRuleChainId}` — path parameters: `loyaltyRuleChainId`; query parameters: `organizationId`, `websiteId`
`POST https://admin.snagsolutions.io/api/loyalty/rule_chains/{loyaltyRuleChainId}`
Update a loyalty rule chain
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `loyaltyRuleChainId` (string, required)
## Query parameters
- `organizationId` (string, required)
- `websiteId` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `name` (string, required)
- `loyaltyRuleId` (string, uuid, required) — Unique identifier for the loyalty rule
- `operatorType` (string, one of: AND, OR, default: "AND") — Type of the condition. AND if all conditions must be met, OR if any condition can be met.
- `loyaltyConditions` (array, required) — Array of conditions for the rule chain
- `items` (object)
- `type` (string, one of: rule, rules, points, badge, badges) — The type of rule
- `description` (string) — The description of the rule
- `amount` (number) — The amount of the rule
- `repeatCount` (number) — The number of times the rule should be repeated to fulfill the condition
- `requiredCount` (number) — The number of rules that need to be fulfilled to satisfy the condition
- `loyaltyRuleChainId` (string, uuid) — UUID of the loyalty rule chain (optional, converted to lowercase)
- `loyaltyRuleIds` (array) — Array of UUIDs of the rules (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyRuleGroupIds` (array) — Array of UUIDs of the rule group (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyLeaderboardViewIds` (array) — Array of UUIDs of the Loyalty Leaderboard (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyCurrencyIds` (array) — Array of UUIDs of the currency (optional, converted to lowercase)
- `items` (string, uuid)
- `loyaltyBadgeIds` (array) — Array of UUIDs of the badge (optional, converted to lowercase)
- `items` (string, uuid)
- `csvUrl` (string, uri) — URL of the CSV file (optional)
## Response 200
200
- response (object)
- `data` (object, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Update Loyalty Rule Group
`POST /api/loyalty/rule_groups/{id}` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/rule_groups/{id}`
Update loyalty rule group by ID
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object) — Schema to update loyalty rule groups body
- `name` (string)
- `isCollapsible` (boolean)
- `subtitle` (string)
## Response 200
200
- response (object)
- `name` (string)
- `subTitle` (string)
- `isCollapsible` (boolean)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Update Loyalty Rule Status
`POST /api/loyalty/rule_statuses`
`POST https://admin.snagsolutions.io/api/loyalty/rule_statuses`
Update a loyalty rule status
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `organizationId` (string, uuid) — Unique identifier for the organization
- `websiteId` (string, uuid) — Unique identifier for the website
- `userId` (string, uuid) — Unique identifier for the user, either userId or walletAddress must be provided
- `walletAddress` (string) — Unique identifier for the wallet address, either userId or walletAddress must be provided
- `loyaltyRuleId` (string, uuid, required) — Unique identifier for the loyalty rule
- `progress` (number) — Progress of the loyalty rule. Clamped to the rule target (100 for percentage rules, the highest reward range end for counter rules).
- `incProgress` (number) — Increment progress of the loyalty rule. The resulting progress is clamped to the rule target (100 for percentage rules, the highest reward range end for counter rules).
## Response 200
200
- response (object)
- `id` (string, uuid, required)
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `userId` (string, uuid, required)
- `loyaltyRuleId` (string, uuid, required)
- `progress` (number, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
---
## Update Loyalty Rule
`POST /api/loyalty/rules/{id}` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/rules/{id}`
Update an existing Loyalty Rule
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `name` (string, required) — Name of the loyalty rule
- `description` (string) — Optional description of the loyalty rule
- `startTime` (string, date-time, required) — Start time for the loyalty rule
- `endTime` (string, date-time, required) — End time for the loyalty rule
- `effectiveStartTime` — Effective start time of the rule
- (oneOf)
- value (string, date-time)
- value (string, null)
- `effectiveEndTime` — Effective end time of the rule
- (oneOf)
- value (string, date-time)
- value (string, null)
- `amount` (required) — Reward amount for the loyalty rule
- `customRewardsCsvUrl` (string, uri) — URL for uploading custom rewards via CSV
- `customRewardsApiUrl` (string, uri) — API URL for custom rewards integration
- `subscriptionId` (string) — Optional stratus subscription id for the rule
- `functionId` (string) — Optional stratus function id for the rule
- `metadata` (object) — Additional metadata for the loyalty rule
- `isCheckInOnEveryTxn` (boolean) — If enabled, the first transaction done on the platform will complete this rule
- `hasPaidFullRoyalty` (boolean) — Indicates if the full royalty has been paid for items.
- `royaltyAddress` (string) — Wallet address of the user can only be used if userId is not provided
- `royaltyPercentage` (number) — Royalty percentage of the item.
- `hasVerifiedTwitter` (boolean) — Indicates if the user has a verified Twitter account.
- `onlyRewardExistingUser` (boolean) — Indicates if only existing users are rewarded.
- `hasNeverSold` (boolean) — Indicates if the item has never been sold.
- `onlyNative` (boolean) — Flag indicating whether to include only native tokens.
- `hasFillSource` (boolean) — Flag indicating whether the fill source is included.
- `fillSource` (string) — Fill source of the order for the token sale
- `liquidity` (object) — Liquidity pool details.
- `network` (string, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence) — Blockchain network of the liquidity pool.
- `protocol` (string) — Protocol of the liquidity pool.
- `onlyRewardInRangeLiquidity` (boolean) — Indicates if only in-range liquidity is rewarded.
- `liquidityPerDay` (number) — Liquidity provided per day in USD
- `customFunction` (string) — Custom function to calculate the the reward amount based on the liquidity provided per day. X is the reward amount, Y is the liquidity provided per day in USD.
- `calculationType` (string, one of: fixed, custom) — Calculation type of the liquidity pool.
- `pools` (array) — Array of liquidity pools associated with the rule.
- `items` (object)
- id: … (nested further)
- `twitterUsername` (string) — Twitter username of the user.
- `twitterUserId` (string) — Unique identifier of the Twitter user.
- `twitterHashtag` (string) — Hashtag associated with the Twitter post.
- `enableVerifiedMultiplier` (boolean) — Flag indicating whether the verified multiplier is enabled.
- `verifiedMultiplier` (number) — Multiplier applied to rewards for verified users.
- `isMultiplierApplied` (boolean) — Indicates if the multiplier has been applied to rewards.
- `onlyKnownUsers` (boolean) — Flag indicating whether to include only known users.
- `firstReferralReward` (number) — Percentage reward given to a user for their first referral.
- `referrerReward` (number) — Lump sum reward given to a referrer.
- `referrerRewardLoyaltyCurrencyId` (string) — Loyalty currency ID of the referrer reward.
- `secondReferralReward` (number) — Percentage reward given for a second-level referral.
- `enableStreaks` (boolean) — Flag indicating whether streaks are enabled.
- `promoCode` (string) — Promotional code associated with the rule.
- `promoCodeType` (string, one of: code, csv, generate) — Type of the promotional code.
- `promoCodeLength` (number) — Numbers of the promotional code to be generated.
- `promoCodeCsvUrl` (string) — URL of the CSV file containing promo codes.
- `enableJoinDiscordServers` (boolean) — Flag indicating whether joining Discord servers is required.
- `discordServersToJoin` (array) — Array of Discord servers, channels, and roles to join.
- `items` (object)
- `id` (string) — ID of the Discord server to join.
- `channels` (array) — Array of Discord channels to join.
- items: … (nested further)
- `roles` (array) — Array of roles to assign in the Discord server.
- items: … (nested further)
- `streakArray` (array) — Array of streak milestones and corresponding rewards.
- `items` (object)
- `streakMilestone` (number, required) — Milestone required to achieve the streak.
- `streakAmount` (number, required) — Reward amount for achieving the streak milestone.
- `dripQuestsToComplete` (array) — Array of drip quests required to complete the rule.
- `items` (object)
- `id` (string, required) — ID of the drip quest to complete.
- `collection` (array) — Array of collections associated with the rule.
- `items` (object)
- `address` (string) — Blockchain address of the collection.
- `network` (string, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence) — Blockchain network of the collection.
- `multiplier` (number) — Multiplier applied to the rewards for this collection.
- `range` (array) — Array defining ranges and corresponding rewards.
- `items` (object)
- `startRange` (number, required) — Start value of the range.
- `endRange` (number, required) — End value of the range.
- `amount` (required) — Reward amount for this range.
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `loyaltyBadgeId` (string, uuid) — ID of the loyalty badge for this range.
- `loyaltyMultiplierAmount` (number) — Amount of the loyalty multiplier for this range.
- `customRewardsApiKey` (string) — API key for custom rewards integration.
- `link` (string) — Optional link associated with the metadata.
- `socialPlatform` (string, one of: Custom, Discord, EpicGames, Instagram, Phone, Steam, Telegram, TikTok, Twitch, X(Twitter), YouTube, Google, GitHub, Reddit) — Social media platform associated with the rule.
- `socialPlatformName` (string) — Name of the social platform.
- `socialPlatformLogo` (string) — URL of the social platform's logo.
- `rewardPerImpression` (boolean) — Flag indicating if rewards are given per impression.
- `snapshotProposals` (array) — Array of snapshot proposals for the rule.
- `items` (object)
- `id` (string, required) — ID of the snapshot proposal.
- `space` (string, required) — Space associated with the snapshot proposal.
- `cta` (object) — Object containing details for the call-to-action.
- `label` (string) — Label for the call-to-action.
- `href` (string) — Link for the call-to-action.
- `hasSaleCurrency` (boolean) — Flag indicating if the sale currency is included.
- `isTokenHoldMultiplier` (boolean) — Flag indicating if the token hold multiplier is applied.
- `saleCurrency` (string) — Currency associated with sales.
- `isRetroactive` (boolean) — Flag indicating if rewards are applied retroactively.
- `timeDelayToVerifySeconds` — Time delay in seconds to verify actions.
- (oneOf)
- value (string)
- value (number)
- `referralRequirements` (object) — Object defining referral requirements.
- `points` (object)
- `amount` (number) — Points required for referral.
- `loyaltyCurrecyId` (string) — ID of the loyalty currency for referral.
- `achievePoints` (boolean) — Flag indicating if achieving points is required.
- `completeProfile` (boolean) — Flag indicating if completing the profile is required.
- `connectTwitter` (boolean) — Flag indicating if connecting Twitter is required.
- `connectDiscord` (boolean) — Flag indicating if connecting Discord is required.
- `connectEmail` (boolean) — Flag indicating if connecting email is required.
- `multiplierLoyaltyCurrencyIds` (array) — Array of loyalty currency IDs used for multipliers.
- `items` (string)
- `verifyPlaceHolderText` (string) — Placeholder text for verification input fields.
- `verificationTextMinimumLength` (integer) — Minimum length of the verification text.
- `buttonText` (string) — Text displayed on the action button.
- `telegramChannelId` (string) — ID of the Telegram channel.
- `twitterAccountUrl` (string) — URL of the associated Twitter account.
- `twitterPostUrl` (string) — URL of the associated Twitter post.
- `checkLike` (boolean) — Flag indicating if liking the post is required.
- `checkRepost` (boolean) — Flag indicating if reposting is required.
- `checkComment` (boolean) — Flag indicating if commenting is required.
- `maxLikes` (integer) — Reaction limit for likes. Once the post reaches this many likes, the Like requirement is hidden from users. Likes are never verified server-side, so this only controls display.
- `maxReposts` (integer) — Reaction limit for reposts. Once the post reaches this many reposts, the Repost requirement is hidden and no longer verified.
- `maxComments` (integer) — Reaction limit for comments. Once the post reaches this many comments, the Comment requirement is hidden and no longer verified.
- `reactionStats` (object) — Latest scraped public reaction counts for the post, written back from social-service (throttled). Drives which reaction requirements are hidden once they hit their configured limit. Not admin-editable.
- `likeCount` (number)
- `repostCount` (number)
- `commentCount` (number)
- `syncedAt` (string)
- `checkText` — Text to check in the Twitter post, username, or bio.
- (oneOf)
- value (string)
- value (array)
- items: … (nested further)
- `requirePostLink` (boolean) — Flag indicating if the post link is required.
- `requirePostMediaLink` (boolean) — Flag indicating if media metadata is required.
- `preGeneratedPostText` (string) — Pre-generated text template that will be used to prefill the post content.
- `rewardCriteria` (string, one of: IMPRESSIONS_COUNT, ELIGIBLE_POST) — Criteria to evaluate the reward.
- `smartContract` (object) — Object containing details of the associated smart contract.
- `contractId` (string) — ID of the smart contract.
- `event` (string) — Event emitted by the smart contract.
- `type` (string, one of: function, event, default: "event") — Type of the smart contract interaction.
- `criteria` (string, one of: everyEvent, byParameter) — Criteria to evaluate the smart contract event.
- `params` (array) — Array of parameters for the smart contract.
- `items` (object)
- name: … (nested further)
- condition: … (nested further)
- value: … (nested further)
- `addressMapping` (string) — Mapping of addresses for the smart contract.
- `amountMultiplier` (object) — Object containing details of the amount multiplier from the event.
- `valueMapping` (string) — Mapping of the value for the smart contract.
- `walletType` (string, one of: evm, solana, imx, sui, ton, cosmos, ultra, agw, flow_cadence, substrate) — Type of wallet associated with the rule.
- `imageUrl` (string) — URL of the image associated with the rule.
- `onlyRewardSingleTokenOwnership` (boolean) — give points for only one token ownership per contract
- `skipMultiplier` (boolean) — Flag indicating if the multiplier is skipped.
- `completeProfileConditions` (object) — Conditions for completing the profile.
- `steamAppId` (string) — ID of the Steam app.
- `githubRepoUrl` (string) — URL of the GitHub repository to check for star.
- `githubBranchName` (string) — Name of the GitHub branch to check for PR merge.
- `resetProgressOnClaim` (boolean) — If enabled, the progress tracking row will be deleted after a successful claim, allowing the user to re-earn the rule.
- `rewardPerAction` (boolean) — Flag indicating if the reward is rewarded per action.
- `rewardByBatch` (boolean) — Flag indicating if the reward is rewarded by batch.
- `batchSize` (number) — Number of tokens per batch.
- `youtubeVideoId` (string) — ID of the Youtube video.
- `youtubeChannelId` (string) — ID of the Youtube channel.
- `redditPostId` (string) — ID of the Reddit post.
- `isRestrictedToNewUsers` (boolean) — Flag indicating if the rule is restricted to new users.
- `allowMultipleRedemptions` (boolean) — Flag indicating if the rule allows multiple redemptions. Applies to Enter a Code only.
- `rewardBadgePerRange` (boolean) — Flag indicating if the rule can also reward badges per range.
- `swap` (object) — Metadata for swap loyalty rules
- `provider` (string, one of: any, relay, lifi)
- `relayReferrerId` (string)
- `from` (object)
- `mode` (string, one of: any, specific)
- `chain`
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- value: … (nested further)
- `tokens` (array)
- items: … (nested further)
- `trackAmount` (boolean)
- `to` (object)
- `mode` (string, one of: any, specific)
- `chain`
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- value: … (nested further)
- `tokens` (array)
- items: … (nested further)
- `trackAmount` (boolean)
- `requireCrossChainSwap` (boolean)
- `rewardPerValue` (boolean) — Flag indicating if the rule should reward based on value of traded tokens instead of count.
- `conditionDescription` (string) — Description of the external rule condition (only for external rules)
- `directRpc` (boolean) — Flag indicating if the rule should use direct RPC to get the balance of tokens.
- `erc20Type` (string, one of: erc20, native) — Type of ERC20 token for the loyalty rule.
- `rewardQualityPosts` (boolean) — Flag indicating if the rule should reward quality posts.
- `minimumFollowerCount` (number) — Minimum follower count for the rule. Accepts both number and string values.
- `autoCheckInRuleIds` (array) — Array of loyalty rule IDs that count as check-in when completed. If ["any"] then any rule completions count as check-in.
- `items`
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `robinhoodSymbols` (array)
- `items` (string)
- `network` (string, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence, default: "mainnet") — Blockchain network where the rule will apply
- `collectionAddress` (string) — Blockchain address of the associated collection
- `collections` (array) — List of associated collections
- `items` (object)
- `address` (string, required) — Blockchain address of the collection
- `network` (string, required, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence) — Blockchain network for the collection
- `symbol` (string) — Symbol of the collection.
- `hideInUi` (boolean, default: false) — Whether to hide this rule in the user interface
- `showBeforeStart` (boolean, default: false) — Whether to show this rule before the start time
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `isRequired` (boolean, default: false) — Whether this rule is required for participation
- `oauthCredentialsId` (string, uuid) — ID for associated OAuth credentials
- `rewardType` (string, one of: points, multiplier, badge, token, default: "points") — Type of reward issued by this rule
- `loyaltyCurrencyId` (string, uuid) — Loyalty currency. Optional when rewardType is token and tokenReward is provided; backend resolves the hidden currency from the contract.
- `tokenReward` (object) — When rewardType is token, provide relayer and either an ERC-20 contract or isNative=true to find or create the hidden loyalty currency and onchain token.
- `relayerId` (string, uuid, required) — Stratus relayer for token distribution (same network as contract)
- `contractId` (string, uuid) — ERC-20 contract to reward with; must match relayer network. Required unless isNative is true.
- `isNative` (boolean) — When true, reward is the chain native gas token (ETH, MATIC, BNB, …) and contractId is omitted.
- `frequency` (string, one of: none, once, hourly, daily, weekly, monthly, immediately) — Frequency of the rule execution
- `interval` (string, one of: hourly, daily, weekly, monthly, once, custom, every_message, unlimited) — Time interval for recurring rule execution
- `loyaltyRuleGroupId` — ID of the rule group section to associate with the rule
- (oneOf)
- value (string, uuid)
- value (string, one of: no-section)
- `mediaUrl` (string) — URL of the media to be displayed
- `maxAmountPerInterval` (number) — The maximum amount of points a user can earn per interval. Available for the smart contract and external rules.
- `maxAmountInterval` (string, one of: daily, weekly, monthly, lifetime) — The interval for the max amount. Available for the smart contract and external rules.
- `externalIntegrationId` (string, uuid) — ID of the external integration
- `shopifyStoreUrl` (string) — URL of the Shopify store
- `rewardLifetime` (string, one of: permanent, dynamic) — The lifetime of the reward
- `claimType` (string, one of: manual, auto) — The type of claim for the reward
- `contractIds` (array) — The IDs of the contracts
- `items` (string)
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `deletedAt` (string, date-time) — Timestamp when the loyalty rule was deleted. Set to null to restore a deleted rule.
## Response 200
200
- response
- (allOf)
- value (object)
- `id` (string, uuid, required)
- value (object)
- `name` (string, required) — Name of the loyalty rule
- `description` (string) — Optional description of the loyalty rule
- `startTime` (string, date-time, required) — Start time for the loyalty rule
- `endTime` (string, date-time, required) — End time for the loyalty rule
- `effectiveStartTime` — Effective start time of the rule
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `effectiveEndTime` — Effective end time of the rule
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `amount` (required) — Reward amount for the loyalty rule
- `customRewardsCsvUrl` (string, uri) — URL for uploading custom rewards via CSV
- `customRewardsApiUrl` (string, uri) — API URL for custom rewards integration
- `subscriptionId` (string) — Optional stratus subscription id for the rule
- `functionId` (string) — Optional stratus function id for the rule
- `metadata` (object) — Additional metadata for the loyalty rule
- `isCheckInOnEveryTxn` (boolean) — If enabled, the first transaction done on the platform will complete this rule
- `hasPaidFullRoyalty` (boolean) — Indicates if the full royalty has been paid for items.
- `royaltyAddress` (string) — Wallet address of the user can only be used if userId is not provided
- `royaltyPercentage` (number) — Royalty percentage of the item.
- `hasVerifiedTwitter` (boolean) — Indicates if the user has a verified Twitter account.
- `onlyRewardExistingUser` (boolean) — Indicates if only existing users are rewarded.
- `hasNeverSold` (boolean) — Indicates if the item has never been sold.
- `onlyNative` (boolean) — Flag indicating whether to include only native tokens.
- `hasFillSource` (boolean) — Flag indicating whether the fill source is included.
- `fillSource` (string) — Fill source of the order for the token sale
- `liquidity` (object) — Liquidity pool details.
- network: … (nested further)
- protocol: … (nested further)
- onlyRewardInRangeLiquidity: … (nested further)
- liquidityPerDay: … (nested further)
- customFunction: … (nested further)
- calculationType: … (nested further)
- pools: … (nested further)
- `twitterUsername` (string) — Twitter username of the user.
- `twitterUserId` (string) — Unique identifier of the Twitter user.
- `twitterHashtag` (string) — Hashtag associated with the Twitter post.
- `enableVerifiedMultiplier` (boolean) — Flag indicating whether the verified multiplier is enabled.
- `verifiedMultiplier` (number) — Multiplier applied to rewards for verified users.
- `isMultiplierApplied` (boolean) — Indicates if the multiplier has been applied to rewards.
- `onlyKnownUsers` (boolean) — Flag indicating whether to include only known users.
- `firstReferralReward` (number) — Percentage reward given to a user for their first referral.
- `referrerReward` (number) — Lump sum reward given to a referrer.
- `referrerRewardLoyaltyCurrencyId` (string) — Loyalty currency ID of the referrer reward.
- `secondReferralReward` (number) — Percentage reward given for a second-level referral.
- `enableStreaks` (boolean) — Flag indicating whether streaks are enabled.
- `promoCode` (string) — Promotional code associated with the rule.
- `promoCodeType` (string, one of: code, csv, generate) — Type of the promotional code.
- `promoCodeLength` (number) — Numbers of the promotional code to be generated.
- `promoCodeCsvUrl` (string) — URL of the CSV file containing promo codes.
- `enableJoinDiscordServers` (boolean) — Flag indicating whether joining Discord servers is required.
- `discordServersToJoin` (array) — Array of Discord servers, channels, and roles to join.
- items: … (nested further)
- `streakArray` (array) — Array of streak milestones and corresponding rewards.
- items: … (nested further)
- `dripQuestsToComplete` (array) — Array of drip quests required to complete the rule.
- items: … (nested further)
- `collection` (array) — Array of collections associated with the rule.
- items: … (nested further)
- `range` (array) — Array defining ranges and corresponding rewards.
- items: … (nested further)
- `customRewardsApiKey` (string) — API key for custom rewards integration.
- `link` (string) — Optional link associated with the metadata.
- `socialPlatform` (string, one of: Custom, Discord, EpicGames, Instagram, Phone, Steam, Telegram, TikTok, Twitch, X(Twitter), YouTube, Google, GitHub, Reddit) — Social media platform associated with the rule.
- `socialPlatformName` (string) — Name of the social platform.
- `socialPlatformLogo` (string) — URL of the social platform's logo.
- `rewardPerImpression` (boolean) — Flag indicating if rewards are given per impression.
- `snapshotProposals` (array) — Array of snapshot proposals for the rule.
- items: … (nested further)
- `cta` (object) — Object containing details for the call-to-action.
- label: … (nested further)
- href: … (nested further)
- `hasSaleCurrency` (boolean) — Flag indicating if the sale currency is included.
- `isTokenHoldMultiplier` (boolean) — Flag indicating if the token hold multiplier is applied.
- `saleCurrency` (string) — Currency associated with sales.
- `isRetroactive` (boolean) — Flag indicating if rewards are applied retroactively.
- `timeDelayToVerifySeconds` (number) — Time delay in seconds to verify actions.
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `referralRequirements` (object) — Object defining referral requirements.
- points: … (nested further)
- achievePoints: … (nested further)
- completeProfile: … (nested further)
- connectTwitter: … (nested further)
- connectDiscord: … (nested further)
- connectEmail: … (nested further)
- `multiplierLoyaltyCurrencyIds` (array) — Array of loyalty currency IDs used for multipliers.
- items: … (nested further)
- `verifyPlaceHolderText` (string) — Placeholder text for verification input fields.
- `verificationTextMinimumLength` (integer) — Minimum length of the verification text.
- `buttonText` (string) — Text displayed on the action button.
- `telegramChannelId` (string) — ID of the Telegram channel.
- `twitterAccountUrl` (string) — URL of the associated Twitter account.
- `twitterPostUrl` (string) — URL of the associated Twitter post.
- `checkLike` (boolean) — Flag indicating if liking the post is required.
- `checkRepost` (boolean) — Flag indicating if reposting is required.
- `checkComment` (boolean) — Flag indicating if commenting is required.
- `maxLikes` (integer) — Reaction limit for likes. Once the post reaches this many likes, the Like requirement is hidden from users. Likes are never verified server-side, so this only controls display.
- `maxReposts` (integer) — Reaction limit for reposts. Once the post reaches this many reposts, the Repost requirement is hidden and no longer verified.
- `maxComments` (integer) — Reaction limit for comments. Once the post reaches this many comments, the Comment requirement is hidden and no longer verified.
- `reactionStats` (object) — Latest scraped public reaction counts for the post, written back from social-service (throttled). Drives which reaction requirements are hidden once they hit their configured limit. Not admin-editable.
- likeCount: … (nested further)
- repostCount: … (nested further)
- commentCount: … (nested further)
- syncedAt: … (nested further)
- `checkText` — Text to check in the Twitter post, username, or bio.
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `requirePostLink` (boolean) — Flag indicating if the post link is required.
- `requirePostMediaLink` (boolean) — Flag indicating if media metadata is required.
- `preGeneratedPostText` (string) — Pre-generated text template that will be used to prefill the post content.
- `rewardCriteria` (string, one of: IMPRESSIONS_COUNT, ELIGIBLE_POST) — Criteria to evaluate the reward.
- `smartContract` (object) — Object containing details of the associated smart contract.
- contractId: … (nested further)
- event: … (nested further)
- type: … (nested further)
- criteria: … (nested further)
- params: … (nested further)
- addressMapping: … (nested further)
- amountMultiplier: … (nested further)
- `walletType` (string, one of: evm, solana, imx, sui, ton, cosmos, ultra, agw, flow_cadence, substrate) — Type of wallet associated with the rule.
- `imageUrl` (string) — URL of the image associated with the rule.
- `onlyRewardSingleTokenOwnership` (boolean) — give points for only one token ownership per contract
- `skipMultiplier` (boolean) — Flag indicating if the multiplier is skipped.
- `completeProfileConditions` (object) — Conditions for completing the profile.
- `steamAppId` (string) — ID of the Steam app.
- `githubRepoUrl` (string) — URL of the GitHub repository to check for star.
- `githubBranchName` (string) — Name of the GitHub branch to check for PR merge.
- `resetProgressOnClaim` (boolean) — If enabled, the progress tracking row will be deleted after a successful claim, allowing the user to re-earn the rule.
- `rewardPerAction` (boolean) — Flag indicating if the reward is rewarded per action.
- `rewardByBatch` (boolean) — Flag indicating if the reward is rewarded by batch.
- `batchSize` (number) — Number of tokens per batch.
- `youtubeVideoId` (string) — ID of the Youtube video.
- `youtubeChannelId` (string) — ID of the Youtube channel.
- `redditPostId` (string) — ID of the Reddit post.
- `isRestrictedToNewUsers` (boolean) — Flag indicating if the rule is restricted to new users.
- `allowMultipleRedemptions` (boolean) — Flag indicating if the rule allows multiple redemptions. Applies to Enter a Code only.
- `rewardBadgePerRange` (boolean) — Flag indicating if the rule can also reward badges per range.
- `swap` (object) — Metadata for swap loyalty rules
- provider: … (nested further)
- relayReferrerId: … (nested further)
- from: … (nested further)
- to: … (nested further)
- requireCrossChainSwap: … (nested further)
- `rewardPerValue` (boolean) — Flag indicating if the rule should reward based on value of traded tokens instead of count.
- `conditionDescription` (string) — Description of the external rule condition (only for external rules)
- `directRpc` (boolean) — Flag indicating if the rule should use direct RPC to get the balance of tokens.
- `erc20Type` (string, one of: erc20, native) — Type of ERC20 token for the loyalty rule.
- `rewardQualityPosts` (boolean) — Flag indicating if the rule should reward quality posts.
- `minimumFollowerCount` (number) — Minimum follower count for the rule. Accepts both number and string values.
- `autoCheckInRuleIds` (array) — Array of loyalty rule IDs that count as check-in when completed. If ["any"] then any rule completions count as check-in.
- items: … (nested further)
- `robinhoodSymbols` (array)
- items: … (nested further)
- `network` (string, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence, default: "mainnet") — Blockchain network where the rule will apply
- `collectionAddress` (string) — Blockchain address of the associated collection
- `collections` (array) — List of associated collections
- `items` (object)
- address: … (nested further)
- network: … (nested further)
- symbol: … (nested further)
- `hideInUi` (boolean, default: false) — Whether to hide this rule in the user interface
- `showBeforeStart` (boolean, default: false) — Whether to show this rule before the start time
- `trackProgress` (boolean) — Flag indicating if the progress is tracked. If enabled, the rule can only be completed once the progress is 100%.
- `progressType` (string, one of: counter, percentage) — How tracked progress is interpreted. "percentage" counts 0-100 and is shown as a percentage. "counter" counts up to the highest reward range end and is shown as a raw count.
- `isRequired` (boolean, default: false) — Whether this rule is required for participation
- `oauthCredentialsId` (string, uuid) — ID for associated OAuth credentials
- `rewardType` (string, one of: points, multiplier, badge, token, default: "points") — Type of reward issued by this rule
- `loyaltyCurrencyId` (string, uuid) — Loyalty currency. Optional when rewardType is token and tokenReward is provided; backend resolves the hidden currency from the contract.
- `tokenReward` (object) — When rewardType is token, provide relayer and either an ERC-20 contract or isNative=true to find or create the hidden loyalty currency and onchain token.
- `relayerId` (string, uuid, required) — Stratus relayer for token distribution (same network as contract)
- `contractId` (string, uuid) — ERC-20 contract to reward with; must match relayer network. Required unless isNative is true.
- `isNative` (boolean) — When true, reward is the chain native gas token (ETH, MATIC, BNB, …) and contractId is omitted.
- `frequency` (string, one of: none, once, hourly, daily, weekly, monthly, immediately) — Frequency of the rule execution
- `interval` (string, one of: hourly, daily, weekly, monthly, once, custom, every_message, unlimited) — Time interval for recurring rule execution
- `loyaltyRuleGroupId` — ID of the rule group section to associate with the rule
- (oneOf)
- value: … (nested further)
- value: … (nested further)
- `mediaUrl` (string) — URL of the media to be displayed
- `maxAmountPerInterval` (number) — The maximum amount of points a user can earn per interval. Available for the smart contract and external rules.
- `maxAmountInterval` (string, one of: daily, weekly, monthly, lifetime) — The interval for the max amount. Available for the smart contract and external rules.
- `externalIntegrationId` (string, uuid) — ID of the external integration
- `shopifyStoreUrl` (string) — URL of the Shopify store
- `rewardLifetime` (string, one of: permanent, dynamic) — The lifetime of the reward
- `claimType` (string, one of: manual, auto) — The type of claim for the reward
- `contractIds` (array) — The IDs of the contracts
- `items` (string)
- `dappDeployedWithin` (string, one of: daily, weekly, monthly) — Filter dApps by deployment age before ranking.
- `dappDataWindow` (string, one of: daily, weekly, monthly) — Reward based on data usage within the last day, week, or month. Leave it empty for all time usage.
- `deletedAt` (string, date-time) — Timestamp when the loyalty rule was deleted. Set to null to restore a deleted rule.
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Update loyalty website page sections
`POST /api/loyalty/website_page_sections`
`POST https://admin.snagsolutions.io/api/loyalty/website_page_sections`
Update loyalty website page sections
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `loyaltyPageSections` (array, required)
- `items` (object)
- `id` (string)
- `type` (string, required, one of: badges, leaderboard, feed, history)
- `isEnabled` (boolean, required)
- `sortIdentifier` (integer, required)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `id` (string, uuid, required)
- `type` (string, required, one of: badges, leaderboard, feed, history)
- `isEnabled` (boolean, required)
- `sortIdentifier` (integer, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Connect a user to User Group
`POST /api/users/connect`
`POST https://admin.snagsolutions.io/api/users/connect`
This endpoint allows you to connect a user to a user group
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for a connect user schema
- `websiteId` (string, uuid, required) — The ID of the website
- `organizationId` (string, uuid, required) — The ID of the organization
- `walletType` (string, required, one of: evm, solana, imx, sui, ton, cosmos, ultra, agw, flow_cadence, substrate)
- `walletAddress` (string, required) — The wallet address of the user
- `verificationData` (object) — The verification data of the user walletAddress
- `message` (string)
- `signature` (string)
- `tonVerification` (object)
- `proof` (object, required)
- `timestamp` (number, required)
- `domain` (object, required)
- lengthBytes: … (nested further)
- value: … (nested further)
- `payload` (string, required)
- `signature` (string, required)
- `state_init` (string)
- `account` (object, required)
- `address` (string, required)
- `chain` (string, required, one of: -239, -3)
- `publicKey` (string, required)
- `walletStateInit` (string)
- `accessToken` (string)
- `dynamicAuthToken` (string)
- `verifiedLocally` (boolean)
- `userId` (string, uuid) — The ID of the user to whome this wallet is being connected
- `confirmDisconnect` (boolean) — Whether to confirm the disconnect
## Response 200
200
- response (object) — Schema for a connect users response
- `walletAddress` (string, required) — The wallet address of the user
- `id` (string, uuid, required) — The ID of the user
- `createdAt` (string, date-time, required) — Timestamp when the user was created
- `updatedAt` (string, date-time, required) — Timestamp when the user was last updated
## Response 400
400
- response (object)
- `message` (string, required, one of: Not a valid request, User not found, You cannot connect same wallet address, signatue is not valid)
- `error` (string, required, one of: MAXIMUM_ACCOUNT_LINKED)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Connect Auth Verify
`GET /api/{authType}/auth/connect` — path parameters: `authType`; query parameters: `code`, `state`, `redditProfileUrl`, `tiktokProfileUrl`, `instagramUsernameOrUrl`, `responseType`
`GET https://admin.snagsolutions.io/api/{authType}/auth/connect`
endpoint to verify and complete connecting an auth account to a Snag account
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `authType` (string, required)
## Query parameters
- `code` (string)
- `state` (string, required)
- `redditProfileUrl` (string) — The URL of the Reddit profile
- `tiktokProfileUrl` (string) — The URL of the TikTok profile
- `instagramUsernameOrUrl` (string) — The username or URL of the Instagram profile
- `responseType` (string) — The type of response to return, this is not valid for email auth, email auth only supports the redirect flow via verification link.
## Response 200
200
- response (object) — 200 response for GET /api/{authType}/auth/connect. Success, conflict (account already linked), or verification error.
- `success` (boolean, required) — Whether the account was linked successfully
- `error` (string) — The error message if the account was not linked successfully
- `message` (string) — The message of the response
- `jwtState` (string) — The JWT state
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Connect Auth
`GET /api/{authType}/auth` — path parameters: `authType`; query parameters: `websiteId`, `userId`, `walletAddress`, `redirect`, `responseType`, `skipYTChannelCheck`, `emailAddress`
`GET https://admin.snagsolutions.io/api/{authType}/auth`
This endpoint allows you to connect your auth account to a Snag account
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `authType` (string, required)
## Query parameters
- `websiteId` (string) — The ID of the website
- `userId` (string) — The ID of the user
- `walletAddress` (string) — The wallet address of the user
- `redirect` (string) — The redirect URL where the user will land after completing the auth
- `responseType` (string) — The type of response to return, this is not valid for email auth, email auth only supports the redirect flow via verification link.
- `skipYTChannelCheck` (string) — Whether to skip the YouTube channel check, this is only value for google auth
- `emailAddress` (string) — The email address of the user, this is only required for email auth
## Response 200
200
- response (object) — Schema for a connect social auth response schema
- `url` (string, uri, required) — The URL to redirect to.
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create a new website user attribute value
`POST /api/website_user_attributes/values`
`POST https://admin.snagsolutions.io/api/website_user_attributes/values`
Create a new website user attribute value
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `data` (array, required)
- `items` (object)
- `id` (string, uuid) — Unique identifier for the website user attribute value
- `websiteUserAttributeId` (string, uuid, required) — Unique identifier for the website user attribute
- `userId` (string, uuid, required) — Unique identifier for the user
- `value` (string, required) — Value for the website user attribute
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `organizationId` (string, uuid, required) — Unique identifier for the organization
## Response 200
200
- response (array)
- `items` (object)
- `id` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `websiteUserAttributeId` (string, required)
- `value` (string, required)
- `userId` (string, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create a new website user attribute
`POST /api/website_user_attributes`
`POST https://admin.snagsolutions.io/api/website_user_attributes`
Create a new website user attribute
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `data` (array, required)
- `items` (object)
- `id` (string, uuid) — Unique identifier for the website user attribute
- `label` (string, required) — Label for the website user attribute
- `websiteId` (string, uuid) — Unique identifier for the website
- `organizationId` (string, uuid) — Unique identifier for the organization
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `id` (string, required)
- `label` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create user device
`POST /api/users/devices`
`POST https://admin.snagsolutions.io/api/users/devices`
This endpoint is used to create user devices for fraud tracking
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for a create user device schema
- `userId` (string, uuid)
- `walletAddress` (string)
- `ipAddress` (string, required) — IP address of the user device
- `deviceIdentifier` (string) — Device identifier of the user device
## Response 200
200
- response (object) — Schema for a user device
- `id` (string, uuid, required) — Unique identifier for the user device
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `ipAddress` (string, required) — IP address of the user device
- `deviceIdentifier` (string) — Device identifier of the user device
- `source` (string, required) — Source of the user device
- `createdAt` (string, date-time, required) — Timestamp when the wallet was created
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create user metadata
`POST /api/users/metadatas`
`POST https://admin.snagsolutions.io/api/users/metadatas`
This endpoint is used to create user metadata
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for a create user metadata schema
- `userId` (string, uuid)
- `walletAddress` (string)
- `isBlocked` (boolean)
- `epicAccountIdentifier` (string)
- `discordUser` (string)
- `discordUserId` (string)
- `twitterUser` (string)
- `twitterUserFollowersCount` (number)
- `twitterUserId` (string)
- `emailAddress` (string, email)
- `walletGroupIdentifier` (string)
- `userGroupId` (string, uuid)
- `telegramUserId` (string)
- `telegramUsername` (string)
- `steamUserId` (string)
- `steamUsername` (string)
- `googleUserId` (string)
- `googleUser` (string)
- `YTChannelId` (string)
- `displayName` (string)
- `location` (string)
- `bio` (string)
- `portfolioUrl` (string)
- `externalLoyaltyScore` (string)
- `blockStatusReason` (string, one of: sybil_and_multi_account, account_migration, abuse_manipulation, not_sybil, issue_resolved, twitter_suspended, other)
- `isBlockExempt` (boolean)
- `internalNotes` (string)
- `logoUrl` (string)
- `userGroupExternalIdentifier` (string)
- `externalIdentifier` (string)
- `websiteId` (string, uuid)
- `organizationId` (string, uuid)
- `tiktokUser` (string)
- `tiktokUserId` (string)
- `sybilScore` (number)
- `githubUser` (string)
- `githubUserId` (string)
- `phoneNumber` (string) — Phone number in E.164 format (e.g., +14155552671)
## Response 200
200
- response (object) — Schema for a user metadata
- `id` (string, uuid, required) — Unique identifier for the user metadata
- `websiteId` (string, uuid, required) — Unique identifier for the website
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `walletGroupIdentifier` (string, required) — Identifier for the user wallet group set via api
- `userGroupId` (string, uuid, required) — Identifier for the user group set via api
- `userGroup` (object, required) — User group associated with the user
- `id` (string, uuid, required) — id for the user group
- `externalIdentifier` (string, required) — External identifier for the user group
- `user` (object, required)
- `id` (string, uuid, required) — Id for the user
- `walletAddress` (string, required) — Wallet address of the user
- `createdAt` (string, date-time, required) — Timestamp when the wallet was created
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Delete a website user attribute
`DELETE /api/website_user_attributes/{id}` — path parameters: `id`
`DELETE https://admin.snagsolutions.io/api/website_user_attributes/{id}`
Delete a website user attribute
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `id` (string, uuid, required) — Unique identifier for the website user attribute
- `organizationId` (string, uuid, required) — Unique identifier for the organization
- `websiteId` (string, uuid, required) — Unique identifier for the website
## Response 200
200
- response (object)
- `message` (string, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Disconnect a user from User Group
`POST /api/users/disconnect`
`POST https://admin.snagsolutions.io/api/users/disconnect`
This endpoint allows you to disconnect a user from another user
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for a disconnect user schema
- `websiteId` (string, uuid, required)
- `organizationId` (string, uuid, required)
- `userId` (string, uuid, required)
## Response 200
200
- response (object)
- `message` (string, required, one of: Success) — The message of the disconnect user response
## Response 400
400
- response (object)
- `message` (string, required, one of: Not a valid request, User is not part of the same group, User is not part of the group)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Disconnect Phone
`POST /api/phone_verifications/disconnect`
`POST https://admin.snagsolutions.io/api/phone_verifications/disconnect`
Disconnects the phone number from the user profile and clears associated badges.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Request body for disconnecting phone number
- `websiteId` (string, uuid) — The ID of the website
- `organizationId` (string, uuid) — The ID of the organization
- `userId` (string, uuid) — The ID of the user
- `walletAddress` (string) — The wallet address of the user
## Response 200
200
- response (object) — Standard response wrapper after disconnecting a phone number from the user profile
- `data` (object, required) — Response payload after disconnecting phone number
- `message` (string, required) — Status message
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get all website user attributes
`GET /api/website_user_attributes` — query parameters: `websiteId`, `organizationId`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/website_user_attributes`
Get all website user attributes
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string) — Unique identifier for the website
- `organizationId` (string) — Unique identifier for the organization
- `limit` (integer) — Maximum number of records to return (max 100)
- `startingAfter` (string) — Pagination cursor to start after a specific resource ID
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `id` (string, required)
- `label` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `hasNextPage` (boolean, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get user metadata
`GET /api/users/metadatas` — query parameters: `organizationId`, `websiteId`, `userId`, `walletAddress`, `externalIdentifier`, `limit`, `startingAfter`, `userSearch`
`GET https://admin.snagsolutions.io/api/users/metadatas`
This endpoint is used to get user metadata
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string) — UUID of the organization
- `websiteId` (string) — UUID of the website
- `userId` (string) — UUID of the user
- `walletAddress` (string) — Wallet address of the user
- `externalIdentifier` (string) — External identifier for the user
- `limit` (integer) — Number of records to fetch
- `startingAfter` (string) — Fetch records starting after this ID
- `userSearch` (string)
## Response 200
200
- response (object) — Response schema for fetching user metadata
- `data` (array, required)
- `items` (object)
- `id` (string, uuid, required) — Unique identifier for the user metadata
- `hasNextPage` (boolean, required) — Indicates if there are more records available
## Response 400
400
- response (object)
- `message` (string, required, one of: User Already exists, Either walletAddress or userId must be provided, Cannot provide both walletAddress and userId)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get users
`GET /api/users` — query parameters: `websiteId`, `organizationId`, `walletAddress`, `startingAfter`, `limit`, `twitterUser`, `discordUser`, `steamUsername`, `steamUserId`, `telegramUsername`, `telegramUserId`, `tiktokUser`, `redditUser`, `redditUserId`, `emailAddress`, `includeDelegation`, `userGroupId`, `userGroupExternalIdentifier`, `externalIdentifier`, `address`, `userId`
`GET https://admin.snagsolutions.io/api/users`
This endpoint allows you to get users
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string) — The ID of the website
- `organizationId` (string) — The ID of the organization
- `walletAddress` (string) — The wallet address of the user
- `startingAfter` (string) — Pagination cursor to start after a specific user ID
- `limit` (integer) — Number of users to return
- `twitterUser` (string) — Twitter username of the user
- `discordUser` (string) — Discord username of the user
- `steamUsername` (string) — Steam username of the user
- `steamUserId` (string) — Steam ID of the user
- `telegramUsername` (string) — Telegram username of the user
- `telegramUserId` (string) — Telegram ID of the user
- `tiktokUser` (string) — TikTok username of the user
- `redditUser` (string) — Reddit username of the user
- `redditUserId` (string) — Reddit ID of the user
- `emailAddress` (string) — Email address of the user
- `includeDelegation` (boolean) — Whether to include delegations in the response
- `userGroupId` (string) — The ID of the user group
- `userGroupExternalIdentifier` (string) — External identifier for the user group controlled by you
- `externalIdentifier` (string) — External identifier for the user
- `address` (string) — The address of the user
- `userId` (string) — The ID of the user
## Response 200
200
- response (object)
- `data` (array, required) — Schema for users response
- `items` (object) — Schema for a user response
- `id` (string, uuid, required)
- `temporaryLoyaltyUser` (boolean)
- `isSnagSuperAdmin` (boolean, required)
- `walletAddress` (string, required)
- `privyUserId` (string, uuid)
- `notifications` (boolean) — Whether the user has notifications enabled, only returned if includeNotifications is true
- `delegationsFrom` (array) — The delegations from the user, only returned if includeDelegation is true
- `items` (object)
- walletAddress: … (nested further)
- `userMetadata` (array)
- `items` (object)
- emailAddress: … (nested further)
- emailVerifiedAt: … (nested further)
- discordUser: … (nested further)
- discordVerifiedAt: … (nested further)
- twitterUser: … (nested further)
- twitterVerifiedAt: … (nested further)
- instagramUser: … (nested further)
- instagramVerifiedAt: … (nested further)
- logoUrl: … (nested further)
- displayName: … (nested further)
- location: … (nested further)
- bio: … (nested further)
- portfolioUrl: … (nested further)
- meta: … (nested further)
- userGroupId: … (nested further)
- walletGroupIdentifier: … (nested further)
- twitterUserFollowersCount: … (nested further)
- telegramUserId: … (nested further)
- telegramVerifiedAt: … (nested further)
- telegramUsername: … (nested further)
- isBlocked: … (nested further)
- steamUserId: … (nested further)
- steamUsername: … (nested further)
- externalLoyaltyScore: … (nested further)
- epicUsername: … (nested further)
- epicAccountIdentifier: … (nested further)
- googleUserId: … (nested further)
- googleUser: … (nested further)
- YTChannelId: … (nested further)
- tiktokUser: … (nested further)
- tiktokUserId: … (nested further)
- redditUserId: … (nested further)
- redditUser: … (nested further)
- githubUser: … (nested further)
- githubUserId: … (nested further)
- createdAt: … (nested further)
- updatedAt: … (nested further)
- verifiedLocallyAt: … (nested further)
- `websiteUserAttributes` (array)
- `items` (object)
- id: … (nested further)
- label: … (nested further)
- websiteUserAttributeValues: … (nested further)
- `referrers` (array)
- `items` (object)
- eligible: … (nested further)
- user: … (nested further)
- referralCode: … (nested further)
- `hasNextPage` (boolean, required) — Indicates if there is a next page of results
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get values for a website user attribute
`GET /api/website_user_attributes/values` — query parameters: `websiteId`, `organizationId`, `userIds`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/website_user_attributes/values`
Get values for a website user attribute
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string) — Unique identifier for the website
- `organizationId` (string) — Unique identifier for the organization
- `userIds` (string, required) — Unique identifier for the user
- `limit` (integer) — Maximum number of records to return (max 100)
- `startingAfter` (string) — Pagination cursor to start after a specific resource ID
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `value` (string, required)
- `id` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `customFieldId` (string, required)
- `userId` (string, required)
- `hasNextPage` (boolean, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Send Phone Verification Code
`POST /api/phone_verifications`
`POST https://admin.snagsolutions.io/api/phone_verifications`
Initiates phone number verification by sending an OTP code via SMS using Twilio Verify.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Request body for initiating phone verification via OTP
- `phoneNumber` (string, required) — Phone number in E.164 format (e.g., +14155552671)
- `websiteId` (string, uuid) — The ID of the website
- `userId` (string, uuid) — The ID of the user
- `walletAddress` (string) — The wallet address of the user
## Response 200
200
- response (object) — Standard response wrapper for initiating a phone number's verification
- `data` (object, required) — Response payload after initiating phone verification via OTP
- `success` (boolean, required) — Whether the phone number verification was successful
- `message` (string, required) — Status message
- `userId` (string, uuid, required) — The ID of the user
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Transfer Social Account
`POST /api/users/verify`
`POST https://admin.snagsolutions.io/api/users/verify`
Transfers a social account from one user to another when a conflict is detected. Disconnects the account from the previous user and connects it to the current user's profile. Used when a social account is already linked to a different user (MAXIMUM_ACCOUNT_LINKED error).
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `websiteId` (string, uuid)
- `organizationId` (string, uuid)
- `accountLinkData` (string, required)
- `userId` (string, uuid)
- `walletAddress` (string) — The wallet address of the user
## Response 200
200
- response (object)
- `message` (string, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## User count
`GET /api/users/count` — query parameters: `websiteId`, `organizationId`
`GET https://admin.snagsolutions.io/api/users/count`
This endpoint returns the total user count
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string, required) — The ID of the website
- `organizationId` (string, required) — The ID of the organization
## Response 200
200
- response (object) — Schema for counting users response
- `totalCount` (number, required) — Total count of users
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Verify Phone OTP and Connect
`POST /api/phone_verifications/connect`
`POST https://admin.snagsolutions.io/api/phone_verifications/connect`
Verifies the OTP code and connects the phone number to the user profile.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Request body for verifying OTP and connecting phone number
- `phoneNumber` (string, required) — Phone number in E.164 format (e.g., +14155552671)
- `code` (string, required) — The OTP code received via SMS
- `websiteId` (string, uuid) — The ID of the website
- `userId` (string, uuid) — The ID of the user
- `walletAddress` (string) — The wallet address of the user
## Response 200
200
- response (object) — Standard response wrapper after verifying OTP and connecting phone number
- `data` (object, required) — Response payload after verifying the OTP code and connecting the phone number
- `success` (boolean, required) — Whether the phone number was verified successfully
- `message` (string) — Status message
- `error` (string) — Error code if verification failed
- `accountLinkData` (string) — JWT-encoded data for resolving account link conflicts when the phone is already linked
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get mint status
`GET /api/minting/status/{id}` — path parameters: `id`
`GET https://admin.snagsolutions.io/api/minting/status/{id}`
Get the status of a mint
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Response 200
200
- response (object)
- `errorCode` (number, required)
- `errorMessage` (string, required)
- `relayerTxId` (string, required)
- `price` (number, required)
- `status` (string, required, one of: minted, minting, failed, generatingSignature)
- `txHash` (string, required)
## Response 304
304
- response (object) — Schema for not modified response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get minting allowlist
`GET /api/minting/assets/allowlist` — query parameters: `mintingContractAssetId`, `walletAddress`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/minting/assets/allowlist`
Fetch allowlist entries for a minting contract asset
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `mintingContractAssetId` (string, required)
- `walletAddress` (string)
- `limit` (integer) — Number of items to return
- `startingAfter` (string) — Cursor for pagination (entry ID)
## Response 200
200
- response (object) — Response schema for allowlist entries
- `data` (array, required)
- `items` (object)
- `mintingContractAssetId` (string, uuid, required)
- `walletAddress` (string, required)
- `totalQuantity` (integer, required)
- `quantityMinted` (integer, required)
- `hasNextPage` (boolean, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get minting assets
`GET /api/minting/assets` — query parameters: `websiteId`, `organizationId`, `mintingContractAssetId`, `mintingContractIds`, `tokenId`, `network`, `contractAddress`, `includeMetadata`, `includeDeleted`, `walletAddress`, `status`, `currencyType`, `sortBy`, `sortDir`, `isListed`, `isHidden`
`GET https://admin.snagsolutions.io/api/minting/assets`
Returns minting assets and any relevant context
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string, required) — Website ID
- `organizationId` (string, required) — Organization ID
- `mintingContractAssetId` (string) — Minting Contract Asset ID
- `mintingContractIds` (string) — Comma-separated list of Minting Contract UUIDs
- `tokenId` (string) — Token ID
- `network` (string) — Network
- `contractAddress` (string) — Contract Address
- `includeMetadata` (boolean) — Whether to include metadata in the response
- `includeDeleted` (boolean) — Whether to include deleted assets in the response
- `walletAddress` (string) — Wallet Address
- `status` (string) — Status
- `currencyType` (string) — Currency Type
- `sortBy` (string) — Sort Key
- `sortDir` (string) — Sort Dir
- `isListed` (boolean) — Whether to include listed assets in the response
- `isHidden` (boolean) — Whether to include hidden assets in the response
## Response 200
200
- response (object) — Schema for getting minting assets response
- `data` (array, required) — Array of minting assets
- `items` (object)
- `id` (string, uuid, required) — UUID of the minting asset
- `hasNextPage` (boolean, required) — Indicates if more pages are available
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get minting contracts
`GET /api/minting/contracts` — query parameters: `websiteId`, `organizationId`, `startingAfter`, `limit`, `showOnMarketplace`, `includeDeleted`
`GET https://admin.snagsolutions.io/api/minting/contracts`
Get minting contracts
## Authentication
No authentication required.
## Query parameters
- `websiteId` (string)
- `organizationId` (string)
- `startingAfter` (string)
- `limit` (integer)
- `showOnMarketplace` (boolean)
- `includeDeleted` (boolean)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `address` (string, required)
- `network` (string, required, one of: abstract, abstractTestnet, apechain, arbitrum, avalanche, avalancheFuji, base, baseSepolia, berachain, berachainArtio, berachainBepolia, binance, bscTestnet, campTestnet, fantom, fantomTestnet, flowMainnet, mainnet, nexus, nexusTestnet, optimism, polygon, polygon_mumbai, skaleNebula, skaleEuropa, skaleCalypso, solana, sophon, sophonTestnet, sui, superseed, superseedSepolia, vanar, xai, zksync, coti, cotiTestnet, dogeosTestnet, morph, morphTestnet, morphHolesky, ultra, ultraTestnet, nitrograph, sepolia, optimism_sepolia, arbitrumSepolia, goerli, optimism_goerli, arbitrumGoerli, basecamp, somnia, zkverify, polkadot, kusama, horizen, horizenTestnet, robinhoodChain, robinhoodChainTestnet, flow_cadence)
- `contractKind` (string, required, one of: ONCHAIN, OFFCHAIN)
- `description` (string, required)
- `enableGasless` (boolean, required)
- `externalLinkUrl` (string, required)
- `hasRevealMechanic` (boolean, required)
- `hideInactiveListings` (boolean, required)
- `id` (string, uuid, required)
- `imageUrl` (string, required)
- `isFreeClaimForBadgeHolders` (boolean, required)
- `isFreeClaimForTokenHolders` (boolean, required)
- `isListed` (boolean, required)
- `isPerAssetMintLimit` (boolean, required)
- `perUserMintLimit` (integer, required)
- `isSoulbound` (boolean, required)
- `name` (string, required)
- `organizationId` (string, uuid, required)
- `preRevealMedia` (string, required)
- `primarySaleRecipientAddress` (string, required)
- `relayerId` (string, uuid, required)
- `requireHoldingAllGatedItems` (boolean, required)
- `revealAvailableAt` (string, date-time, required)
- `royaltyFee` (required)
- `royaltyFeeAddress` (string, required)
- `shouldCollectEmail` (boolean, required)
- `showOnMarketplace` (boolean, required)
- `sortId` (number, required)
- `symbol` (string, required)
- `tokenType` (string, required, one of: erc721, erc1155, erc721c)
- `websiteId` (string, uuid, required)
- `mintingContractAssets` (array, required)
- `items` (object)
- auctionItems: … (nested further)
- animationUrl: … (nested further)
- assetNr: … (nested further)
- currencyAddress: … (nested further)
- currencyDecimals: … (nested further)
- description: … (nested further)
- id: … (nested further)
- imageUrl: … (nested further)
- isListed: … (nested further)
- isPhygitalItem: … (nested further)
- isHidden: … (nested further)
- shouldCollectUserInfo: … (nested further)
- collectInfoType: … (nested further)
- listingEndsAt: … (nested further)
- listingStartsAt: … (nested further)
- loyaltyCurrencyId: … (nested further)
- name: … (nested further)
- organizationId: … (nested further)
- price: … (nested further)
- quantity: … (nested further)
- quantityMinted: … (nested further)
- unlimitedQty: … (nested further)
- revealedAt: … (nested further)
- tokenId: … (nested further)
- websiteId: … (nested further)
- `totalAssets` (number, required)
- `mintingContractBadgeAssociation` (array, required)
- `items` (object)
- loyaltyBadge: … (nested further)
- `mustOwnCollections` (array, required)
- `items` (object)
- address: … (nested further)
- network: … (nested further)
- `hasNextPage` (boolean, required)
- `stats` (array, required)
- `items` (object)
- `mintingContractId` (string, uuid, required)
- `_sum` (object, required)
- `quantity` (number, required)
- `quantityMinted` (number, required)
- `_count` (object, required)
- `isListed` (number, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
---
## Get token gate status
`GET /api/minting/token_gate` — query parameters: `network`, `collectionAddress`, `tokenId`, `mintingContractAssetId`, `mintingContractId`
`GET https://admin.snagsolutions.io/api/minting/token_gate`
Pass in to see if a token is eligible for minting
## Authentication
No authentication required.
## Query parameters
- `network` (string, required)
- `collectionAddress` (string, required)
- `tokenId` (string, required)
- `mintingContractAssetId` (string, required)
- `mintingContractId` (string, required)
## Response 200
200
- response (object)
- `eligible` (boolean, required)
- `message` (string, required)
---
## Mint an asset
`POST /api/minting/contracts/mint`
`POST https://admin.snagsolutions.io/api/minting/contracts/mint`
Generates a signature for minting an asset on a given contract
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `assetId` (string, uuid, required)
- `contractId` (required)
- (oneOf)
- value (string, uuid)
- value (string)
- `emailAddress` (string)
- `walletAddress` (string)
- `quantity` (number, default: 1)
- `shippingId` (string)
- `shippingOrderType` (string, one of: stripe, shipstation, drip, twitter_bearer, epic_games, stardust, twitter_clientsecret, immutable_passport, sequence, google, walletConnect, auth0, privy, thirdweb, steam, discord, google_api, shopify, ultra, github, twilio)
- `agreedToSendW9` (boolean)
- `customInputValue` (string)
## Response 202
202
- response (object) — Use the id returned here to fetch the status and signature of the mint from the `/api/minting/status/:id` endpoint
- `mintingContractAssetMintStatusId` (string, uuid, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Update mint status
`POST /api/minting/status/{id}` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/minting/status/{id}`
Update the status of a mint
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `status` (string, required, one of: failed, minted)
- `txHash` (string)
## Response 200
200
- response (object)
- `message` (string, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
## Response 504
504
- response (object)
- `message` (string, required)
---
## Upsert minting allowlist entries
`POST /api/minting/assets/allowlist`
`POST https://admin.snagsolutions.io/api/minting/assets/allowlist`
Create or update allowlist entries for a minting contract asset (max 25 entries per request). If an entry already exists, it will be updated with the new quantity.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Create or update allowlist entries for a minting asset
- `mintingContractAssetId` (string, uuid, required)
- `entries` (array, required) — Allowlist entries (max 25 per request)
- `items` (object)
- `walletAddress` (string, required)
- `quantity` (integer, required)
## Response 200
200
- response (object) — Response schema for allowlist mutations
- `createdCount` (integer, required)
- `mintingContractAssetId` (string, uuid, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create a new question
`POST /api/loyalty/questions`
`POST https://admin.snagsolutions.io/api/loyalty/questions`
This endpoint allows you to create a new question.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `loyaltyRuleId` (string, uuid, required) — UUID of the loyalty rule
- `questionText` (string, required) — The text of the question
- `allowMultipleAttempts` (boolean, default: false) — Whether the question allows multiple attempts
- `loyaltyQuestionAnswers` (array, required) — Array of choices for the question
- `items` (object)
- `id` (string, uuid) — UUID of the choice (optional for new choices)
- `text` (string, required) — The text of the choice
- `isCorrect` (boolean, required) — Whether this choice is correct
- `sortIdentifier` (number, required) — The order in which to display the choice
## Response 201
201
- response (object) — Response returned for a question
- `id` (string, uuid, required)
- `text` (string, required)
- `loyaltyQuestionChoices` (array, required)
- `items` (object)
- `id` (string, uuid, required)
- `text` (string, required)
- `sortIdentifier` (number, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get questions for a loyalty rule
`GET /api/loyalty/questions` — query parameters: `loyaltyRuleIds`, `websiteId`, `organizationId`, `userId`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/loyalty/questions`
This endpoint allows you to fetch questions for a loyalty rule.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `loyaltyRuleIds` (string, required) — UUID of the loyalty rule to fetch questions for
- `websiteId` (string) — UUID of the website to fetch questions for
- `organizationId` (string) — UUID of the organization to fetch questions for
- `userId` (string) — UUID of the user to fetch questions for
- `limit` (integer) — Maximum number of records to return (max 100)
- `startingAfter` (string) — Pagination cursor to start after a specific resource ID
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object) — Response returned for a question
- `id` (string, uuid, required)
- `text` (string, required)
- `loyaltyQuestionChoices` (array, required)
- `items` (object)
- id: … (nested further)
- text: … (nested further)
- sortIdentifier: … (nested further)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get user's question responses
`GET /api/loyalty/questions_responses` — query parameters: `questionsId`, `websiteId`, `organizationId`, `userId`, `limit`, `startingAfter`
`GET https://admin.snagsolutions.io/api/loyalty/questions_responses`
Retrieve responses submitted by the authenticated user, optionally filtered by question IDs
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `questionsId` (string) — UUID(s) of the questions to filter responses
- `websiteId` (string) — UUID of the website to filter responses
- `organizationId` (string) — UUID of the organization to filter responses
- `userId` (string) — UUID of the user to filter responses
- `limit` (integer) — Maximum number of records to return (max 100)
- `startingAfter` (string) — Pagination cursor to start after a specific resource ID
## Response 200
200
- response (object) — List of question responses with details
- `data` (array, required)
- `items` (object) — Response details including the question and choice information
- `id` (string, uuid, required)
- `userId` (string, uuid, required)
- `loyaltyQuestionId` (string, uuid, required)
- `loyaltyQuestionChoiceId` (string, uuid, required)
- `createdAt` (string, date-time, required)
- `isCorrect` (boolean, required)
- `question` (object, required)
- `text` (string, required)
- `loyaltyQuestionChoices` (array, required)
- items: … (nested further)
- `hasNextPage` (boolean, required) — Indicates if there are more pages of results available
## Response 401
401
- response (object)
- `error` (string, required)
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Submit a response to a question
`POST /api/loyalty/questions_responses`
`POST https://admin.snagsolutions.io/api/loyalty/questions_responses`
This endpoint allows you to submit a response to a question.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `loyaltyQuestionChoiceId` (string, uuid, required) — UUID of the selected choice
- `websiteId` (string, uuid) — UUID of the website to filter responses
- `organizationId` (string, uuid) — UUID of the organization to filter responses
- `userId` (string, uuid) — UUID of the user who submitted the response
## Response 200
200
- response (object) — Response returned when submitting an answer to a question
- `response` (object, required)
- `id` (string, uuid, required) — UUID of the response
- `userId` (string, uuid, required) — UUID of the user who submitted the response
- `loyaltyQuestionId` (string, uuid, required) — UUID of the question being answered
- `loyaltyQuestionChoiceId` (string, uuid, required) — UUID of the selected choice
- `isCorrect` (boolean, required) — Whether the submitted answer was correct
- `alreadySubmitted` (boolean, required) — Whether the user had already submitted an answer to this question
## Response 401
401
- response (object)
- `error` (string, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object)
- `error` (string, required)
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Update an existing question
`POST /api/loyalty/questions/{id}` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/questions/{id}`
This endpoint allows you to update an existing question.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `questionText` (string, required) — The text of the question
- `loyaltyQuestionAnswers` (array, required) — Array of choices for the question
- `items` (object)
- `id` (string, uuid) — UUID of the choice (optional for new choices)
- `text` (string, required) — The text of the choice
- `isCorrect` (boolean, required) — Whether this choice is correct
- `sortIdentifier` (number, required) — The order in which to display the choice
## Response 200
200
- response (object) — Response returned for a question
- `id` (string, uuid, required)
- `text` (string, required)
- `loyaltyQuestionChoices` (array, required)
- `items` (object)
- `id` (string, uuid, required)
- `text` (string, required)
- `sortIdentifier` (number, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create referral code
`POST /api/referral/codes`
`POST https://admin.snagsolutions.io/api/referral/codes`
This endpoint allows you to create a referral code
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `websiteId` (string, uuid)
- `organizationId` (string, uuid)
- `userId` (string, uuid)
- `loyaltyRuleId` (string, uuid, required)
- `customReferralCode` (string)
## Response 200
200
- response (object)
- `id` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `referralCode` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `userId` (string, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Create referral user
`POST /api/referral/users`
`POST https://admin.snagsolutions.io/api/referral/users`
This endpoint allows you to create a referral user
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `referralCode` (string)
- `referralCodeId` (string, uuid)
- `userIds` (array, required)
- `items` (string, uuid)
## Response 200
200
- response (object)
- `message` (string, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get referrals
`GET /api/referral/users` — query parameters: `organizationId`, `websiteId`, `walletAddress`, `limit`, `startingAfter`, `loyaltyRuleId`
`GET https://admin.snagsolutions.io/api/referral/users`
This endpoint allows you to get referrals
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `organizationId` (string)
- `websiteId` (string)
- `walletAddress` (string)
- `limit` (integer)
- `startingAfter` (string)
- `loyaltyRuleId` (string)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `referralCode` (object, required)
- `id` (string, required)
- `referralCode` (string, required)
- `loyaltyRuleId` (string, uuid)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `userId` (string, required)
- `user` (object, required)
- `id` (string, required)
- `walletAddress` (string, required)
- `eligible` (boolean)
- `id` (string, required)
- `tier1Amount` (number, required) — This will only return data for earnings after 2025-04-24
- `tier2Amount` (number, required) — This will only return data for earnings after 2025-04-24
- `amount` (number, required) — This will only return data for earnings after 2025-04-24
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
---
## Get social posts
`GET /api/loyalty/posts` — query parameters: `websiteId`, `organizationId`, `userGroupId`, `userId`, `loyaltyPostId`, `cursor`, `limit`, `sortBy`
`GET https://admin.snagsolutions.io/api/loyalty/posts`
Fetch social posts with pagination and filtering options. Note: Rank is calculated for top 20,000 posts only. Posts beyond this threshold will have a null rank.
## Authentication
No authentication required.
## Query parameters
- `websiteId` (string) — UUID of the website to fetch social posts for
- `organizationId` (string) — UUID of the organization to fetch social posts for
- `userGroupId` (string) — Optional UUID of the user group to filter posts
- `userId` (string) — Optional UUID of the user to filter posts
- `loyaltyPostId` (string) — Optional UUID of the loyalty post to filter posts
- `cursor` (string) — Cursor for pagination
- `limit` (integer) — Number of posts to fetch per page
- `sortBy` (string) — Sort posts by createdAt or rank
## Response 200
200
- response (object) — Response containing social posts with pagination info
- `data` (array, required)
- `items` (object) — A social media post
- `id` (string, uuid, required)
- `text` (string)
- `mediaUrl` (string)
- `previewUrl` (string)
- `externalIdentifier` (string)
- `latestReward` (number)
- `loyaltyCurrencyId` (string, uuid, required)
- `userMetadata` (object, required)
- `userId` (string, uuid, required)
- `logoUrl` (string)
- `twitterUser` (string, required)
- `twitterUserId` (string, uuid, required)
- `twitterUserFollowersCount` (number, required)
- `twitterVerifiedAccount` (boolean, required)
- `mediaType` (string, required, one of: image, video)
- `platform` (string, required, one of: twitter, tiktok)
- `postType` (string, required, one of: post, comment, repost)
- `parentPostId` (string, uuid)
- `likeCount` (number)
- `commentCount` (number)
- `repostCount` (number)
- `viewCount` (number)
- `manualRank` (number)
- `dynamicScore` (number)
- `rank` (number)
- `loyaltyRuleId` (string, uuid, required)
- `contentQualityScore` (number)
- `sentimentScore` (integer)
- `postedAt` (string, date-time)
- `isSeen` (boolean, required)
- `authorId` (string, required)
- `userId` (string, uuid, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Reward a social post
`POST /api/loyalty/posts/{id}/reward` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/loyalty/posts/{id}/reward`
Reward a user for their social post.
## Authentication
No authentication required.
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object) — Request body for rewarding a social post
- `loyaltyCurrencyId` (string, uuid, required) — UUID of the loyalty currency to reward with
- `amount` (number, required) — Amount to reward
## Response 200
200
- response (object) — Response containing the reward transaction details
- `success` (boolean, required)
- `transaction` (object, required)
- `id` (string, uuid, required)
- `amount` (number, required)
- `loyaltyCurrencyId` (string, uuid, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create a new subscription (blockchain or snag)
`POST /api/stratus/subscriptions`
`POST https://admin.snagsolutions.io/api/stratus/subscriptions`
This endpoint allows you to create a stratus subscription, which runs its associated functions and calls its callback URL whenever a matching event is detected. Provide either `blockchainSubscriptions`, for contract events or function calls to watch on a given chain, or `snagSubscriptions`, for changes to Snag platform tables. Exactly one of the two is required — sending both, or neither, is rejected.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object)
- `name` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `functionIds` (array)
- `items` (string)
- `callbackUrl` (string, uri) — Must be the one below!!
- `startsAt` (string)
- `endsAt` (string)
- `maxEventsPerDay` (number)
- `blockchainSubscriptions` (array)
- `items` (object)
- `triggerId` (string)
- `chainId` (number, required)
- `contractAddress` (string, required)
- `blockchainSubscriptionType` (string, required, one of: EVENT, FUNCTION)
- `eventAbi` (string)
- `functionAbi` (string)
- `fromBlock` (string)
- `snagSubscriptions` (array)
- `items` (object)
- `triggerId` (string)
- `tableNames` (array, required)
- `items` (string)
- `mainLoyaltyAccountIds` (array)
- `items` (string)
## Response 200
200
- response (object)
- `name` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `functionIds` (array)
- `items` (string)
- `callbackUrl` (string, required)
- `startsAt` (string)
- `endsAt` (string)
- `id` (string, required)
- `version` (string, one of: v1, v2)
- `signingKey` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `disabled` (boolean)
- `disabledAt` (string, date-time)
- `disabledReason` (string)
- `consecutiveFailures` (number)
- `blockchainSubscriptions` (array)
- `items` (object)
- `id` (string, required)
- `chainId` (number, required)
- `contractAddress` (string, required)
- `blockchainSubscriptionType` (string, required, one of: EVENT, FUNCTION)
- `eventSelector` (string, required)
- `eventAbi` (string, required)
- `functionAbi` (string, required)
- `fromBlock` (string, required)
- `createdAtBlock` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `snagSubscriptions` (array)
- `items` (object)
- `id` (string, required)
- `table` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
## Response 400
400
- response (object)
- `error` (string, required)
## Response 409
409
- response (object)
- `name` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `functionIds` (array)
- `items` (string)
- `callbackUrl` (string, required)
- `startsAt` (string)
- `endsAt` (string)
- `id` (string, required)
- `version` (string, one of: v1, v2)
- `signingKey` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `disabled` (boolean)
- `disabledAt` (string, date-time)
- `disabledReason` (string)
- `consecutiveFailures` (number)
- `blockchainSubscriptions` (array)
- `items` (object)
- `id` (string, required)
- `chainId` (number, required)
- `contractAddress` (string, required)
- `blockchainSubscriptionType` (string, required, one of: EVENT, FUNCTION)
- `eventSelector` (string, required)
- `eventAbi` (string, required)
- `functionAbi` (string, required)
- `fromBlock` (string, required)
- `createdAtBlock` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `snagSubscriptions` (array)
- `items` (object)
- `id` (string, required)
- `table` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
## Response 429
429
- response (object)
- `error` (string, required)
## Response 500
500
- response (object)
- `error` (string, required)
---
## Delete a stratus subscription
`DELETE /api/stratus/subscriptions/{id}` — path parameters: `id`
`DELETE https://admin.snagsolutions.io/api/stratus/subscriptions/{id}`
This endpoint allows you to delete a stratus subscription.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `organizationId` (string, uuid, required)
- `websiteId` (string, uuid, required)
## Response 200
200
- response (object)
- `data` (object, required)
- `id` (string, uuid, required)
- `message` (string, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get available networks
`GET /api/stratus/networks`
`GET https://admin.snagsolutions.io/api/stratus/networks`
This endpoint allows you to list the chains available to blockchain subscriptions, each with a flag indicating whether chain analytics is enabled on it.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Response 200
200
- response (array)
- `items` (object)
- `chainId` (number, required)
- `analyticsEnabled` (boolean, required)
---
## Get subscription by id
`GET /api/stratus/subscriptions/{id}` — path parameters: `id`
`GET https://admin.snagsolutions.io/api/stratus/subscriptions/{id}`
This endpoint allows you to get a stratus subscription.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Response 200
200
- response (object)
- `name` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `functionIds` (array)
- `items` (string)
- `callbackUrl` (string, required)
- `startsAt` (string)
- `endsAt` (string)
- `id` (string, required)
- `version` (string, one of: v1, v2)
- `signingKey` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `disabled` (boolean)
- `disabledAt` (string, date-time)
- `disabledReason` (string)
- `consecutiveFailures` (number)
- `blockchainSubscriptions` (array)
- `items` (object)
- `id` (string, required)
- `chainId` (number, required)
- `contractAddress` (string, required)
- `blockchainSubscriptionType` (string, required, one of: EVENT, FUNCTION)
- `eventSelector` (string, required)
- `eventAbi` (string, required)
- `functionAbi` (string, required)
- `fromBlock` (string, required)
- `createdAtBlock` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `snagSubscriptions` (array)
- `items` (object)
- `id` (string, required)
- `table` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
## Response 500
500
- response (object)
- `error` (string, required)
---
## Get subscription events by subscription id
`GET /api/stratus/subscriptions/{id}/events` — path parameters: `id`; query parameters: `search`, `cursor`, `startingAfter`, `limit`
`GET https://admin.snagsolutions.io/api/stratus/subscriptions/{id}/events`
This endpoint allows you to get events for a stratus subscription.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Query parameters
- `search` (string)
- `cursor` (string)
- `startingAfter` (string)
- `limit` (integer)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `id` (string, required)
- `triggeredAt` (string, required)
- `functionRun` (object, required)
- `id` (string, required)
- `runId` (string, required)
- `status` (string)
- `output` (string)
- `callback` (object, required)
- `response` (string, required)
- `event` (object, required)
- `blockNumber` (string, required)
- `txHash` (string, required)
- `eventKey` (string, required)
- `contractAddress` (string, required)
- `detectedAt` (string, required)
- `hasNextPage` (boolean, required)
## Response 500
500
- response (object)
- `error` (string, required)
---
## Get subscriptions filtered by type and other fields
`GET /api/stratus/subscriptions` — query parameters: `cursor`, `startingAfter`, `limit`, `id`, `websiteId`, `organizationId`, `chainId`, `contractAddress`, `name`, `hideInUi`
`GET https://admin.snagsolutions.io/api/stratus/subscriptions`
This endpoint allows you to list stratus subscriptions. Results are cursor-paginated — page through them with `cursor` or `startingAfter` while `hasNextPage` is true — and can be narrowed by `id`, `websiteId`, `organizationId`, `chainId`, `contractAddress`, `name` and `hideInUi`.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `cursor` (string)
- `startingAfter` (string)
- `limit` (number)
- `id` (string)
- `websiteId` (string)
- `organizationId` (string)
- `chainId` (number)
- `contractAddress` (string)
- `name` (string)
- `hideInUi` (string)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `name` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `functionIds` (array)
- `items` (string)
- `callbackUrl` (string, required)
- `startsAt` (string)
- `endsAt` (string)
- `id` (string, required)
- `version` (string, one of: v1, v2)
- `signingKey` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `disabled` (boolean)
- `disabledAt` (string, date-time)
- `disabledReason` (string)
- `consecutiveFailures` (number)
- `blockchainSubscriptions` (array)
- `items` (object)
- id: … (nested further)
- chainId: … (nested further)
- contractAddress: … (nested further)
- blockchainSubscriptionType: … (nested further)
- eventSelector: … (nested further)
- eventAbi: … (nested further)
- functionAbi: … (nested further)
- fromBlock: … (nested further)
- createdAtBlock: … (nested further)
- createdAt: … (nested further)
- updatedAt: … (nested further)
- deletedAt: … (nested further)
- `snagSubscriptions` (array)
- `items` (object)
- id: … (nested further)
- table: … (nested further)
- createdAt: … (nested further)
- updatedAt: … (nested further)
- deletedAt: … (nested further)
- `hasNextPage` (boolean, required)
## Response 500
500
- response (object)
- `error` (string, required)
---
## Update a subscription (blockchain or snag)
`POST /api/stratus/subscriptions/{id}` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/stratus/subscriptions/{id}`
This endpoint allows you to update a stratus subscription. Every field is optional and only the ones you send are changed — its name, website and organization, associated function IDs, callback URL, active window, and the blockchain or snag subscriptions it watches.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `name` (string)
- `websiteId` (string)
- `organizationId` (string)
- `functionIds` (array)
- `items` (string)
- `callbackUrl` (string, uri)
- `startsAt` (string)
- `endsAt` (string)
- `blockchainSubscriptions` (array)
- `items` (object)
- `triggerId` (string)
- `chainId` (number, required)
- `contractAddress` (string, required)
- `blockchainSubscriptionType` (string, required, one of: EVENT, FUNCTION)
- `eventAbi` (string)
- `functionAbi` (string)
- `fromBlock` (string)
- `snagSubscriptions` (array)
- `items` (object)
- `triggerId` (string)
- `tableNames` (array, required)
- `items` (string)
- `mainLoyaltyAccountIds` (array)
- `items` (string)
## Response 200
200
- response (object)
- `name` (string, required)
- `websiteId` (string, required)
- `organizationId` (string, required)
- `functionIds` (array)
- `items` (string)
- `callbackUrl` (string, required)
- `startsAt` (string)
- `endsAt` (string)
- `id` (string, required)
- `version` (string, one of: v1, v2)
- `signingKey` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `disabled` (boolean)
- `disabledAt` (string, date-time)
- `disabledReason` (string)
- `consecutiveFailures` (number)
- `blockchainSubscriptions` (array)
- `items` (object)
- `id` (string, required)
- `chainId` (number, required)
- `contractAddress` (string, required)
- `blockchainSubscriptionType` (string, required, one of: EVENT, FUNCTION)
- `eventSelector` (string, required)
- `eventAbi` (string, required)
- `functionAbi` (string, required)
- `fromBlock` (string, required)
- `createdAtBlock` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
- `snagSubscriptions` (array)
- `items` (object)
- `id` (string, required)
- `table` (string, required)
- `createdAt` (string, date-time, required)
- `updatedAt` (string, date-time, required)
- `deletedAt` (string, date-time, required)
## Response 400
400
- response (object)
- `error` (string, required)
## Response 404
404
- response (object)
- `error` (string, required)
## Response 500
500
- response (object)
- `error` (string, required)
---
## Get a token claim
`GET /api/token_claims/{id}` — path parameters: `id`; query parameters: `id`
`GET https://admin.snagsolutions.io/api/token_claims/{id}`
Get a token claim by id
## Authentication
No authentication required.
## Path parameters
- `id` (string, required)
## Query parameters
- `id` (string, required)
## Response 200
200
- response (object)
- `totalAllocation` (number, required)
- `tokenClaimId` (string, uuid, required)
- `walletAddress` (string, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
---
## Get token claim eligibility
`GET /api/token_claims/{id}/eligibility` — path parameters: `id`; query parameters: `walletAddress`, `id`
`GET https://admin.snagsolutions.io/api/token_claims/{id}/eligibility`
Check if a wallet address is eligible for a token claim
## Authentication
No authentication required.
## Path parameters
- `id` (string, required)
## Query parameters
- `walletAddress` (string, required)
- `id` (string, required)
## Response 200
200
- response (object)
- `eligible` (boolean, required)
- `alreadyClaimed` (boolean, required)
- `amount` (number, required)
- `tokenClaimId` (string, uuid, required)
- `claimContractAddress` (string, required)
- `claimedAt` (string, required)
- `stakes` (array, required)
- `items` (object)
- `stakeId` (string, required)
- `amount` (string, required)
- `duration` (number, required)
- `startTime` (string, required)
- `claimed` (string, required)
- `claimable` (string, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
---
## Get token claim proof
`GET /api/token_claims/{id}/proof` — path parameters: `id`; query parameters: `walletAddress`, `id`
`GET https://admin.snagsolutions.io/api/token_claims/{id}/proof`
Get a token claim proof for a given wallet address
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Query parameters
- `walletAddress` (string, required)
- `id` (string, required)
## Response 200
200
- response (object)
- `totalAllocation` (number, required)
- `tokenClaimId` (string, uuid, required)
- `walletAddress` (string, required)
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 404
404
- response (object) — Schema for not found response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
---
## Get token claim users
`GET /api/token_claims/{id}/users` — path parameters: `id`; query parameters: `walletAddress`, `limit`, `startingAfter`, `sortBy`, `sortOrder`
`GET https://admin.snagsolutions.io/api/token_claims/{id}/users`
Get all users part of a token claim
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Query parameters
- `walletAddress` (string)
- `limit` (integer)
- `startingAfter` (string)
- `sortBy` (string)
- `sortOrder` (string)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `id` (string, uuid, required)
- `amount` (number, required)
- `claimedAt` (string, date-time, required)
- `user` (object, required)
- `id` (string, uuid, required)
- `walletAddress` (string, required)
- `hasNextPage` (boolean, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
---
## Get token claims
`GET /api/token_claims` — query parameters: `contractId`, `websiteId`, `organizationId`
`GET https://admin.snagsolutions.io/api/token_claims`
Get all available token claims for a given website and organization
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `contractId` (string)
- `websiteId` (string)
- `organizationId` (string)
## Response 200
200
- response (object)
- `data` (array, required)
- `items` (object)
- `claimContractAddress` (string, required)
- `csvUrl` (string, uri, required)
- `dataJobId` (string, uuid, required)
- `endTime` (string, required)
- `geoBlock` (array, required)
- `items` (string)
- `id` (string, uuid, required)
- `isDraft` (boolean, required)
- `name` (string, required)
- `relayerId` (string, uuid, required)
- `startTime` (string, required)
- `totalAllocatedAmount` (number, required)
- `totalClaimedAmount` (number, required)
- `totalWalletsAllocated` (number, required)
- `updatedAt` (string, required)
- `contract` (object, required)
- `id` (string, uuid, required)
- `name` (string, required)
- `address` (string, required)
- `chainId` (integer, int64, required)
- `claimAndStake` (boolean, default: false)
- `stakingContractType` (string, one of: linear, timelock, default: "timelock")
- `stakingContractAddress` (string, required)
- `maxBonus` (number, default: 0)
- `minLockup` (number, default: 0)
- `minLockupForMultiplier` (number, default: 0)
- `multiplier` (number, default: 0)
- `isStakingOptional` (boolean, default: false)
- `minPercentageToStake` (number, default: 0)
- `erc20Decimals` (number, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
---
## Update user claim
`POST /api/token_claims/{id}/claim` — path parameters: `id`
`POST https://admin.snagsolutions.io/api/token_claims/{id}/claim`
Update a user claim status after a user has claimed
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Path parameters
- `id` (string, required)
## Request body
- Content type: `application/json`
- request body (object)
- `walletAddress` (string, required)
- `txHash` (string, required)
## Response 200
200
- response (object)
- `message` (string, required)
- `data` (object, required)
- `walletAddress` (string, required)
- `tokenClaimId` (string, uuid, required)
- `claimedAt` (string, date-time, required)
- `totalAllocation` (number, required)
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 429
429
- response (object) — Schema for too many request response
- `message` (string, required) — Error message
---
## Assign roles to users for a website
`POST /api/website_user_roles`
`POST https://admin.snagsolutions.io/api/website_user_roles`
Assign roles to users for a specific website
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Query parameters for getWebsiteUserRoles
- `websiteId` (string, uuid, required) — UUID of the website
- `websiteUserRoles` (array, required) — Array of website user roles
- `items` (object)
- `walletAddress` (string, required) — Wallet address of the user
- `roleType` (string, required, one of: WEBSITE_SUPER_ADMIN, WEBSITE_ADMIN, WEBSITE_MODERATOR, WEBSITE_API_KEY, STRATUS_API_KEY, WEBSITE_READ_ONLY_API_KEY, ORGANIZATION_API_KEY) — Role type of the user
- `deletedAt` (string) — Deletion date of the user role
## Response 200
200
- response (object) — Schema for postWebsiteUserRoles response
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Create website
`POST /api/websites`
`POST https://admin.snagsolutions.io/api/websites`
Creates a new website. `adminWalletAddress` can only be used when authenticating with an organization API key.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Request body
- Content type: `application/json`
- request body (object) — Schema for creating a new website
- `organizationId` (string, uuid) — Organization ID for which this website is being created
- `adminWalletAddress` (array) — Optional admin wallet addresses to grant admin access on creation (organization API key only). Maximum 2.
- `items` (string)
- `name` (string, required) — Name of the website
- `twitterUsername` (string) — Twitter username of the website account
- `discordUrl` (string) — Discord URL of the website account
- `telegramUrl` (string) — Telegram URL of the website account
- `instagramUsername` (string) — Instagram username of the website account
- `homeUrl` (string) — Home URL of the official website
## Response 200
200
- response (object) — Schema representing a website
- `id` (string, uuid, required) — Website ID
- `name` (string, required) — Website name
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get roles of users for a website
`GET /api/website_user_roles` — query parameters: `websiteId`, `userId`
`GET https://admin.snagsolutions.io/api/website_user_roles`
Retrieves roles of users for a specific website. If userId is provided, retrieves the role of that specific user for the website.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `websiteId` (string, required) — UUID of the website
- `userId` (string) — UUID of the user
## Response 200
200
- response (object) — Schema for getWebsiteUserRoles response
- `data` (array, required)
- `items` (object)
- `id` (string, uuid, required) — UUID of the website user role
## Response 400
400
- response (object) — Schema for bad request response
- `message` (string, required) — Error message
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Get websites
`GET /api/websites` — query parameters: `startingAfter`, `limit`, `organizationId`, `name`
`GET https://admin.snagsolutions.io/api/websites`
Returns a list of websites. Includes optional query parameters for filtering and pagination.
## Authentication
- `apiKeyAuth` — apiKey in header: `X-API-KEY`
## Query parameters
- `startingAfter` (string) — Pagination cursor, start after this ID
- `limit` (integer) — Number of items to return
- `organizationId` (string) — Filter by organization ID
- `name` (string) — Filter by name
## Response 200
200
- response (object) — Response schema for fetching websites
- `data` (array, required) — Array of websites
- `items` (object) — Schema representing a website
- `id` (string, uuid, required) — Website ID
- `name` (string, required) — Website name
- `hasNextPage` (boolean, required) — Indicates if more pages are available
## Response 403
403
- response (object) — Schema for forbidden response
- `message` (string, required) — Error message
## Response 500
500
- response (object) — Schema for internal server error response
- `success` (boolean, required) — Indicates operation success
- `message` (string, required) — Error message
- `debugInfo` (string) — Optional debug information
---
## Check In
*Daily check-in loyalty rule. Reward users for returning to your platform each day.*
## **Overview**
This loyalty rule allows admins to reward users for regularly engaging with the platform. Users earn rewards simply by visiting or interacting with the site on a scheduled basis. This rule supports streak-based rewards, which incentivizes consistent user engagement by providing increased rewards for consecutive check-ins.

## **Rule Configuration Fields**
### **Basic Settings**
- **Points to rward**: Define the number of points users receive for each check-in.
- **User reward frequency**: Determine how often users can check in and receive rewards (Daily, Weekly, Monthly).
- **Update Cadence**: The Check-in rule rewards users **immediately** upon completion of a valid check-in. This instant gratification helps drive continued engagement with the platform.
### **Custom Setting**
- **Count any rule completion as check-in**: Toggle to count any rule completion as a check-in.
- **Enable streak**: Toggle to activate streak-based reward multipliers.
- **Reward only streak completions**: If both of the above are enabled, and amount is set to 0, this setting will reward users only for completing the full streak. **Note**: This means the check-in rule will not appear on the frontend.
- **Streak Milestone**: Define specific numbers of consecutive days of check-ins for enhanced rewards.
- **Points per streak**: Specify the increased reward amounts for achieving each defined streak threshold.
- **Asset Selection**: Choose between an image or video that will be displayed as a modal when users successfully check in.
- **Image Option**: Upload an image file that will appear in the success modal.
- **Video Option**: Upload a video file that will play automatically in the success modal.
- **Visual Feedback**: The selected asset creates a visual reward that enhances the check-in experience, providing immediate confirmation of successful check-in completion.
---
## Enter a Code
*Code entry loyalty rule. Reward users for entering promotional or secret codes.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on the entry of a specific code. This rule is useful for rewarding users for joining certain community events e.g twitter spaces.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Code Entry rule rewards users **immediately** upon successful entry of the code.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
- **Link**: Provide a URL for users to take further action.
- **Platform**: This lets you choose from specific list of platforms of which icon will be shown in the rule card for better user experience.
- available platforms:
- discord
- epic games
- instagram
- steam
- telegram
- tiktok
- twitch
- twitter/X
- youtube
- **Promo Code**: Define the promo code that users need to enter to earn the reward.
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
## Use cases
- **Event attendance** -- Share a code during a Twitter Space, Discord voice chat, or AMA and reward attendees who enter it
- **Newsletter rewards** -- Include a hidden code in your email newsletter to reward engaged readers
- **Partner activations** -- Distribute codes through partners or influencers to track which channels drive engagement
- **Scavenger hunts** -- Hide codes across your website, social channels, or community for gamified discovery
## Code types
You can configure three types of promo codes:
| Type | Description |
|------|-------------|
| **Single code** | One shared code that all users enter (e.g., `SNAG2024`) |
| **CSV upload** | Upload a CSV of unique codes -- each user gets a different code |
| **Auto-generate** | Snag generates a set of unique codes for distribution |
With unique codes (CSV or auto-generated), each code can only be redeemed once. This prevents sharing and ensures each reward goes to a unique user.
## Related pages
See full configuration options for all loyalty rules.
Boost rewards with multipliers.
---
## Complete Profile Details
*Profile completion loyalty rule. Reward users for filling out their profile information.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for completing their profile. This rule is useful for getting more engagement on the platform.

## Prerequisites
- The user must have completed their profile, which includes:
- A bio
- A profile picture
- A Twitter handle
- An email address
- A Discord handle
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: The Complete Profile rule rewards users **immediately** upon completion of the profile.
- **User Reward Frequency**: This is a one-time reward that users can claim after completing the profile.
### **Custom Settings**
- Skip the multiplier for this rule
---
## Connect Discord
*Discord connection loyalty rule. Reward users for linking their Discord account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their [Discord](https://discord.com/developers/docs) accounts to the platform. This rule encourages users to join the community Discord server and link their accounts, enabling better community engagement and cross-platform interaction.

## Prerequisites
- The user must have connected their Discord account to Snag.
## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Discord rule rewards users **immediately** upon successful connection of their Discord account and fulfillment of any required conditions (like joining a specific server).
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
In order to enable following configuration you must invite our bot to the server with read member permission.
Here is the [invite link](https://discord.com/oauth2/authorize?client_id=1079691446724075562&permissions=8&response_type=code&redirect_uri=https://admin.snagsolutions.io/&scope=bot+guilds.members.read)
- **Add Discord Guild user should follow to earn this reward**: Toggle to add a Discord guild/server that users should join to earn this reward.
- **Discord Server ID**: Enter the ID of your community's Discord server that users should join.
---
## Connect Email
*Email connection loyalty rule. Reward users for verifying their email address.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their Email accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Email rule rewards users **immediately** upon successful connection of their Email account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
---
## Connect Epic
*Epic Games connection loyalty rule. Reward users for linking their Epic Games account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their [Epic Games](https://www.epicgames.com/) accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Epic Games rule rewards users **immediately** upon successful connection of their Epic Games account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Connect GitHub
*GitHub connection loyalty rule. Reward users for linking their GitHub account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their GitHub accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect GitHub rule rewards users **immediately** upon successful connection of their GitHub account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
---
## Connect Instagram
*Instagram connection loyalty rule. Reward users for linking their Instagram account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their Instagram accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Instagram rule rewards users **immediately** upon successful connection of their Instagram account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
## **Verification Process**
Users verify ownership of their Instagram account through a bio verification process:
The system generates a unique verification code for the user when they initiate the connection process.
Users must add this verification code to their Instagram profile bio to prove
account ownership.
Users must provide their Instagram username (e.g., `@username`) or profile URL
(e.g., `https://www.instagram.com/username/`) to verify their account.
When users click "Verify", the system checks their Instagram bio for the
verification code and validates the profile information.
Once verified, the account is successfully linked and users can remove the verification code from their bio.
---
## Connect Reddit
*Reddit connection loyalty rule. Reward users for linking their Reddit account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their Reddit accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Reddit rule rewards users **immediately** upon successful connection of their Reddit account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
## **Verification Process**
Users verify ownership of their Reddit account through a profile description verification process:
The system generates a unique verification code for the user when they initiate the connection process.
Users must add this verification code to their Reddit profile description to
prove account ownership.
Users must provide their Reddit profile URL (e.g.,
`https://www.reddit.com/user/username`) to verify their account.
When users click "Verify", the system checks their Reddit profile description
for the verification code and validates the profile URL.
Once verified, the account is successfully linked and users can remove the verification code from their profile description.
---
## Connect Steam
*Steam connection loyalty rule. Reward users for linking their Steam account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their [Steam](https://store.steampowered.com/) accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Steam rule rewards users **immediately** upon successful connection of their Steam account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
---
## Connect Telegram
*Telegram connection loyalty rule. Reward users for linking their Telegram account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their [Telegram](https://telegram.org/) accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Telegram rule rewards users **immediately** upon successful connection of their Telegram account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
---
## Connect Tiktok
*TikTok connection loyalty rule. Reward users for linking their TikTok account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their Tiktok accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Tiktok rule rewards users **immediately** upon successful connection of their Tiktok account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
## **Verification Process**
Users verify ownership of their TikTok account through a bio verification process:
The system generates a unique verification code for the user when they initiate the connection process.
Users must add this verification code to their TikTok profile bio to prove
account ownership.
When users click "Verify", the system checks their TikTok bio for the
verification code.
Once verified, the account is successfully linked and users can remove the verification code from their bio.
---
## Connect X
*X (Twitter) connection loyalty rule. Reward users for linking their X account to your Web3 loyalty program.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their Twitter accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Twitter rule rewards users **immediately** upon successful connection of their Twitter account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
## How it works
1. A user visits your loyalty program and sees the "Connect X" rule
2. They click to connect, which triggers an OAuth flow to authorize their X account
3. Once authorized, Snag verifies the connection and immediately rewards the configured points
4. The user's X account is now linked to their loyalty profile, enabling other X-based rules (posts, follows, reactions)
## Prerequisites
- Your platform must have X (Twitter) authentication enabled. Contact Snag to configure OAuth credentials for your organization.
- Users must have an active X account to connect.
Connecting an X account is often a prerequisite for other X-based loyalty rules like [Follow an X Account](/loyalty/rules/follow-x-account), [Post on X](/loyalty/rules/x-new-tweet), and [X Post Reaction](/loyalty/rules/x-post-reaction). Consider setting up this rule first.
## Troubleshooting
- **"Authorization failed"** -- The user may have denied the OAuth permission. They should try again and accept all permissions.
- **Account already connected** -- Each X account can only be linked to one loyalty profile. If the account is already linked to another profile, the user will see an error.
## Related pages
See full configuration options for all loyalty rules.
Boost rewards with multipliers.
---
## Connect Wallet
*Wallet connection loyalty rule. Reward users for connecting their crypto wallet.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their Crypto Wallet to the platform..

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect Wallet rule rewards users **immediately** upon successful linking of their crypto wallet.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
- **Wallet Type**: Choose the type of wallet to connect to.
**Supported wallets:** EVM, Solana, IMX, SUI, and TON.
---
## Connect YouTube
*YouTube connection loyalty rule. Reward users for linking their YouTube account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for connecting their YouTube accounts to the platform. This rule encourages users to link their accounts, enabling better community engagement and cross-platform interaction.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Connect YouTube rule rewards users **immediately** upon successful connection of their YouTube account.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
---
## Smart Contract Event
*Smart contract event loyalty rule. Reward users for triggering specific onchain events.*
## **Overview**
This loyalty rule allows admins to reward users based on specific events occurring on a contract. This rule is useful for rewarding users for specific contract interactions, such as token transfers or transactions.

## Contract Address
Users must select a contract from the available list, or add a new contract. This contract defines where the event will be monitored.
### Recommended Rules
Users can choose from pre-configured rule templates that will show upon selection of contract, such as:
- **Reward User for Every ERC20 Transfer Received**: Grants a reward whenever a user receives any ERC20 tokens.
- **Reward User for Large ERC20 Transfer Received**: Rewards the user only if the received amount exceeds a set threshold (e.g., 1000 tokens).
## Event Selection
User will be able to see the list of all the events of the selected contract to choose from.
### Criteria
- **Every Event**: The rule applies to all occurrences of the event.
- **By Parameter**: The rule only applies when specific conditions are met.
## User Address Mapping
This lets you define a mapping of the wallet address field that should be chosen for the reward. Examples include sender, recipient, or values derived from event parameters.
## Parameter Selection
Users can define specific conditions to filter events. Each condition consists of:
- **Parameter**: The relevant field in the event (e.g., `spender`).
- **Condition**: The logic applied to the parameter (e.g., `Equal`).
- **Value**: The specific value to match (e.g., a specific wallet address like `0xd8dA6BF26964aF9D7c`).
Multiple parameters can be added, and they will be combined with an AND logic.
## Event Tracking Duration
Users can set a custom date range to track the event by enabling the toggle. The selected range determines when the event monitoring begins and ends.
## Reward Configuration
Users can define the reward granted when the event occurs.
### Points to Reward
- The user can specify the reward amount (e.g., Xp1 tokens).
- **Set Maximum**: Enables an upper limit on the number of rewards a user can earn.
- **Bonus Reward**: Optionally enables extra rewards beyond the base amount.
### Event Count & Points
Users can configure:
- **Event Count**: The number of occurrences required to trigger the reward.
- **Points to Reward**: The reward per event occurrence.
## User Reward Frequency
Determines how often a user can receive the reward. Options include:
- **One-time**: The user receives the reward only once per rule configuration.
- **Daily**: The user can receive the reward once per day.
- **Weekly**: The user can receive the reward once per week.
- **Monthly**: The user can receive the reward once per month.
## Custom Settings
Users have additional options:
- **Skip the Multiplier for this Rule**: Ignores any external multipliers affecting rewards.
- **Only Check for Known Users**: Limits rewards to users that are already present and signed into the loyalty program.
## Call to Action (Optional)
Users can define a call-to-action link with custom button text (e.g., "Browse Items").
---
## Create Partner Account
*Partner account creation loyalty rule. Reward users for creating accounts on partner platforms.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users if they create an account on a partner platform.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Create Partner Account rule rewards users **immediately** upon successful completion of the partner account creation.
- **User Reward Frequency**: This is a one-time reward that users can claim after completing the partner account creation.
### **Custom Settings**
- **Link**: This lets you define the link to the partner platform.
- **Button Text**: This lets you define the button text to the partner platform.
- Skip the multiplier for this rule
---
## dApp Activity Rules
*Reward dApp deployers based on on-chain performance metrics like gas usage, active users, new users, and transaction volume.*
## Overview
dApp Activity rules let you incentivize smart contract deployers by ranking their contracts on a chosen on-chain metric and distributing rewards based on rank tiers. This is designed for networks and ecosystems that want to drive developer engagement and reward the dApps that contribute the most on-chain activity.
Rewards go to the **deployer wallet** — the address that originally deployed the smart contract. If a single deployer has multiple contracts that rank within your configured tiers, their rewards are **aggregated** across all qualifying contracts into a single payout.
## Rule types
There are four rule types, each ranking dApps by a different metric:
| Rule type | Metric | What it measures |
| ------------------------------ | --------------------- | ----------------------------------------------------------------------- |
| **dApps by Gas Spent** | Total gas consumed | Rewards dApps generating the most computational activity on the network |
| **dApps by Active Users** | Unique active wallets | Rewards dApps with the highest number of unique interacting wallets |
| **dApps by New Users** | New unique wallets | Rewards dApps attracting the most first-time users |
| **dApps by Transaction Count** | Total transactions | Rewards dApps with the highest transaction throughput |
All four rule types share the same configuration flow, scheduling options, and
reward logic — they only differ in which on-chain metric is used for ranking.
## Availability and enablement
- **Availability**: dApp Activity rules are available to enterprise partners.
- **Network support**: Currently available on EVM-compatible chains. We add new blockchain support upon request, so please reach out to Snag if you need this configured for a specific chain.
## How it works
1. **Data collection** — Snag's analytics service continuously indexes on-chain activity (gas usage, unique wallets, transactions) per smart contract on supported chains.
2. **Ranking** — When the rule executes, it fetches the top contracts for the selected chain and metric, filtered by the configured data window and optional deployment recency filter.
3. **Tier matching** — Each ranked contract is matched against your configured reward tiers. A contract's rank determines how many points (or which badge) its deployer receives.
4. **Aggregation** — If a deployer has multiple contracts in the ranking, the rewards from all their qualifying contracts are summed into a single payout to the deployer wallet.
The data window always covers **full completed days** — it starts from the end
of the previous day (midnight UTC) and looks back. For example, if a rule with
a **weekly** data window runs on March 15th, the period analyzed is **March
7th – March 14th** (7 full days). The current day (March 15th) is not included
because it is not yet complete.
### Claim types
dApp rules support two claim modes:
| Mode | Behavior |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Auto** | The rule runs on its configured schedule (daily, weekly, or monthly). Snag's backend automatically fetches the leaderboard, calculates rewards, and distributes them to all qualifying deployer wallets — no user action needed. |
| **Manual** | Deployers visit your loyalty page and trigger a claim themselves. When they do, the system looks up contracts deployed by their connected wallet, checks the current leaderboard, and distributes any earned rewards on the spot. |
## Configuration
### Network
Select the EVM chain to analyze. The rule will rank contracts deployed on this specific network.
### Data window
Controls which time period of on-chain data is used when building the leaderboard. This is required.
| Option | Time range |
| ----------------- | ------------------------------ |
| **Last 24 hours** | Activity from the past 1 day |
| **Last 7 days** | Activity from the past 7 days |
| **Last 30 days** | Activity from the past 30 days |
Choose a data window that matches your reward cadence. For example, pair a
weekly data window with a weekly schedule so each run covers a fresh,
non-overlapping period of activity.
### Deployed within
Filters which contracts are eligible based on when they were deployed.
| Option | Eligibility |
| --------- | -------------------------------------------------------- |
| **Any** | All contracts are eligible regardless of deployment date |
| **Day** | Only contracts deployed in the last 24 hours |
| **Week** | Only contracts deployed in the last 7 days |
| **Month** | Only contracts deployed in the last 30 days |
### Reward tiers
Define how rewards are distributed based on ranking position. Each tier specifies a **start rank**, **end rank**, and **reward amount**. You can configure multiple tiers for a graduated reward structure.
**Example configuration:**
| Rank range | Reward per contract |
| ---------- | ------------------- |
| 1 – 10 | 1,000 points |
| 11 – 50 | 500 points |
| 51 – 100 | 100 points |
Contracts ranked beyond your highest configured tier receive no rewards. If a deployer has contracts at rank 5 (1,000 pts) and rank 30 (500 pts), they receive a combined 1,500 points.
You can also attach a **loyalty badge** to a tier to grant badges alongside (or instead of) point rewards.
### Frequency
Controls how often deployers can earn rewards from this rule, regardless of whether the claim type is auto or manual.
| Option | Behavior |
| ----------- | ---------------------------------------- |
| **Daily** | Deployers can be rewarded once per day |
| **Weekly** | Deployers can be rewarded once per week |
| **Monthly** | Deployers can be rewarded once per month |
The reward lifetime for dApp rules is **permanent** — once distributed, rewards are not automatically revoked.
## Best practices
- **Align data window and frequency** — If the frequency is weekly, use a weekly data window so each cycle covers fresh data without overlap or gaps.
- **Set realistic tier boundaries** — Consider how many active contracts exist on the target chain. Setting a tier that ends at rank 1,000 on a chain with 50 active contracts means most of that tier goes unused.
- **Combine with manual claims for user engagement** — Auto rules are hands-off, but manual claims drive users to your loyalty page and increase engagement touchpoints.
- **Use graduated tiers** — Rather than a flat reward for all ranked contracts, create multiple tiers with decreasing rewards to disproportionately incentivize top performers.
## Troubleshooting
| Symptom | Likely cause | Resolution |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| Manual claim says "No eligible dApps found" | The connected wallet hasn't deployed any contracts on the selected chain within the configured windows | Verify the wallet address and that the contract was deployed on the correct network |
---
## Grant Discord Role
*Discord role grant loyalty rule. Automatically assign Discord roles based on loyalty achievements.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to automatically grant a specific [Discord](https://discord.com/developers/docs) role to users when they claim the reward. This rule is useful for automating role distribution based on loyalty achievements or user actions.
## Prerequisites
- Snag Discord bot must be added to the Discord server with **Manage Roles** permission.
- [Invite Link](https://discord.com/oauth2/authorize?client_id=1079691446724075562&permissions=8&response_type=code&redirect_uri=https://admin.snagsolutions.io/&scope=bot+guilds.members.read)
- The Snag Bot role must be higher in the Discord role hierarchy than the role being granted.
- The user must have connected their Discord account to Snag.
**Important:** The Snag Discord bot requires the **Manage Roles** permission
to function correctly. Additionally, ensure the bot's role is positioned above
the role you intend to grant in your Discord server settings.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: This defines when the rule will run. It can also be claimed manually by users via the **"Claim" button**.
- **User Reward Frequency**: This defines how often users can receive the reward (e.g., one-time).
### **Custom Settings**
- **Skip the multiplier for this rule**: If enabled, any global multipliers will not apply to this rule's reward.
- **Discord Server Id**: The ID of the Discord server where the role will be granted.
- **Discord Role Id**: The ID of the role(s) that will be granted to the users upon completion. You can define multiple role IDs to grant multiple roles at once.
---
## Get Discord Role
*Discord member role loyalty rule. Reward users who hold specific Discord server roles.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for having a certain role in their [Discord](https://discord.com/developers/docs) server. This rule is useful for encouraging users to join a specific Discord server and engage with the platform.

## Prerequisites
- Snag Discord bot must be added to the Discord server with the following permissions:
- View Audit Log
- Read Message History
- [Invite Link](https://discord.com/oauth2/authorize?client_id=1079691446724075562&permissions=8&response_type=code&redirect_uri=https://admin.snagsolutions.io/&scope=bot+guilds.members.read)
- The user must have connected their Discord account to Snag.
- The user must have any one of the defined roles in the Discord server.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: This defines that the rule will run daily/weekly/monthly at the defined start time and check if the users in the system have the defined role in the Discord server, This rule can also be claimed from the UI by clicking on the **"Claim" button** by the users.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
- **Discord Server Id**: The ID of the Discord server where the rule will be applied.
- **Discord Role Id**: The ID of the role that will be rewarded to the users. You can define multiple role ids and user will be rewarded if they have any of the roles.
---
## Send Discord Messages
*Discord messages loyalty rule. Reward users for engaging in Discord conversations.*
import RewardWithMultiplierRangeAmount from '/snippets/rules/reward-with-multiplier-range-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for sending messages in their [Discord](https://discord.com/developers/docs) server. This rule rewards discord messages based on a specific emoji response, normally from a moderator, or a specific string of text included by the user. This rule is useful for encouraging users to be more engaged in the Discord server.

## Prerequisites
- Snag Discord bot must be added to the Discord server with the following permissions:
- Read Messages
- Read Message History
- Read Message Content
- [Invite Link](https://discord.com/oauth2/authorize?client_id=1079691446724075562&permissions=8&response_type=code&redirect_uri=https://admin.snagsolutions.io/&scope=bot+guilds.members.read)
- The user must have connected their Discord account to Snag.
- The user must have message in defined channel with either specific emoji or text.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: This defines that the rule will run daily/weekly/monthly at the defined start time and check if the users has sent the defined number of messages in the Discord server which also includes the specific text or reaction, This rule can also be claimed from the UI by clicking on the **"Claim" button** by the users.
- **User Reward Frequency**: This defines how often the users can claim the reward, available options are **"One-time"**, **"Daily"**, **"Weekly"**, **"Monthly"** and **"Every Message"**. For **"Every Message"** option, the users will be rewarded for every message they send in the Discord server channel. Daily/Weekly/Monthly options will reward the users to claim the reward daily/weekly/monthly, this can encourage users to be more active in the Discord server.
### **Custom Settings**
- Skip the multiplier for this rule
- **Discord Channel Id**: The ID of the Discord channel where the rule will be applied.
**Message will be counted in the reward range if any of the following is true**:
- **Text**: The text that should be included in the message to be included in the reward range.
- **Emoji**: This rule rewards the user whose message got reacted to by the specified emoji. The emoji can be added by any user or moderator in the Discord server. The emoji must be either:
- The ID of a custom emoji (e.g., `123456789012345678`)
- The unicode version of an emoji (e.g., `🔥`, `🙏`, `👍`, `✅`)
If you self-react on your own message (reacting to your own message with the specified emoji), it will not reward you. The reaction must come from a different user.
To get the unicode version of an emoji, right-click on the emoji in Discord and select "Copy Emoji".

Pasting the text name of Discord's default emojis (e.g., typing `:gorilla:`) will not work. You must use the unicode version of the emoji (e.g., `🦍`).
You can define multiple emojis and the message will be counted if any of the emojis are present in the message.
---
## Follow X Account
*Follow X account loyalty rule. Reward users for following specific X (Twitter) accounts.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for following a specific X account. This rule is useful for encouraging users to follow a specific X account.

## Prerequisites
- The user must have connected their X account to Snag.
- The user must follow the defined X account and submit the claim.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: Points for this rule will be rewarded immediately on user claim after they follow the defined X account.
- **User Reward Frequency**: This defines how often the user will be rewarded. You can choose between one time, daily, weekly, or monthly.
### **Custom Settings**
- **X Account URL**: This is the URL of the X account that the user will follow.
- Skip the multiplier for this rule
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Merge a GitHub PR
*GitHub PR merge loyalty rule. Reward users for getting pull requests merged into your repository.*
import RewardWithMultiplierRangeAmount from '/snippets/rules/reward-with-multiplier-range-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for merging pull requests to a specific branch in a [GitHub](https://github.com/) repository. This rule is useful for incentivizing contributions to your open-source projects and rewarding active developers who merge PRs to your codebase.

## **Prerequisites**
- The user must have connected their GitHub account to Snag.
- The user must have merged at least one pull request to the specified branch in the target repository.
- The user's GitHub access token must be valid and have appropriate permissions to access the repository.
## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Merge GitHub PR rule can be configured to run on a **daily**, **weekly**, or **monthly** interval. The system will check for merged PRs during each interval period.
- **User Reward Frequency**: Users can claim rewards **once**, **daily**, **weekly**, or **monthly** based on the configured frequency. The system tracks merged PRs within the specified time window.
**Time Window Restriction**: Only PRs merged within the **last interval
period** are counted toward rewards. For example: - **Daily rule**: Only PRs
merged from the last day to today will be counted - **Weekly rule**: Only PRs
merged from the last week to today will be counted - **Monthly rule**: Only
PRs merged from the last month to today will be counted PRs merged outside of
this time window will not be included in the reward calculation.
### **Custom Settings**
- **Skip the multiplier for this rule**: Toggle to skip the multiplier for this rule
- **GitHub Repo URL** \*: The full URL of the GitHub repository where users need to merge PRs (e.g., `https://github.com/owner/repository`). This is a required field that identifies the target repository.
- **GitHub Branch Name**: The name of the branch where PRs must be merged (e.g., `main`, `develop`, `master`). If not specified, the system defaults to `main`. Only PRs merged to this specific branch will be counted.
## **Verification Process**
Users verify their GitHub PR merges through the following process:
Users must first connect their GitHub account to Snag if they haven't already done so. The system requires a valid GitHub OAuth access token to verify PR merges.
Users create and merge a pull request to the specified branch in the target
GitHub repository. The PR must be merged (not just opened or closed) to count
toward the reward.
After merging PRs, users can claim the reward through the loyalty rule
interface. The system will verify the number of merged PRs within the
configured time window.
The system verifies that: - The user's GitHub account has access to the
repository - The user has merged PRs to the specified branch - The PRs were
merged within the configured time window (based on the rule's interval) - The
repository URL and branch name match the configured values - The merged PRs
were authored by the user's connected GitHub account
The system counts the total number of merged PRs within the time window and
calculates rewards based on the configured range tiers. If range-based rewards
are configured, users receive rewards corresponding to the number of PRs
they've merged.
Once verified, users receive their reward based on the number of merged PRs and the configured reward ranges.
## **How It Works**
The rule uses the GitHub Search API to count merged pull requests. The system:
1. **Queries GitHub API**: Searches for merged PRs using the GitHub Search API with filters for:
- Repository owner and name
- Branch name (base branch)
- Author (the user's GitHub username)
- Merge date (within the last interval period - e.g., last 24 hours for daily rules, last 7 days for weekly rules, last 30 days for monthly rules)
- PR status (merged only)
2. **Counts Merged PRs**: Returns the total count of merged PRs matching the criteria.
3. **Calculates Rewards**: If range-based rewards are configured, the system matches the PR count to the appropriate reward tier and distributes the corresponding reward.
4. **Handles Token Refresh**: The system automatically refreshes expired GitHub access tokens when needed to ensure continuous verification.
## **Important Notes**
The GitHub Repo URL should be in the format
`https://github.com/owner/repository`. The system will automatically parse the
owner and repository name from the URL.
The GitHub Branch Name is optional and defaults to `main` if not specified.
Only PRs merged to the specified branch will be counted. Make sure the branch
name matches exactly (case-sensitive).
The system only counts PRs that have been **merged** (not just opened or
closed). Draft PRs or PRs that were closed without merging will not be
counted.
The rule supports range-based rewards, allowing you to reward users with
different amounts based on the number of PRs they've merged. For example, you
can reward 10 points for 1-2 PRs, 25 points for 3-5 PRs, and 50 points for 6+
PRs.
**Interval-Based Counting**: The system only counts PRs merged within the
current interval period. When the rule runs: - For **daily** rules: Only PRs
merged in the last 24 hours (from the last run to now) are counted - For
**weekly** rules: Only PRs merged in the last 7 days (from the last run to
now) are counted - For **monthly** rules: Only PRs merged in the last 30 days
(from the last run to now) are counted This ensures that users are rewarded
for recent contributions within each interval, not for all-time PR counts.
## **Example Use Cases**
- **Open Source Projects**: Reward contributors who merge PRs to your main branch
- **Development Teams**: Incentivize team members to merge PRs to specific feature branches
- **Community Engagement**: Encourage community contributions by rewarding merged PRs on a weekly or monthly basis
- **Quality Contributions**: Use range-based rewards to incentivize multiple contributions (e.g., more PRs = higher rewards)
---
## Become a GitHub Repository Collaborator
*GitHub collaborator loyalty rule. Reward users who are collaborators on your repository.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for becoming a collaborator on a specific [GitHub](https://github.com/) repository. This rule is useful for recognizing and incentivizing contributors who have been granted collaborator access to your repository.

## **Prerequisites**
- The user must have connected their GitHub account to Snag.
- The user must be added as a collaborator to the specified GitHub repository by the repository owner or administrator.
## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The GitHub Repository Collaborator rule rewards users **immediately** upon successful verification that they are a collaborator on the repository.
- **User Reward Frequency**: This is a one-time reward that users can claim after being added as a collaborator.
### **Custom Settings**
- **Skip the multiplier for this rule**: Toggle to skip the multiplier for this rule
- **GitHub Repo URL** \*: The full URL of the GitHub repository where users need to be collaborators (e.g., `https://github.com/owner/repository`). This is a required field that identifies the target repository.
## **Verification Process**
Users verify their GitHub repository collaborator status through the following process:
Users must first connect their GitHub account to Snag if they haven't already done so.
Repository owners or administrators must add the user as a collaborator to the
specified GitHub repository through the repository settings.
After being added as a collaborator, users can claim the reward through the
loyalty rule interface.
The system verifies that: - The user is listed as a collaborator on the
specified repository
Once verified, users receive their reward immediately.
## **Important Notes**
The GitHub Repo URL should be in the format
`https://github.com/owner/repository`.
To add a user as a collaborator, go to your repository on GitHub, navigate to
Settings → Collaborators, and add the user by their GitHub username. The user
will need to accept the collaboration invitation before they can claim the
reward.
---
## Fork a GitHub Repository
*GitHub repo fork loyalty rule. Reward users for forking your GitHub repository.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for forking a specific [GitHub](https://github.com/) repository. This rule is useful for encouraging developers to contribute to your open-source projects and build community engagement.

## **Prerequisites**
- The user must have connected their GitHub account to Snag.
- The user must fork the specified GitHub repository.
## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Fork GitHub Repository rule rewards users **immediately** upon successful verification that the repository has been forked.
- **User Reward Frequency**: This is a one-time reward that users can claim after forking the repository.
### **Custom Settings**
- **Skip the multiplier for this rule**: Toggle to skip the multiplier for this rule
- **GitHub Repo URL** \*: The full URL of the GitHub repository that users need to fork (e.g., `https://github.com/owner/repository`). This is a required field that identifies the target repository.
## **Verification Process**
Users verify their GitHub repository fork through the following process:
Users must first connect their GitHub account to Snag if they haven't already done so.
Users navigate to the specified GitHub repository using the provided URL and
click the fork button to create a fork of the repository.
After forking the repository, users can claim the reward through the loyalty
rule interface.
The system verifies that: - The repository has been forked by the user's
connected GitHub account - The fork is a direct fork of the specified
repository
Once verified, users receive their reward immediately.
## **Important Notes**
The GitHub Repo URL should be in the format
`https://github.com/owner/repository`.
---
## Star a GitHub Repository
*GitHub repo star loyalty rule. Reward users for starring your GitHub repository.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for starring a specific [GitHub](https://github.com/) repository. This rule is useful for increasing repository visibility and building community engagement around your open-source projects.

## **Prerequisites**
- The user must have connected their GitHub account to Snag.
- The user must star the specified GitHub repository.
## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Star GitHub Repository rule rewards users **immediately** upon successful verification that the repository has been starred.
- **User Reward Frequency**: This is a one-time reward that users can claim after starring the repository.
### **Custom Settings**
- **Skip the multiplier for this rule**: Toggle to skip the multiplier for this rule
- **GitHub Repo URL** \*: The full URL of the GitHub repository that users need to star (e.g., `https://github.com/owner/repository`). This is a required field that identifies the target repository.
## **Verification Process**
Users verify their GitHub repository star through the following process:
Users must first connect their GitHub account to Snag if they haven't already done so.
Users navigate to the specified GitHub repository using the provided URL and
click the star button to star the repository.
After starring the repository, users can claim the reward through the loyalty
rule interface.
The system verifies that: - The user's GitHub account has access to the
repository - The repository has been starred by the user's connected GitHub
account - The repository URL matches the configured repository
Once verified, users receive their reward immediately.
## **Important Notes**
Users must have their GitHub account connected before they can claim this
reward. The system will automatically verify that the repository has been
starred by the connected GitHub account.
The GitHub Repo URL should be in the format
`https://github.com/owner/repository`.
---
## Post on Instagram
*Instagram post loyalty rule. Reward users for creating Instagram content about your brand.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for creating posts on [Instagram](https://www.instagram.com/). This rule encourages users to create content and engage with your brand on Instagram, helping to increase your social media presence and community engagement.

## **Prerequisites**
- The user must have connected their Instagram account to Snag.
- The user must create a new post on Instagram.
## **Rule Configuration Fields**
### **Reward criteria**
- **Post should include**: Specify hashtags or text that posts must include in the caption to be eligible for rewards
- Example: `#dreamersclub` or `#dreamers on tour`
- You can add multiple criteria using the "Add Another" button
- The system checks if any of the specified texts appear in the post caption (case-insensitive)
### **Basic Settings**
- **Update Cadence**: This defines how often the rule will run to check for new Instagram posts. You can set it to:
- **Once**: Users can only earn the reward once
- **Daily**: Users can earn the reward once per day
- **Weekly**: Users can earn the reward once per week
- **Monthly**: Users can earn the reward once per month
- **User Reward Frequency**: This defines how often users can claim rewards for their Instagram posts. The reward is distributed **immediately** upon successful verification.
### **Custom Settings**
- **Skip multiplier**: Skip the multiplier for this rule
- **Post must include media**: Toggle to require posts to include media (images or videos). When enabled, text-only posts will not be eligible for rewards.
## **Verification Process**
Users verify their Instagram post through the following process:
Users must first connect their Instagram account to Snag if they haven't already done so.
Users create a post on Instagram with the required text or hashtags in the
caption.
Users must submit the Instagram post URL (e.g.,
`https://www.instagram.com/p/SHORTCODE/`) for verification. The URL can be for regular posts, Reels, or IGTV posts.
The system verifies that: - The post exists and is valid - The post belongs to
the user's connected Instagram account - The post caption contains the
required text or hashtags - The post includes media (if required) - The post
was created after the rule start date (if applicable)
Once verified, users receive their reward immediately.
## **Important Notes**
Users must ensure their Instagram account is connected before creating posts.
Posts created before connecting the account may not be eligible for rewards.
Users must provide a valid Instagram post URL for verification. The system supports verification of regular posts (`/p/`), Reels (`/reel/`), and IGTV posts (`/tv/`). All post types are eligible for rewards as long as they meet the specified criteria.
---
## Click a Link
*Link click loyalty rule. Reward users for visiting specific URLs.*
## **Overview**
This loyalty rule allows admins to reward users based on the click of a specific link. This rule is useful for redirecting users to new launch or specific pages.

## **Rule Configuration Fields**
### **Reward**
- **Points**: Users will get the defined no of points.
### **Basic Settings**
- **Update Cadence**: The Link Click rule rewards users **immediately** upon successful click of the link.
- **User Reward Frequency**: This defines how often the user can earn the reward, e.g. daily, weekly, monthly, etc. If set to daily, the user can earn the reward once per day based on UTC time.
### **Custom Settings**
- **Verification Placeholder**: This placeholder text appears inside the input field to guide users on what to type.
- **Minimum verification text length**: Specify the number of characters that must be entered in the verification text.
- Skip the multiplier for this rule
- **Link**: Provide a URL for users to take further action.
- **Platform**: This lets you choose from specific list of platforms of which icon will be shown in the rule card for better user experience.
- available platforms:
- discord
- epic games
- instagram
- steam
- telegram
- tiktok
- twitch
- twitter/X
- youtube
- \*\*Time delay to verify \*\*: The time in seconds we'll wait before rewarding users for clicking the link to encourage completion of the flow.
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
## **Downloading Responses**
You can download all link click responses as a CSV file. This feature is available for link click rules with any reward type (points, multiplier, or badge).
The CSV file includes the following columns:
- **walletAddress**: The wallet address of the user who clicked the link
- **textInput**: The verification text that the user entered (if verification is enabled)
- **createdAt**: The timestamp when the user clicked the link
To download responses, navigate to the link click rule in your admin dashboard and click the download button. The CSV file will be generated and made available for download.
## **Implementation Considerations for API-Based Partners**
The Link Click rule is designed for direct user interactions through the Snag platform's UI. For API-based partners, please note:
1. **No Click Verification**:
- The system does not verify if users actually clicked the provided link
- Rewards are granted when the completion is called, regardless of whether the link was clicked
2. **Implementation Note**:
- While the rule is named "Link Click", it functions more as a completion-based reward
- You can use this rule to reward users for any action that triggers the completion, not necessarily a link click
---
## Liquidity: Uniswap V2 & V3
*Reward liquidity activity across most EVM DEXes by targeting Uniswap V2/V3-compatible pools with configurable rewards and safeguards.*
## **Overview**
Snag's liquidity rules let you reward users for activity in decentralized exchanges (DEXes) that implement the standard Uniswap pool interfaces:
- **[Uniswap V2](https://docs.uniswap.org/contracts/v2/overview) (constant-product AMM)**: the classic x·y=k invariant model.
- **[Uniswap V3](https://docs.uniswap.org/contracts/v3/overview) (concentrated liquidity AMM)**: LPs allocate liquidity to price ranges, with fees tracked per-liquidity-unit across ticks.
Most EVM DEXes adopt one of these standards for their core pool contracts (swaps, liquidity positions, fees). Because of this alignment, a single rule can cover the majority of EVM AMMs by pointing to the correct protocol factory on a given chain.
Some alternative designs — for example, Curve stableswap or Balancer weighted pools — fall outside this model.
We integrate with the core, standardized pool interfaces only. Custom DEX features (hooks, bonus math, rewards) typically sit on top of V2/V3 and don’t affect our ability to read the canonical liquidity data.
## **Availability & Enablement**
- **Partner enablement required**: This rule is disabled by default. Contact Snag to enable it for your workspace.
- **Protocol onboarding**: We enable protocols on request. Provide the DEX **factory address** and target **network**; we will add the protocol so it shows up in the rule configuration.
- **Chain support**: We support chains where we can map the DEX factory and the chain has standard V2/V3-compatible pools.
Submit the exact factory address for the DEX you want to support. The rule validates pools against this factory and will reject pools that don’t match.
## **How it works**
At configuration time you specify:
- **Chain**: Any supported EVM network.
- **Protocol**: A V2/V3-compatible DEX identified by its namespace/name and factory address.
- **Pools**: Up to 5 pool addresses from the selected chain and protocol.
During validation we verify each pool’s factory matches the selected protocol’s factory. This ensures the pools you enter actually belong to the chosen DEX implementation on that network.
## **Rule configuration fields**
This rule supports fixed-amount rewards with optional multipliers.
### **Liquidity options**
- **Chain**: Choose the target network where pools exist.
- **Protocol**: Select an enabled Uniswap V2/V3-compatible DEX (Snag enables these on request).
- **Pools**: Enter up to five pool addresses belonging to the selected protocol and chain.
Use block explorers or protocol UIs to confirm a pool address is deployed by the target factory.
### **Custom rewards function**
Enter a deterministic formula that maps the base variable `X` (e.g., daily liquidity provided in USD) to a reward value.
- **Example**: `X * 5` — for each \$1 of qualifying liquidity per day, award 5 points.
- **Inverse rewards**: Reward smaller players proportionally more (e.g., `k / max(X, ε)` within bounds) to reduce whales dominating.
- **Limiting functions**: Cap rewards per interval or per user (e.g., `min(X * r, cap)`) to keep things competitive and predictable.
These functions apply at calculation time and do not change on-chain pool math. They simply modulate how many points a user earns from qualifying liquidity events.
See also: [Loyalty rules logic](/loyalty/x-rules-logic). Ask Snag to enable custom functions for your workspace. Provide desired formulas or guardrails (caps, floors, decay) and we’ll configure them.
## **Best practices**
- Validate each entered pool address belongs to the selected factory.
- Group pools by the same protocol and chain for simpler reasoning.
- Consider a limiting function to prevent farm-and-dump behavior.
- Use multipliers sparingly when inverse rewards are enabled to avoid runaway incentives.
## **Troubleshooting**
- **Pool rejected**: The pool’s factory doesn’t match the selected protocol’s factory. Re-check the address.
- **Protocol missing**: Contact Snag with the chain and factory address to have it enabled.
- **No rewards accruing**: Confirm the rule is enabled for your workspace and that pools are valid.
---
## Answer a Poll
*This recipe explains the concept of a poll rule, how to configure it with Snag and how to issue rewards to your users.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on the completion of a poll.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Poll rule rewards users **immediately** upon successful completion of the poll.
- **User Reward Frequency**: This is a one-time reward that users can claim after completing the poll.
### **Custom Settings**
- Skip the multiplier for this rule
- **Question**: This lets you define the poll question.
- **Answers**: This lets you define the poll answers.
## **Downloading Responses**
You can download all poll responses as a CSV file. This feature is available for poll rules with any reward type (points, multiplier, or badge).
The CSV file includes the following columns:
- **walletAddress**: The wallet address of the user who completed the poll
- **questionText**: The poll question
- **selectedChoiceText**: The answer choice the user selected
- **createdAt**: The timestamp when the user completed the poll
To download responses, navigate to the poll rule in your admin dashboard and click the download button. The CSV file will be generated and made available for download.
---
## Answer a Quiz
*This recipe explains the concept of a quiz rule, how to configure it with Snag and how to issue rewards to your users.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on the completion of a quiz.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Quiz rule rewards users **immediately** upon successful completion of the quiz.
- **User Reward Frequency**: This is a one-time reward that users can claim after completing the quiz.
### **Custom Settings**
- Skip the multiplier for this rule
- **Question**: This is the question that will be asked to the user.
- **Answers**: This is the list of options that will be shown to the user.
- **Correct Answer**: This checkbox lets you define the correct answer to the question.
- **Allow multiple attempts**: This is a boolean that allows the user to attempt the quiz multiple times.
## **Downloading Responses**
You can download all quiz responses as a CSV file. This feature is available for quiz rules with any reward type (points, multiplier, or badge).
The CSV file includes the following columns:
- **walletAddress**: The wallet address of the user who completed the quiz
- **questionText**: The question that was asked
- **selectedChoiceText**: The answer choice the user selected
- **isCorrect**: Whether the selected answer was correct (true/false)
- **createdAt**: The timestamp when the user completed the quiz
To download responses, navigate to the quiz rule in your admin dashboard and click the download button. The CSV file will be generated and made available for download.
---
## Comment on a Reddit Post
*Reddit comment loyalty rule. Reward users for commenting in specific subreddits.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for commenting on specific [Reddit](https://www.reddit.com/) posts. This rule is useful for increasing engagement with your Reddit content and building community interaction.

## **Prerequisites**
- The user must have connected their Reddit account to Snag.
- The user must comment on the specified Reddit post.
- The user must provide the link to the comment they made on the Reddit post for verification.
## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Reddit Comment rule rewards users **immediately** upon successful verification of a comment on the specified Reddit post.
- **User Reward Frequency**: This defines how often the user can earn the reward, e.g. daily, weekly, monthly, etc. If set to daily, the user can earn the reward once per day based on UTC time.
### **Custom Settings**
- **Skip the multiplier for this rule**: Toggle to skip the multiplier for this rule
- **Reddit Post ID** \*: The ID of the Reddit post that users need to comment on. This can be found in the URL of your Reddit post (e.g., `https://www.reddit.com/r/subreddit/comments/abc123/post_title/` where `abc123` is the Post ID).
- **Reddit Post link** \*: This will add a button on the rule display page to take the user directly to the Reddit post. This is a required field that helps users navigate to the correct post.
## **Verification Process**
Users verify their Reddit comment through the following process:
Users must first connect their Reddit account to Snag if they haven't already done so.
Users navigate to the specified Reddit post using the provided link and create
a comment.
After commenting, users must copy the link to their comment and submit it for
verification through the loyalty rule interface.
The system verifies that: - The comment exists and is valid - The comment
belongs to the user's connected Reddit account - The comment was made on the
specified Reddit post
Once verified, users receive their reward immediately.
## **Important Notes**
Users must provide a valid comment link for verification. Comments that don't
belong to the user or aren't on the specified post will not be verified.
The Reddit Post ID can be extracted from the post URL. For example, in the URL
`https://www.reddit.com/r/example/comments/abc123xyz/example_post/`, the Post
ID is `abc123xyz`.
---
## Refer a Friend
*Referral loyalty rule. Reward users for inviting friends to join your platform.*
## **Overview**
This loyalty rule rewards users for referring others, with multiple reward structures supporting first-level and second-level referrals. An admin sets up a referral program where User X gets 1000 points for each new referral, and new users (User Y) get 500 points upon joining. Additionally, User X earns 10% of User Y's earnings and 5% of User Z's earnings. This rule is checked daily, ensuring that referrers continue to earn from their referrals' activity over time.

## **Rule Configuration Fields**
- **Referral Reward Amount for Referrer**: When User X refers to User Y, User X gets rewarded..
- **Points Rewarded to New User**: When User X refers to User Y, User Y gets an initial reward.
- **Percentage Points to Reward for First Referral**: When User X refers to User Y, User X can earn a percentage of Y's earnings based on cadence (daily/weekly/monthly).
- **Percentage Points to Reward for Second Referral**: When User X refers to User Y, and Y refers to User Z, both X and Y can earn a percentage of Z's earnings.
### **Update Cadence**
The Update Cadence field is crucial as it defines:
- **Reward Frequency**: This is only applicable to percentage based rewards, this determines how often the user will receive rewards (e.g., Daily, Weekly, Monthly).
- **Relation to Start Time**: The first verification of all the referrals and reward distribution will begin at the defined Start Time. Subsequent verifications and rewards will follow the selected cadence (e.g., if the cadence is set to "Daily," the system will check and reward holdings every 24 hours from the Start Time).
### **Custom Settings**
Users qualify for referral rewards based on conditions such as achieving points, completing profile details, or connecting social accounts.
## **API Usage**
For API partners, referral rules can be implemented programmatically using the Snag API. After creating a user in Snag, you'll need to follow these steps to set up referrals:
### **Step 1: Generate Referral Code**
Use the [Create referral code](/api-reference/referrals/create-referral-code) endpoint to generate a unique referral code for a user. This code will be used to track referrals.
This endpoint can be treated as both "create" and "get" - it will not generate
a new code if the user already has one. Instead, it will return the existing
referral code, making it safe to call multiple times.
See the [API reference](/api-reference/referrals/create-referral-code) for response structure.
### **Step 2: Create Referral User**
When someone uses a referral code, use the [Create referral user](/api-reference/referrals/create-referral-user) endpoint to register the referral relationship.
See the [API reference](/api-reference/referrals/create-referral-user) for response structure.
### **Step 3: Retrieve Referral Data**
Use the [Get referrals](/api-reference/referrals/get-referrals) endpoint to retrieve referral details including eligibility status and points earned at each tier level. The response includes:
- **`eligible`**: Whether the referred user meets eligibility requirements
- **`amount`**: Direct referral fixed amount
- **`tier1Amount`**: Points earned from tier-1 referrals
- **`tier2Amount`**: Points earned from tier-2 referrals
See the [API reference](/api-reference/referrals/get-referrals) for complete response structure.
The `amount`, `tier1Amount`, and `tier2Amount` fields only return data for earnings after 2025-04-24.
### **Step 4: Retrieve Referral Points**
Snag doesn't store referral points as a single aggregated number per user. Query [Loyalty Transaction Entries](/api-reference/loyalty/get-loyalty-transaction-entries) using `userId` and `loyaltyRuleId` to get all referral point transactions. See the [API reference](/api-reference/loyalty/get-loyalty-transaction-entries#parameter-loyalty-rule-id) for details.
To display a total referral points balance, aggregate these entries on your side. See [Managing Rule-Level Points](/loyalty/manage-rule-level-points) for the recommended approach.
Combine both endpoints: use Get Referrals to show referred friends and their status, and use Loyalty Transaction Entries to show transaction history and calculate totals.
### **Viewing Referrals in Looker Studio**
If your organization has **Looker Studio** access enabled through Snag, you can view referral data directly in your CRM Looker view without using the API.
To view referred users:
1. Navigate to your CRM Looker Studio dashboard
2. Open the **"referred users"** tab
3. Filter by a specific wallet address to see all users referred by that address
Filtering by wallet address allows you to quickly identify all users that were
referred by a specific referrer, making it easy to track referral performance
and relationships.
If Looker Studio is **not yet set up** for your organization, [contact us](https://www.snagsolutions.io/contact) to request access or an extension of your existing Looker Studio dashboard. The Snag team can help you enable the integration and ensure your referral data is automatically synced for full visibility.
### **Implementation Flow**
Create a new user in your Snag system using your existing user management endpoints.
Call the create referral code endpoint to generate a unique referral code for the user.
When a new user signs up using a referral code, call the create referral user endpoint to establish the referral relationship.
Ensure you're calling this endpoint with the correct user IDs to properly link referrer and referee.
Use the get referrals endpoint to retrieve referral data for analytics, reporting, or user dashboard displays.
The referral rule will automatically process rewards based on your
configuration once the referral relationship is established through the API
calls.
### **Quest Chaining with Referral Rules**
When using quest chaining to link a referral rule to another quest, the referral reward system works as follows:
- **Referrer Eligibility**: The user who made the referral (referrer) will only become eligible for rewards after the referred user completes the chained quest
- **Quest Completion Requirement**: The referred user must successfully complete the linked quest before any referral rewards are processed
- **Automatic Processing**: Once the chained quest is completed, the referral rule automatically processes and distributes rewards according to your configuration at the specified cadence (daily/weekly/monthly)
If quest chaining is enabled, referral rewards will not be processed until the
chained quest is completed by the referred user, regardless of when the
referral relationship was established.
---
## Participate in Governance Vote
*Snapshot governance loyalty rule. Reward users for voting on DAO governance proposals via Snapshot.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on their participation in Snapshot governance https://snapshot.box/.
This rule is useful for rewarding users for voting on proposals or participating in governance activities.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: This defines that the rule will run daily/weekly/monthly at the defined start time and check if the users in the system have voted on the proposal.
- **User Reward Frequency**: This is a one-time reward that users will be rewarded once they have voted on a proposal at the specified start time.
### **Custom Settings**
- **Snapshot Space Name**: The name of the snapshot space to check for governance activities.
- **Snapshot Proposal Id**: The id of the snapshot proposal to check for governance activities.
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
## How it works
[Snapshot](https://snapshot.box/) ([developer docs](https://docs.snapshot.org/)) is a decentralized governance platform where DAOs and communities create and vote on proposals. This loyalty rule connects to Snapshot's API to verify whether a user has voted on a specific proposal within your Snapshot space.
1. You configure the rule with your Snapshot space name and proposal ID
2. Snag periodically checks Snapshot's API for votes on that proposal
3. Any user in your system who has voted receives the configured reward
4. Votes are verified onchain -- users cannot fake participation
## Prerequisites
- Your project must have a [Snapshot space](https://snapshot.box/) set up
- The proposal you want to track must be live or completed on Snapshot
- Users must connect a wallet address that matches their Snapshot voting address
This rule works with any Snapshot space, not just your own. You can reward users for participating in governance across the broader ecosystem.
## Related pages
See full configuration options for all loyalty rules.
Boost rewards with multipliers.
---
## Spend in Shopify Store
*Shopify purchase loyalty rule. Reward users for spending in your Shopify store.*
import RewardWithMultiplierRangeAmount from '/snippets/rules/reward-with-multiplier-range-amount.mdx'
## Overview
Reward users based on how much they have spent in your Shopify store. On claim, we match the user’s connected email to a Shopify customer and evaluate their lifetime spend against your configured ranges to award points or a multiplier.
## Prerequisites
- Shopify must be connected in Snag with a Shopify Admin API access token.
- Required scopes: `read_customers` and `read_orders` (needed to look up customers and read lifetime spend).
- The user must have connected an email address in Snag (used to find the Shopify customer).
- You must provide your Shopify store URL in the rule configuration.
Store your Shopify Admin API access token securely and grant only the minimum
required scopes.
## Rule Configuration Fields
### Basic Settings
- **Claim type**: Manual — users click "Claim" to trigger evaluation and rewards.
- **Frequency**: Immediately — the rule evaluates and awards at the time of claim.
- **Interval**: Once — users can claim once (unless otherwise configured globally).
- **Reward lifetime**: Permanent — awarded points remain unless explicitly removed.
### Custom Settings
- **Shopify Store URL**: Your store domain (for example, `your-store.myshopify.com`).
- **Skip the multiplier for this rule**: If enabled, only fixed point rewards are used for matching ranges.
## How it works
1. When a user clicks "Claim", Snag queries the [Shopify Admin GraphQL API](https://shopify.dev/docs/api/admin-graphql) for the customer by the user’s connected email.
2. We read the customer’s lifetime `amountSpent` and compare it to your configured spend ranges.
3. The user is awarded the points or multiplier defined for the first matching range.
If the email doesn’t match a Shopify customer, `amountSpent` is treated as 0 —
ensure users connect the same email they use at checkout.
## Troubleshooting
- **Shopify not connected**: Ensure you’ve connected Shopify in Snag and selected the integration when creating this rule.
- **Email not connected**: Users must connect an email address in their Snag profile before claiming.
- **No reward given**: Confirm your ranges include 0 or the user’s actual spend, and that your store URL is correct.
## Data captured
For auditing, the reward stores the Shopify `customerId` and the evaluated `amountSpent` at claim time.
---
## Add an Item to Steam Wishlist
*Steam wishlist loyalty rule. Reward users for adding games to their Steam wishlist.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for wishlisting games on [Steam](https://store.steampowered.com/). This rule is useful for promoting your game and increasing visibility on the Steam platform.

## Prerequisites
- The user must have connected their Steam account to Snag.
- The user must have added the specified game to their Steam wishlist.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: The Steam Wishlist rule rewards users **immediately** upon successfully adding the specified game to their Steam wishlist.
- **User Reward Frequency**: This defines how often the user can earn the reward, e.g. daily, weekly, monthly, etc. If set to daily, the user can earn the reward once per day based on UTC time.
### **Custom Settings**
- Skip the multiplier for this rule
- **Steam App ID**: The ID of the Steam game that users need to wishlist. This can be found in the URL of your Steam store page (e.g., https://store.steampowered.com/app/1234567/Game_Name/ where 1234567 is the App ID).
- **Go to Steam Store**: This will add a button on the rule display page to take the user to the Steam store page for the game.
## Limitations
- API usage is rate-limited. To avoid restrictions, you can provide your own Steam API key.
---
## Custom Function Rule
*Create custom loyalty rules using JavaScript functions to evaluate complex conditions and determine rewards dynamically.*
## Overview
The Custom Function Rule allows you to create sophisticated loyalty rules using JavaScript code. This rule type leverages Stratus functions to evaluate complex conditions, perform calculations, and determine rewards based on custom logic that goes beyond standard rule types.
## Key Features
- **Custom Logic**: Write JavaScript functions to implement any evaluation logic
- **Dynamic Rewards**: Calculate variable reward amounts based on user data or conditions
- **External Data**: Access external APIs and services using approved modules
- **Complex Conditions**: Implement multi-step validation and conditional rewards
- **Flexible Output**: Return points, badges, or multipliers based on your logic
## Use Cases
Reward users based on complex gaming achievements, streaks, or skill levels.
Calculate rewards based on social media engagement metrics or community
participation.
Implement dynamic reward structures based on user tier, activity level, or
spending patterns.
Combine data from multiple platforms to create unified reward systems.
## How It Works
Create a JavaScript function that receives user data and loyalty rule information as input.
Your function must follow the Stratus function syntax and use only approved modules.
Implement your custom logic to determine if a user qualifies for rewards and
calculate the appropriate reward amount.
Your function returns a structured object describing the reward to grant
(points, badges, or multipliers).
The platform automatically executes your function when users attempt to complete the rule.
## Function Requirements
Your custom function must:
- Accept `(input, output)` parameters
- Parse the input data containing user and rule information
- Call `output.setResult([rewardObject])` with your reward details
- Return `output.buildOutput()`
- Use only approved modules: `axios`, `lodash`, `viem`, and `viem/chains`
Functions that don't follow the required syntax or use unauthorized modules
will fail validation.
## Input Data Structure
Your function receives the following input structure:
```javascript
{
user: {
id: string,
walletAddress: string,
walletType: string
},
loyaltyRule: {
id: string,
name: string,
type: string,
amount: number,
metadata: object,
startTime: string,
endTime: string,
interval: string,
rewardType: string
}
}
```
## Output Requirements
Your function must return an array with a single reward object containing:
- **walletAddress**: The user's wallet address to reward
- **amount**: Points to grant (for fixed point rewards)
- **idempotencyKey**: Unique identifier to prevent duplicate rewards
- **data**: Optional JSON string with additional context (max 1KB)
- **loyaltyBadgeId**: Optional badge to award instead of points
- **loyaltyMultiplierAmount**: Optional multiplier for range-based rules
## Example Use Cases
### Dynamic Point Calculation
```javascript
// Calculate points based on user's social media followers
const followerCount = await getFollowerCount(user.socialId)
const basePoints = 10
const bonusPoints = Math.floor(followerCount / 1000) * 5
const totalPoints = basePoints + bonusPoints
output.setResult([
{
walletAddress: user.walletAddress,
amount: totalPoints,
idempotencyKey: `social-bonus-${user.id}-${Date.now()}`,
data: JSON.stringify({ followerCount, bonusPoints }),
},
])
```
### Conditional Badge Award
```javascript
// Award special badge for high-value users
const userValue = await calculateUserValue(user.id)
const isHighValue = userValue > 1000
if (isHighValue) {
output.setResult([
{
walletAddress: user.walletAddress,
loyaltyBadgeId: 'high-value-user-badge-id',
idempotencyKey: `high-value-${user.id}`,
},
])
}
```
## Getting Started
Read the [Stratus Function Syntax](/stratus/syntax) guide to understand the required export patterns and validation rules.
Follow the [Function Loyalty Rule Guide](/stratus/function-loyalty-rule) for a
complete tutorial on creating custom loyalty rule functions.
Use the function editor in the loyalty rule form to write and test your custom
logic.
Save your rule and monitor function execution through the admin dashboard.
## Best Practices
**Keep functions simple and focused** - Complex logic should be broken into
smaller, testable functions.
**Handle errors gracefully** - Always include try-catch blocks and meaningful
error messages.
**Use idempotency keys** - Ensure your functions can be safely retried without
creating duplicate rewards.
**Optimize for performance** - Minimize external API calls and use efficient
data structures.
## Troubleshooting
- Ensure your function follows the exact syntax requirements
- Check that you're only using approved modules
- Verify your output structure matches the required format
- Review function logs for specific error messages - Test your function with
sample data before deploying - Ensure all external dependencies are available
- Optimize external API calls and database queries
- Consider caching frequently accessed data
- Monitor function execution time and memory usage
## Related Documentation
- [Stratus Function Syntax](/stratus/syntax) - Learn the required function structure
- [Function Loyalty Rule Guide](/stratus/function-loyalty-rule) - Complete tutorial for writing loyalty rule functions
- [Function Templates](/stratus/function-templates) - Ready-to-use function examples
- [Loyalty API Reference](/api-reference/loyalty) - API documentation for loyalty rules
---
## Swap Tokens
*Token swap loyalty rule. Reward users for swapping tokens on supported DEXs.*
## **Overview**
This loyalty rule rewards users for swapping tokens through supported DEX aggregators. Users are rewarded based on their total swap volume or token amounts within a specified time window. The rule supports filtering by specific providers, chains, tokens, and can require cross-chain swaps.
This rule is triggered **on user request**—users can call the completion endpoint via API or through your loyalty template, and the system will check their swap history to determine rewards.

## **Rule Configuration Fields**
### **Provider**
Select which DEX aggregator to track swaps from:
- **Any**: Track swaps from all supported providers
- **Relay**: Only track swaps executed through [Relay](https://relay.link)
Currently, only Relay is fully supported. Additional providers will be added in future updates.
### **Relay Referrer ID (Optional)**
When using Relay as the provider, you can optionally specify a referrer ID to filter swaps by a specific exchange.
- **Purpose**: Filter swaps to a specific DEX within the Relay network
- **Example**: `opensea`, `uniswap`
- **Leave blank**: To reward swaps across all Relay-supported exchanges
### **Chain**
Select the blockchain network where swaps must occur:
- **All**: Track swaps on any supported blockchain
- **Specific Chain**: Select a particular network (Ethereum, Arbitrum, Polygon, etc.)
If you select specific tokens, the chain will be automatically determined by the token configurations.
### **Token Mode**
Choose how to filter which token swaps are rewarded:
#### **Any token on this chain**
Users are rewarded for swapping to any token on the selected chain(s).
#### **Specific tokens only**
Users are only rewarded for swapping to designated tokens. Configure each token with:
- **Chain**: The blockchain network for this token
- **Token Address**: The contract address of the token (e.g., `0x...`)
Add multiple tokens to create an allowlist. Users swapping to any of these tokens will be eligible for rewards.
### **Track Token Amount**
This setting is only available when exactly **one** specific token is configured.
When enabled, the reward calculation changes:
- **Disabled (Default)**: Rewards are based on total USD swap volume
- **Enabled**: Rewards are based on the actual token amount received
Token decimals are automatically handled by the system—you don't need to account for them in your configuration.
### **Require Cross-Chain Swap**
Toggle this setting to only reward swaps where the source and destination chains are different.
- **Use case**: Incentivize users to bridge assets between networks
- **Behavior**: Swaps on the same chain are excluded from rewards
### **Reward**
Define reward tiers based on swap volume or token amounts:
- **Type of Reward**: Choose between "Points" or "Multiplier"
- **Points**: Users receive a fixed number of points
- **Multiplier**: Users receive a points multiplier for the specified range
- **Required Swap Range**: Define minimum swap volume/amount thresholds
- If tracking USD: Define ranges in USD (e.g., $100, $500, $1000)
- If tracking token amount: Define ranges in token units
- **Points to Reward**: Specify the reward for each range tier
- **Add New Range**: Create multiple tiers for progressive rewards
Users receive rewards for the highest tier they qualify for based on their total swap volume or amount.
### **Update Cadence**
The **Update Cadence** field defines:
1. **Reward Frequency**: How often users can claim rewards (e.g., Daily, Weekly, Monthly)
2. **Lookback Window**: The time period to check for swap activity
3. **Relation to Start Time**: The first check and reward distribution begins at the defined **Start Time**. Subsequent checks follow the selected cadence
If no start time is set, the system looks back 24 hours from when the user requests completion.
### **Custom Settings**
- **Only reward existing users**: Limit rewards to users already enrolled in the loyalty program instead of all eligible wallets
### **Call to Action (Optional)**
Display a call to action button in the loyalty rule interface:
- **Link**: Provide a URL where users can perform swaps (e.g., your DEX interface or Relay link)
- **Button Text**: Customize the button label (e.g., "Swap Now", "Start Trading")
## **How It Works**
The user calls the rule completion endpoint via API or clicks to complete the rule in your loyalty template.
Snag queries the configured provider (Relay) for the user's swap transactions within the time window.
The system applies all configured filters:
- Provider/referrer ID
- Chain requirements
- Specific tokens (if configured)
- Cross-chain requirement
Total swap volume (USD) or token amount is summed across all qualifying swaps.
Based on the calculated total, the user receives rewards for the highest tier they qualify for.
## **Configuration Examples**
### **Example 1: Reward Any Swap on Arbitrum**
- Provider: Any
- Chain: Arbitrum
- Token Mode: Any token on this chain
- Range: $100 = 10 points, $500 = 50 points, $1000 = 150 points
**Result**: Users get points based on total USD swap volume on Arbitrum.
### **Example 2: Reward Specific Token Purchases**
- Provider: Relay
- Chain: (Auto-selected from tokens)
- Token Mode: Specific tokens only
- Token 1: Ethereum, 0x6B175474E89094C44Da98b954EedeAC495271d0F (DAI)
- Token 2: Ethereum, 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 (USDC)
- Track Token Amount: Disabled
- Range: $50 = 5 points, $250 = 30 points
**Result**: Users are rewarded for swapping to DAI or USDC on Ethereum, based on USD volume.
### **Example 3: Reward Cross-Chain Swaps**
- Provider: Relay
- Chain: All
- Token Mode: Any token on this chain
- Require Cross-Chain Swap: Enabled
- Range: $100 = 20 points, $500 = 120 points
**Result**: Only cross-chain swaps are rewarded, regardless of the tokens involved.
### **Example 4: Track Specific Token Amount**
- Provider: Relay
- Chain: Arbitrum
- Token Mode: Specific tokens only
- Token: Arbitrum, 0x912CE59144191C1204E64559FE8253a0e49E6548 (ARB)
- Track Token Amount: Enabled
- Range: 100 ARB = 10 points, 500 ARB = 60 points, 1000 ARB = 150 points
**Result**: Users are rewarded based on the total amount of ARB tokens they've swapped to, not USD value.
## **Important Notes**
This rule only tracks **successful** swaps. Failed or pending transactions are not included in reward calculations.
The time window for checking swaps is determined by the rule's start time and update cadence. If no start time is set, the default lookback period is 24 hours.
When using specific tokens across multiple chains, ensure each token address is correct for its respective network. Token addresses differ between chains.
## **API Integration**
Users can trigger this rule completion via API:
```bash
POST /api/loyalty/rules/{ruleId}/complete
```
The system will automatically fetch and evaluate their swap history based on the configured parameters.
See the [Verifying Rule Completion](/loyalty/verifying-rule-completion) documentation for detailed API usage.
## **Best Practices**
**Incentivize specific behaviors**: Use the cross-chain requirement to encourage users to bridge assets, or specify tokens to drive liquidity to particular assets.
**Layer ranges strategically**: Create multiple reward tiers to encourage higher swap volumes. Users will strive to reach the next tier for better rewards.
**Choose the right metric**: Use USD volume for broad swap incentives, but switch to token amount tracking when you want to drive adoption of a specific token regardless of price fluctuations.
**Set appropriate cadences**: Daily cadences work well for active trading communities, while weekly or monthly cadences suit longer-term holder incentives.
---
## Join Telegram Group
*Telegram join loyalty rule. Reward users for joining your Telegram group or channel.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for joining their [Telegram](https://telegram.org/) Group. This rule is useful for building a community in Telegram.

## Prerequisites
- Snag [Telegram loyalty bot](https://t.me/snag_loyalty_bot) must be added to the Telegram group with admin permissions:
- [Bot Link](https://t.me/snag_loyalty_bot)
- The user must have connected their Telegram account to Snag.
- The user must have joined the defined Telegram group.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: This defines that the rule will run daily/weekly/monthly at the defined start time and check if new users has joined the defined Telegram group. This rule can also be claimed from the UI by clicking on the **"Claim" button** by the users.
- **User Reward Frequency**: This is one time reward and user will be rewarded once they join the defined Telegram group.
### **Custom Settings**
- Skip the multiplier for this rule
- **Group link**: This will add the button on rule display page to take the user to the Telegram group.
- **Telegram group ID**: The identifier for the Telegram group where the rule will be applied.
For public group and group, you can use either:
- **Username**: Enter the group username (e.g., `@groupname` or `groupname`)
- **Invite link**: Use the full invite link (e.g., `https://t.me/groupname`)
For private group, you need the group ID:
Go to [https://web.telegram.org](https://web.telegram.org) and navigate to your private group or group.
Copy the URL from your browser's address bar. It will look like: https://web.telegram.org/k/#-3243034605
You can use either:
- The full URL: `https://web.telegram.org/k/#-3243034605`
- Just the ID number: `-3243034605`
The ID will always be a negative number for private group. Make sure to include the minus sign when copying just the ID number.
## Troubleshooting
### User Verification Issues
When users report verification issues with Telegram join rules, follow this troubleshooting process:
If the user was already a member of the Telegram group before the Snag loyalty bot was added as an admin, they may experience verification issues. In such cases, instruct the user to:
**Leave and rejoin the group**
This simple action will refresh their membership status and allow the system to properly verify their participation. This is a common solution for users who joined before the loyalty rule was configured.
---
## Send Telegram Messages
*Telegram messages loyalty rule. Reward users for engaging in Telegram conversations.*
import RewardWithMultiplierRangeAmount from '/snippets/rules/reward-with-multiplier-range-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for sending messages in their [Telegram](https://telegram.org/) group. This rule is useful for encouraging users to be more engaged in the Telegram group.

## Prerequisites
- Snag [Telegram loyalty bot](https://t.me/snag_loyalty_bot) must be added to the Telegram group with admin permissions:
- [Bot Link](https://t.me/snag_loyalty_bot)
- The user must have connected their Telegram account to Snag.
- The user must have sent messages in defined group.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: This defines how often the user will be rewarded and also how often the rule will check if the user has sent the defined number of messages in the Telegram channel.
### **Custom Settings**
- Skip the multiplier for this rule
- **Group link**: This will add the button on rule display page to take the user to the Telegram group.
- **Telegram Group Id**: The ID of the Telegram group where the rule will be applied.
For public group, you can use either:
- **Username**: Enter the group username (e.g., `@groupname` or `groupname`)
- **Invite link**: Use the full invite link (e.g., `https://t.me/groupname`)
For private group, you need the group ID:
Go to [https://web.telegram.org](https://web.telegram.org) and navigate to your private group.
Copy the URL from your browser's address bar. It will look like: https://web.telegram.org/k/#-3243034605
You can use either:
- The full URL: `https://web.telegram.org/k/#-4908688673`
- Just the ID number: `-4908688673`
The ID will always be a negative number for private group. Make sure to include the minus sign when copying just the ID number.
---
## Submit Text Input
*Text input loyalty rule. Collect and reward free-form text responses from users.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on the text input in a specific field.

## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: The Text Input rule rewards users **immediately** upon successful text input.
- **User Reward Frequency**: This defines how often the user can earn the reward, e.g. daily, weekly, monthly, etc. If set to daily, the user can earn the reward once per day based on UTC time.
### **Custom Settings**
- **Button Text**: This is the text that will be shown on the button that will show the text input field.
- **Verification Placeholder**: This placeholder text appears inside the input field to guide users on what to type.
- **Minimum verification text length**: Specify the number of characters that must be entered in the verification text.
- Skip the multiplier for this rule
## **Downloading Responses**
You can download all text input responses as a CSV file. This feature is available for text input rules with any reward type (points, multiplier, or badge).
The CSV file includes the following columns:
- **walletAddress**: The wallet address of the user who submitted the text input
- **textInput**: The text that the user entered (verification code)
- **createdAt**: The timestamp when the user submitted the text input
To download responses, navigate to the text input rule in your admin dashboard and click the download button. The CSV file will be generated and made available for download.
---
## Follow a TikTok Account
*TikTok follow loyalty rule. Reward users for following your TikTok account.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for following a specific [TikTok](https://www.tiktok.com/) account. This rule is useful for encouraging users to follow your TikTok account and build your social media presence.

## **Prerequisites**
- The user must have connected their TikTok account to Snag.
- The user must follow the specified TikTok account and submit the claim.
## **Rule Configuration Fields**
### **Basic Settings**
- **Update Cadence**: Points for this rule will be rewarded immediately on user claim after they follow the specified TikTok account.
- **User Reward Frequency**: This defines how often the user will be rewarded. You can choose between one time, daily, weekly, or monthly.
### **Custom Settings**
- **TikTok Account URL** \*: This is the URL of the TikTok account that users need to follow (e.g., `https://www.tiktok.com/@username`). This is a required field that identifies the target account.
- **Skip the multiplier for this rule**: Toggle to skip the multiplier for this rule
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
## **Verification Process**
Users verify their TikTok follow through the following process:
Users must first connect their TikTok account to Snag if they haven't already done so.
Users navigate to the specified TikTok account using the provided URL and
follow the account.
After following the account, users can claim the reward through the loyalty
rule interface.
The system verifies that: - The user's TikTok account has access to the
platform - The user is following the specified TikTok account - The account
URL matches the configured account
Once verified, users receive their reward immediately.
## **Important Notes**
Users must have their TikTok account connected before they can claim this
reward. The system will automatically verify that the account has been
followed by the connected TikTok account. If verification fails, users should
try unfollowing and following the account again, then retry the claim.
The TikTok Account URL should be in the format
`https://www.tiktok.com/@username` where `username` is the TikTok username of
the account to follow.
---
## Post on TikTok
*TikTok post loyalty rule. Reward users for creating TikTok content about your brand.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users for creating posts on [TikTok](https://www.tiktok.com/). This rule encourages users to create content and engage with your brand on TikTok, helping to increase your social media presence and community engagement.

## **Prerequisites**
- The user must have connected their TikTok account to Snag.
- The user must create a new post on TikTok.
## **Rule Configuration Fields**
### **Reward criteria**
- **Post should include**: Specify hashtags or text that posts must include to be eligible for rewards
- Example: `#dreamersclub` or `#dreamers on tour`
- You can add multiple criteria using the "Add Another" button
- **Impression count**: Allows defining multiple view count ranges with corresponding rewards. Users are rewarded based on how many views their TikTok posts receive, with different reward amounts for different impression ranges (e.g., 1-100 views, 101-1000 views, 1000+ views).
- **Every eligible post**: Users receive the fixed reward amount for TikTok post they create.
### **Basic Settings**
- **Update Cadence**: This defines how often the rule will run to check for new TikTok posts. You can set it to:
- **Once**: Users can only earn the reward once
- **Daily**: Users can earn the reward once per day
- **Weekly**: Users can earn the reward once per week
- **Monthly**: Users can earn the reward once per month
- **Unlimited**: Users can earn rewards for every post they create
- **User Reward Frequency**: This defines how often users can claim rewards for their TikTok posts.
### **Custom Settings**
- **Skip multiplier**: Skip the multiplier for this rule
---
## Hold a Fungible Token
*ERC-20 token hold loyalty rule. Reward users for holding specific ERC-20 tokens in their wallet.*
## **Overview**
This loyalty rule allows admins to reward users based on their [ERC-20](https://eips.ethereum.org/EIPS/eip-20) holdings or Native Currency balances across supported blockchain networks. The rule enables the aggregation of token balances across multiple collections and defines a structured reward system based on holding requirements.

## **Rule Configuration Fields**
### **Contract Address**
- **Asset Type**: Choose between **ERC-20 Token** and **Native Currency**.
- **Token contract address**: (For ERC-20) Enter one or more contract addresses representing the collections.
- **Network**: Select the blockchain network where the asset is deployed.
- **Add New Token**: Allows adding multiple ERC-20 collections for token aggregation.
- **Use Direct Blockchain Query (RPC)**: Enable this option to track user balances via direct RPC calls.
When enabled, the system executes a direct RPC balance call for **every user currently existing in the system** (signed-up users). It fetches the real-time balance for each user, matches it against the configured **Reward** ranges, and distributes rewards accordingly. This ensures accurate tracking for native currencies or specific tokens without relying on indexed data, but effectively limits the scope to users already tracked by the platform.
### **Reward**
- **Type of Reward**: Choose between **Points**, **Multiplier**, or **Badge**.
- **Range**: Define a range of tokens that need to be held for eligibility.
- **Points to reward**: Define the number of points assigned for the specified holding range.
- **Reward per token**: Toggle on to reward users per token held.
- **Set max reward**: Set a maximum limit on the rewards a user can receive.
- **Reward by batch**: Enable to process rewards in batches.
- **Reward badge per range**: Assign specific badges for different holding ranges (available when Badge reward type is selected).
- **Only reward existing users**: If enabled, this will limit rewards to users already in the loyalty program instead of all eligible wallets.
### **Update Cadence**
The **Update Cadence** field is crucial as it defines:
1. **Reward Frequency**: Determines how often the user will receive rewards (e.g., Daily, Weekly, Monthly).
2. **Holding Verification**: Specifies when the system will check the token holdings to determine eligibility.
3. **Relation to Start Time**: The first verification of token holdings and reward distribution will begin at the defined **Start Time**. Subsequent verifications and rewards will follow the selected cadence (e.g., if the cadence is set to "Daily," the system will check and reward holdings every 24 hours from the Start Time).
### **Custom Settings**
- **Skip the multiplier for this rule**: Enable this to exclude this rule from global multiplier calculations.
### **Call to Action (Optional)**
This will show a call to action in our loyalty rule card.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Hold an NFT
*NFT hold loyalty rule. Reward users for holding specific NFTs in their wallet.*
## **Overview**
This loyalty rule allows admins to reward users based on their NFT holdings across supported blockchain networks. The rule enables the aggregation of token balances across multiple collections and defines a structured reward system based on holding requirements.

## **Delegated Wallets and Cold Storage**
We support combining token holdings from delegated wallets using [delegate.cash](https://delegate.cash/). When your connected hot wallet has a full-wallet delegation from a cold wallet on Ethereum mainnet, NFTs held by the delegated cold wallet are aggregated with the hot wallet when evaluating this rule.
We use Delegate Cash to resolve delegations and combine holdings for eligibility and rewards.
Limitations:
- Only full-wallet delegations are considered. Collection- or token-level delegations are not supported.
- Only Ethereum mainnet delegations are tracked. Delegations on other networks are ignored.
Scope: Delegation-based holding checks apply only to the "Hold an NFT" rule. No other loyalty rules use delegated wallet holdings.
See our complete guide on enabling and using delegate.cash integration, including how it differs from user groups and troubleshooting tips.
## **Rule Configuration Fields**
### **Contract Address**
- **Collection Contract Address**: Enter one or more contract addresses representing the collections.
- **Network**: Select the blockchain network where the contract is deployed.
- **Add New Collection**: Allows adding multiple collections for token aggregation.
- **Important Note**: Token holdings from different collections will be aggregated to determine total holdings when more than one collection is added.
### **Reward**
- **Type of Reward**: Choose between "Points" or "Multiplier".
- **Points**: Users will get the defined no of points based on their holdings.
- **Multiplier**: Users will get the multiplier of the defined range based on their holdings.
- **Reward Per Token**: Toggle on to reward users per token held.
- **Required Hold Tokens Range**: Define a range of tokens that need to be held for eligibility.
- **Points to Reward**: Define the number of points assigned for the specified holding range.
- **Add New Range**: Allows defining multiple token holding ranges with corresponding rewards.
- **Only reward existing users**: If enabled this will limit rewards to users already in the loyalty program instead of all eligible wallets.
### **Update Cadence**
The **Update Cadence** field is crucial as it defines:
1. **Reward Frequency**: Determines how often the user will receive rewards (e.g., Daily, Weekly, Monthly).
2. **Holding Verification**: Specifies when the system will check the token holdings to determine eligibility.
3. **Relation to Start Time**: The first verification of token holdings and reward distribution will begin at the defined **Start Time**. Subsequent verifications and rewards will follow the selected cadence (e.g., if the cadence is set to "Daily," the system will check and reward holdings every 24 hours from the Start Time).
### **Custom Settings**
- Only reward users that bought on native marketplace
- Only reward users with unlisted tokens
- Skip the multiplier for this rule
- Only reward single token ownership per collection
- **Add API to get dynamic rewards for each token**: Configure an external API endpoint to provide custom reward amounts for individual tokens. You'll need to provide:
- **Custom Rewards URL**: Your API endpoint URL that returns paginated reward data
- **API Key**: Authentication key sent in the `x-api-key` header
Your API must return a paginated JSON response with `tokenId` and `reward` (or `points`/`bonus`) fields. The API endpoint must be accessible from Snag's servers and handle pagination through a `next` field. See [Custom Rewards API](/loyalty/custom-rewards-api) for detailed API format requirements and examples.
This feature works for both [ERC-721](https://eips.ethereum.org/EIPS/eip-721) and [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155) token standards. Use the numeric token ID (e.g., `123`, `456` for ERC721 or `1`, `2`, `3` for ERC1155). When multiple collections are configured, the same token ID across different collections will all match the custom reward for that token ID.
**Interaction with Token Hold Ranges**: When custom rewards are enabled alongside token hold ranges, the system calculates rewards per token:
- Tokens with custom rewards use their specified reward amount from the API
- Tokens without custom rewards fall back to the range amount for the user's total token count
- The final reward is the sum of all individual token rewards
Example: If a user holds tokens `1`, `2`, and `3`, and their total count (3) falls within range 1-5 with a range reward of 10 points:
- Token `1` has custom reward 50 from API → uses 50 points
- Token `2` has custom reward 100 from API → uses 100 points
- Token `3` has no custom reward → uses 10 points (range fallback)
- **Total reward: 160 points** (50 + 100 + 10)
- **Add custom reward for each token**: Upload a CSV file directly through the UI to assign custom reward amounts for specific tokens. The CSV file must include exactly two columns: `tokenId` and `reward`. Both columns are required, and values must be positive numbers. The file is validated before upload to ensure proper formatting.
This feature works for both ERC721 and ERC1155 token standards. Use the numeric token ID (e.g., `123`, `456` for ERC721 or `1`, `2`, `3` for ERC1155). When multiple collections are configured, the same token ID across different collections will all match the custom reward for that token ID.
Example CSV format:
```csv
tokenId,reward
123,50
456,100
789,25
```
When multiple collections are configured, if token ID `1` exists in multiple contracts, defining `1,50` in your CSV will apply the 50-point reward to all instances of token ID `1` across all configured collections.
**Interaction with Token Hold Ranges**: When custom rewards are enabled alongside token hold ranges, the system calculates rewards per token:
- Tokens with custom rewards use their specified reward amount
- Tokens without custom rewards fall back to the range amount for the user's total token count
- The final reward is the sum of all individual token rewards
Example: If a user holds tokens `1`, `2`, and `3`, and their total count (3) falls within range 1-5 with a range reward of 10 points:
- Token `1` has custom reward 50 → uses 50 points
- Token `2` has custom reward 100 → uses 100 points
- Token `3` has no custom reward → uses 10 points (range fallback)
- **Total reward: 160 points** (50 + 100 + 10)
- Only reward users that have paid full royalty
- Only reward users that have verified Twitter accounts
### **Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Mint an NFT
*Token mint loyalty rule. Reward users for minting tokens or NFTs.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on the minting of tokens in specific collections. This rule is useful for motivating users to mint new collections.

## **Rule Configuration Fields**
### **Contract Address**
- **Token Contract Address**: Enter the contract address of the token.
- **Network**: Select the blockchain network where the contract is deployed.
### **Update Cadence**
The **Update Cadence** field is crucial as it defines:
1. **Reward Frequency**: Determines how often the user will receive rewards (e.g., Daily, Weekly, Monthly).
2. **Mint Verification**: Specifies when the system will check the token mint to determine eligibility.
3. **Relation to Start Time**: The first verification of token mint and reward distribution will begin at the defined **Start Time**. Subsequent verifications and rewards will follow the selected cadence (e.g., if the cadence is set to "Daily," the system will check and reward mints every 24 hours from the Start Time).
### **Custom Settings**
- Only reward users with unlisted tokens. This can only be enabled if all the collections are of erc721 type.
- Skip the multiplier for this rule
- **Add API to get dynamic rewards for each token**: Configure an external API endpoint to provide custom reward amounts for individual tokens. You'll need to provide:
- **Custom Rewards URL**: Your API endpoint URL that returns paginated reward data
- **API Key**: Authentication key sent in the `x-api-key` header
Your API must return a paginated JSON response with `tokenId` and `reward` (or `points`/`bonus`) fields. The API endpoint must be accessible from Snag's servers and handle pagination through a `next` field. See [Custom Rewards API](/loyalty/custom-rewards-api) for detailed API format requirements and examples.
This feature works for both ERC721 and ERC1155 token standards. Use the numeric token ID (e.g., `123`, `456` for ERC721 or `1`, `2`, `3` for ERC1155). When multiple collections are configured, the same token ID across different collections will all match the custom reward for that token ID.
**Interaction with Token Hold Ranges**: When custom rewards are enabled alongside token hold ranges, the system calculates rewards per token:
- Tokens with custom rewards use their specified reward amount from the API
- Tokens without custom rewards fall back to the range amount for the user's total token count
- The final reward is the sum of all individual token rewards
Example: If a user holds tokens `1`, `2`, and `3`, and their total count (3) falls within range 1-5 with a range reward of 10 points:
- Token `1` has custom reward 50 from API → uses 50 points
- Token `2` has custom reward 100 from API → uses 100 points
- Token `3` has no custom reward → uses 10 points (range fallback)
- **Total reward: 160 points** (50 + 100 + 10)
- **Add custom reward for each token**: Upload a CSV file directly through the UI to assign custom reward amounts for specific tokens. The CSV file must include exactly two columns: `tokenId` and `reward`. Both columns are required, and values must be positive numbers. The file is validated before upload to ensure proper formatting.
This feature works for both ERC721 and ERC1155 token standards. Use the numeric token ID (e.g., `123`, `456` for ERC721 or `1`, `2`, `3` for ERC1155). When multiple collections are configured, the same token ID across different collections will all match the custom reward for that token ID.
Example CSV format:
```csv
tokenId,reward
123,50
456,100
789,25
```
When multiple collections are configured, if token ID `1` exists in multiple contracts, defining `1,50` in your CSV will apply the 50-point reward to all instances of token ID `1` across all configured collections.
**Interaction with Token Hold Ranges**: When custom rewards are enabled alongside token hold ranges, the system calculates rewards per token:
- Tokens with custom rewards use their specified reward amount
- Tokens without custom rewards fall back to the range amount for the user's total token count
- The final reward is the sum of all individual token rewards
Example: If a user holds tokens `1`, `2`, and `3`, and their total count (3) falls within range 1-5 with a range reward of 10 points:
- Token `1` has custom reward 50 → uses 50 points
- Token `2` has custom reward 100 → uses 100 points
- Token `3` has no custom reward → uses 10 points (range fallback)
- **Total reward: 160 points** (50 + 100 + 10)
### **Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Purchase an NFT
*Token purchase loyalty rule. Reward users for purchasing specific tokens.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on the purchase of a specific token. This rule is useful for pushing users to purchase a specific token.

## **Rule Configuration Fields**
### **Contract Address**
- **Token Contract Address**: Enter the contract address of the token.
- **Network**: Select the blockchain network where the contract is deployed.
### **Update Cadence**
The **Update Cadence** field is crucial as it defines:
1. **Reward Frequency**: Determines how often the user will receive rewards (e.g., Daily, Weekly, Monthly).
2. **Purchase Verification**: Specifies when the system will check the token purchase to determine eligibility.
3. **Relation to Start Time**: The first verification of token purchase and reward distribution will begin at the defined **Start Time**. Subsequent verifications and rewards will follow the selected cadence (e.g., if the cadence is set to "Daily," the system will check and reward purchases every 24 hours from the Start Time).
### **Custom Settings**
- Only reward users that bought on native marketplace
- Only reward users with unlisted tokens. This can only be enabled if all the collections are of erc721 type.
- Skip the multiplier for this rule
- **Add API to get dynamic rewards for each token**: Configure an external API endpoint to provide custom reward amounts for individual tokens. You'll need to provide:
- **Custom Rewards URL**: Your API endpoint URL that returns paginated reward data
- **API Key**: Authentication key sent in the `x-api-key` header
Your API must return a paginated JSON response with `tokenId` and `reward` (or `points`/`bonus`) fields. The API endpoint must be accessible from Snag's servers and handle pagination through a `next` field. See [Custom Rewards API](/loyalty/custom-rewards-api) for detailed API format requirements and examples.
This feature works for both ERC721 and ERC1155 token standards. Use the numeric token ID (e.g., `123`, `456` for ERC721 or `1`, `2`, `3` for ERC1155). When multiple collections are configured, the same token ID across different collections will all match the custom reward for that token ID.
**Interaction with Token Hold Ranges**: When custom rewards are enabled alongside token hold ranges, the system calculates rewards per token:
- Tokens with custom rewards use their specified reward amount from the API
- Tokens without custom rewards fall back to the range amount for the user's total token count
- The final reward is the sum of all individual token rewards
Example: If a user holds tokens `1`, `2`, and `3`, and their total count (3) falls within range 1-5 with a range reward of 10 points:
- Token `1` has custom reward 50 from API → uses 50 points
- Token `2` has custom reward 100 from API → uses 100 points
- Token `3` has no custom reward → uses 10 points (range fallback)
- **Total reward: 160 points** (50 + 100 + 10)
- **Add custom reward for each token**: Upload a CSV file directly through the UI to assign custom reward amounts for specific tokens. The CSV file must include exactly two columns: `tokenId` and `reward`. Both columns are required, and values must be positive numbers. The file is validated before upload to ensure proper formatting.
This feature works for both ERC721 and ERC1155 token standards. Use the numeric token ID (e.g., `123`, `456` for ERC721 or `1`, `2`, `3` for ERC1155). When multiple collections are configured, the same token ID across different collections will all match the custom reward for that token ID.
Example CSV format:
```csv
tokenId,reward
123,50
456,100
789,25
```
When multiple collections are configured, if token ID `1` exists in multiple contracts, defining `1,50` in your CSV will apply the 50-point reward to all instances of token ID `1` across all configured collections.
**Interaction with Token Hold Ranges**: When custom rewards are enabled alongside token hold ranges, the system calculates rewards per token:
- Tokens with custom rewards use their specified reward amount
- Tokens without custom rewards fall back to the range amount for the user's total token count
- The final reward is the sum of all individual token rewards
Example: If a user holds tokens `1`, `2`, and `3`, and their total count (3) falls within range 1-5 with a range reward of 10 points:
- Token `1` has custom reward 50 → uses 50 points
- Token `2` has custom reward 100 → uses 100 points
- Token `3` has no custom reward → uses 10 points (range fallback)
- **Total reward: 160 points** (50 + 100 + 10)
- Only reward users that have paid full royalty
- Only reward users that have verified Twitter accounts.
- Only reward users who have purchased in specific currency.
### **Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Sale an NFT
*Token sale loyalty rule. Track and reward participation in token sales.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on the sale of tokens in specific collections. This rule is useful having rotation of tokens.

## **Rule Configuration Fields**
### **Contract Address**
- **Token Contract Address**: Enter the contract address of the token.
- **Network**: Select the blockchain network where the contract is deployed.
### **Update Cadence**
The **Update Cadence** field is crucial as it defines:
1. **Reward Frequency**: Determines how often the user will receive rewards (e.g., Daily, Weekly, Monthly).
2. **Sale Verification**: Specifies when the system will check the token sale to determine eligibility.
3. **Relation to Start Time**: The first verification of token sale and reward distribution will begin at the defined **Start Time**. Subsequent verifications and rewards will follow the selected cadence (e.g., if the cadence is set to "Daily," the system will check and reward sales every 24 hours from the Start Time).
### **Custom Settings**
- Only reward users that sold on native marketplace
- Only reward users with unlisted tokens. This can only be enabled if all the collections are of erc721 type.
- Skip the multiplier for this rule
- **Add API to get dynamic rewards for each token**: Configure an external API endpoint to provide custom reward amounts for individual tokens. You'll need to provide:
- **Custom Rewards URL**: Your API endpoint URL that returns paginated reward data
- **API Key**: Authentication key sent in the `x-api-key` header
Your API must return a paginated JSON response with `tokenId` and `reward` (or `points`/`bonus`) fields. The API endpoint must be accessible from Snag's servers and handle pagination through a `next` field. See [Custom Rewards API](/loyalty/custom-rewards-api) for detailed API format requirements and examples.
This feature works for both ERC721 and ERC1155 token standards. Use the numeric token ID (e.g., `123`, `456` for ERC721 or `1`, `2`, `3` for ERC1155). When multiple collections are configured, the same token ID across different collections will all match the custom reward for that token ID.
**Interaction with Token Hold Ranges**: When custom rewards are enabled alongside token hold ranges, the system calculates rewards per token:
- Tokens with custom rewards use their specified reward amount from the API
- Tokens without custom rewards fall back to the range amount for the user's total token count
- The final reward is the sum of all individual token rewards
Example: If a user holds tokens `1`, `2`, and `3`, and their total count (3) falls within range 1-5 with a range reward of 10 points:
- Token `1` has custom reward 50 from API → uses 50 points
- Token `2` has custom reward 100 from API → uses 100 points
- Token `3` has no custom reward → uses 10 points (range fallback)
- **Total reward: 160 points** (50 + 100 + 10)
- **Add custom reward for each token**: Upload a CSV file directly through the UI to assign custom reward amounts for specific tokens. The CSV file must include exactly two columns: `tokenId` and `reward`. Both columns are required, and values must be positive numbers. The file is validated before upload to ensure proper formatting.
This feature works for both ERC721 and ERC1155 token standards. Use the numeric token ID (e.g., `123`, `456` for ERC721 or `1`, `2`, `3` for ERC1155). When multiple collections are configured, the same token ID across different collections will all match the custom reward for that token ID.
Example CSV format:
```csv
tokenId,reward
123,50
456,100
789,25
```
When multiple collections are configured, if token ID `1` exists in multiple contracts, defining `1,50` in your CSV will apply the 50-point reward to all instances of token ID `1` across all configured collections.
**Interaction with Token Hold Ranges**: When custom rewards are enabled alongside token hold ranges, the system calculates rewards per token:
- Tokens with custom rewards use their specified reward amount
- Tokens without custom rewards fall back to the range amount for the user's total token count
- The final reward is the sum of all individual token rewards
Example: If a user holds tokens `1`, `2`, and `3`, and their total count (3) falls within range 1-5 with a range reward of 10 points:
- Token `1` has custom reward 50 → uses 50 points
- Token `2` has custom reward 100 → uses 100 points
- Token `3` has no custom reward → uses 10 points (range fallback)
- **Total reward: 160 points** (50 + 100 + 10)
- Only reward users that have sold with full royalty paid
- Only reward users that have verified Twitter accounts.
- Only reward users who have sold in specific currency.
### **Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Reach X followers
*X follower count loyalty rule. Reward users based on their X (Twitter) follower count.*
import RewardWithMultiplierRangeAmount from '/snippets/rules/reward-with-multiplier-range-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for having a certain number of followers on Twitter or X. This rule is useful for encouraging users with a large social media following to join the platform.

## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: This defines that the rule will run daily/weekly/monthly at the defined start time and check the no of followers of the users who joined the platform and their followers fall within the defined reward range.
- **User Reward Frequency**: This is a one-time reward that users can claim after linking their accounts, helping to build a connected community across platforms.
### **Custom Settings**
- Skip the multiplier for this rule
---
## Post on X
*New tweet loyalty rule. Reward users for posting new tweets on X.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for posting a new tweet with specific text on their X account. This rule is useful for getting more engagement on their X account.

## Prerequisites
- The user must have connected their X account to Snag.
- The user must post a new tweet with specific text on their X account and submit the claim.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: Points for this rule will be rewarded immediately on user claim after they follow the defined X account.
- **User Reward Frequency**: This defines how often the user will be rewarded. You can choose between one time, daily, weekly, or monthly.
### **Custom Settings**
- **Create X Post that includes**: This lets you define the text that the user must post in their tweet.
- Skip the multiplier for this rule
Text matching is case-insensitive and handles line breaks automatically. Both the expected text and the user's tweet are normalized to handle different line break formats (e.g., `\n`, `\r\n`), ensuring consistent matching regardless of how the text is formatted.
### **Quality Post Filtering**
- **Only reward quality posts**: When enabled, only posts that meet quality standards will be rewarded. Posts are analyzed using AI to calculate a sentiment score from 1-100.
**How quality is determined:**
- Posts receive an AI-calculated sentiment score (1-100) that evaluates both positivity and content quality
- Only posts with a sentiment score of 75 or higher are considered quality posts
- Low-quality content (AI spam, incentive farming, generic copy-paste content, irrelevant hashtags, engagement bait) receives scores of 1-40
- Positive but low-effort content scores 40-60
- Positive, substantive, high-quality content scores 75-100
If a user's post doesn't meet the quality threshold, they will see an error message: "This post doesn't meet the quality requirement. Try creating a new post with better content and submit again."
- **Minimum X followers (optional)**: When "Only reward quality posts" is enabled, you can optionally set a minimum follower count requirement for users to be eligible for rewards.
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Get X Post Impressions
*X post impression loyalty rule. Reward users based on the reach and impressions of their posts.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## **Overview**
This loyalty rule allows admins to reward users based on the impression count (view count) their X (Twitter) posts receive. This is a follow-up rule that works in conjunction with the [Post on X](/loyalty/rules/x-new-tweet) rule, encouraging users to create engaging content that reaches a wider audience.
Users first create a post through the "Post on X" rule, then submit that same post to this rule to claim rewards based on how many impressions it has received.

## **Prerequisites**
- The user must have connected their X (Twitter) account to Snag.
- The user must have already created and submitted a post through the **"Post on X"** rule.
- The user must submit the same post URL to this rule to claim impression-based rewards.
- At least 1 hour must have passed since the last impression check for accurate data.
## **Rule Configuration Fields**
### **Reward Criteria**
- **Impression count ranges**: Define multiple impression count ranges with corresponding reward amounts. Users are rewarded based on which tier their post's view count falls into when they submit their claim.
Set up tiered rewards to incentivize users to create high-quality, engaging
content that reaches more people. For example, you might reward 10 points
for 100-500 impressions, 50 points for 501-2000 impressions, and 200 points
for 2000+ impressions.
**How impression tiers work:**
- Define multiple impression ranges (e.g., 100-500, 501-2000, 2001-10000)
- Each range has its own reward amount
- When users submit their post URL, the system checks the current impression count
- Users automatically receive rewards based on which tier their post's impression count falls into
- Higher impression counts result in higher rewards, encouraging quality content creation
The system refreshes impression counts when users submit claims. If the post
was last synced more than 1 hour ago, the system will fetch fresh data from
X before calculating rewards.
### **Basic Settings**
- **Update Cadence**: Points for this rule are rewarded when users submit their post claim. The system validates the impression count at that moment.
- **User Reward Frequency**: This defines how often users can claim rewards for their X posts. You can choose between:
- **Once**: Users can only claim rewards once total, regardless of how many posts they submit or how impressions grow
- **Daily**: Users can submit one claim per day (can be for the same post or different posts)
- **Weekly**: Users can submit one claim per week (can be for the same post or different posts)
- **Monthly**: Users can submit one claim per month (can be for the same post or different posts)
- **Unlimited**: Users can claim rewards multiple times for the same post as impressions grow into higher tiers
**How intervals work with impression-based rewards:**
- **Once**: User submits a post at 1,000 views (gets 50 points). If they try to submit again at 5,000 views, they won't receive additional rewards.
- **Daily/Weekly/Monthly**: User can submit once per period. They could submit Post A on Monday at 1,000 views, then submit Post B (or resubmit Post A if views increased) on Tuesday. Each period allows one new claim.
- **Unlimited**: User submits a post at 500 views (gets 25 points), then submits the same post again at 1,500 views (gets 100 points), then again at 5,000 views (gets 250 points). They can keep claiming as the post grows.
### **Custom Settings**
- **Skip multiplier**: Skip the multiplier for this rule
## **How It Works**
The user creates a post on X (Twitter) that meets the requirements of the "Post on X" rule and submits it for the initial reward.
This creates a LoyaltyPost record in the system that tracks the post's metadata and performance.
The post gains views on X over time. Users should wait for their post to
accumulate impressions before claiming rewards through this rule.
When ready to claim impression-based rewards, the user submits the same post
URL they previously submitted to the "Post on X" rule.
The system checks when the post's impression data was last synced. If it's
been more than 1 hour, fresh data is fetched from X to ensure accuracy.
Based on the current impression count, the system determines which tier the post falls into and awards the corresponding reward amount.
Users will see the reward credited to their account immediately based on the impression tier their post achieved.
## **API Integration**
If you're integrating this rule via the Snag API, you'll follow the same asynchronous verification flow as other loyalty rules. The key requirement is to submit the post URL when completing the rule.
For complete details on the API claim flow, including polling for status and
handling responses, see [Verifying rule
completion](https://docs.snagsolutions.io/loyalty/verifying-rule-completion).
### **Required Parameter**
When calling the [Complete Loyalty Rule endpoint](https://docs.snagsolutions.io/api-reference/loyalty/complete-loyalty-rule#body-content-url), you **must** include the `contentUrl` parameter with the X post URL:
```javascript
await client.loyalty.rules.complete(ruleId, {
userId: 'user-uuid',
contentUrl: 'https://x.com/username/status/1234567890', // Required for this rule
})
```
The `contentUrl` should be the same post URL that was previously submitted to the "Post on X" rule. The system will:
1. Extract the tweet ID from the URL
2. Look up the existing `LoyaltyPost` record
3. Fetch current impression data (if needed)
4. Calculate rewards based on the configured impression tiers
5. Award points according to the reward frequency settings
If the `contentUrl` doesn't match a post that was previously submitted via the
"Post on X" rule, the completion will fail with a "Post not found" error.
## **Example Configuration**
Here's a practical example of how you might configure this rule:
**Impression tiers:**
- 100-500 impressions: 10 points
- 501-2000 impressions: 50 points
- 2001-5000 impressions: 150 points
- 5000+ impressions: 300 points
**User Reward Frequency:** Once
**Use case:** Simple campaigns where users claim once at their post's peak performance
User submits their post at 3,000 views and receives 150 points. They cannot claim again, even if the post reaches 10,000 views.
**Impression tiers:**
- 200-1000 impressions: 20 points
- 1001-3000 impressions: 75 points
- 3001-10000 impressions: 200 points
- 10000+ impressions: 400 points
**User Reward Frequency:** Weekly
**Use case:** Ongoing campaigns encouraging consistent content creation
Week 1: User submits Post A at 2,000 views (75 points). Week 2: User can submit Post B at 1,500 views (75 points) OR resubmit Post A if it grew to 8,000 views (200 points).
**Impression tiers:**
- 500-1000 impressions: 25 points
- 1001-3000 impressions: 100 points
- 3001-10000 impressions: 250 points
- 10000+ impressions: 500 points
**User Reward Frequency:** Unlimited
**Use case:** Incentivizing users to track and claim as their viral posts gain traction
User creates a post that goes viral: Claims 25 points at 800 views, then 100 points at 2,000 views, then 250 points at 5,000 views, and finally 500 points at 15,000 views. Total earned: 875 points from one successful post.
## **Best Practices**
- **Once**: Best for limited-time campaigns or when you want to reward peak performance only. Users will wait for maximum impressions before claiming.
- **Daily/Weekly/Monthly**: Ideal for ongoing campaigns where you want to encourage consistent content creation. Users can submit new posts each period or resubmit existing posts that gained more traction.
- **Unlimited**: Perfect for viral content campaigns where you want to reward posts as they grow. Be careful with reward budgets as successful posts can earn multiple rewards.
**Considerations:**
- Unlimited frequency requires larger point budgets but creates excitement around viral content
- Periodic (daily/weekly/monthly) frequencies balance engagement with budget control
- Once frequency is most budget-friendly but may reduce long-term engagement
- Start with realistic tier ranges based on your typical user engagement on X
- Create at least 3-4 tiers to reward different levels of reach
- Make sure the reward amounts increase proportionally with impression ranges
- Consider your audience size and typical post performance when setting thresholds
- Don't set the minimum tier too low - posts need time to accumulate meaningful impressions
- For unlimited frequency, ensure tier gaps are significant to prevent constant re-claiming
- Clearly explain that users need to first submit via "Post on X" rule -
Emphasize that they should wait for impressions to accumulate before claiming
- Make it clear they submit the same URL to this rule to claim impression
rewards - Explain the timing requirements (1 hour minimum between syncs)
- Review your post analytics to understand typical impression ranges
- Set tier breakpoints at natural performance levels
- Ensure lower tiers are achievable for most users to maintain engagement
- Reserve highest rewards for exceptional performance to incentivize quality
## **Troubleshooting**
- Verify the user has already submitted this post through the "Post on X" rule first
- Confirm they're submitting the exact same URL they used in the original post submission
- Check that the post still exists on X and hasn't been deleted
- Ensure the user is logged in with the same X account that created the post
- Impression data is cached for 1 hour to avoid excessive API calls - If a
user claims within 1 hour of their last claim, they'll see the cached count -
Wait at least 1 hour between claims for fresh impression data - The system
automatically refreshes data after the 1-hour window
- Confirm the impression count actually falls within one of the defined tier
ranges - Check if the user has already claimed the maximum number of times
based on reward frequency settings - Verify the rule is active and within its
configured date range - Ensure the impression count meets the minimum
threshold of your lowest tier
---
## React to an X Post
*X post reaction loyalty rule. Reward users for liking, retweeting, or engaging with specific posts.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for commenting, reposting, or liking a specific X post. Please note: Likes are not actively tracked and are only displayed to enhance user engagement.

## Prerequisites
- The user must have connected their X account to Snag.
- The user must have commented, reposted, or liked a specific X post, depending on the rule configuration.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: Points for this rule will be rewarded immediately on user claim after they follow the defined X account.
- **User Reward Frequency**: This defines how often the user will be rewarded. You can choose between one time, daily, weekly, or monthly.
### **Custom Settings**
- **X Post URL**: This lets you define the URL of the X post that the user must react to.
- **Required Actions**: This lets you define the actions that the user must perform on the X post.
- **Comment**: The user must comment on the X post.
- **Repost**: The user must repost the X post.
- **Like**: The user must like the X post. (Likes are not actively tracked and are only displayed to enhance user engagement.)
- Skip the multiplier for this rule
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Add Text to X Bio
*X bio text loyalty rule. Reward users for adding specific text to their X profile bio.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for posting a specific text in their X bio. This rule is useful for getting more engagement on their X account.

## Prerequisites
- The user must have connected their X account to Snag.
- The user must have a specific text in their X bio and submit the claim.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: Points for this rule will be rewarded immediately on user claim after they follow the defined X account.
- **User Reward Frequency**: This defines how often the user will be rewarded. You can choose between one time, daily, weekly, or monthly.
### **Custom Settings**
- **Text to check in X Bio**: This lets you define the text that the user must have in their X bio.
- Skip the multiplier for this rule
Text matching is case-insensitive and handles line breaks automatically. Both the expected text and the user's bio are normalized to handle different line break formats (e.g., `\n`, `\r\n`), ensuring consistent matching regardless of how the text is formatted.
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Comment on an X Post with Text
*X comment text loyalty rule. Reward users for commenting with specific text on posts.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for posting a specific text in their X comment. This rule is useful for getting more engagement on their X account.

## Prerequisites
- The user must have connected their X account to Snag.
- The user must have a specific text in their X comment and submit the claim.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: Points for this rule will be rewarded immediately on user claim after they follow the defined X account.
- **User Reward Frequency**: This defines how often the user will be rewarded. You can choose between one time, daily, weekly, or monthly.
### **Custom Settings**
- **Text to check in X Comment**: This lets you define the text that the user must have in their X comment.
- Skip the multiplier for this rule
Text matching is case-insensitive and handles line breaks automatically. Both the expected text and the user's comment are normalized to handle different line break formats (e.g., `\n`, `\r\n`), ensuring consistent matching regardless of how the text is formatted.
### **Quality Post Filtering**
- **Only reward quality posts**: When enabled, only posts that meet quality standards will be rewarded. Posts are analyzed using AI to calculate a sentiment score from 1-100.
**How quality is determined:**
- Posts receive an AI-calculated sentiment score (1-100) that evaluates both positivity and content quality
- Only posts with a sentiment score of 75 or higher are considered quality posts
- Low-quality content (AI spam, incentive farming, generic copy-paste content, irrelevant hashtags, engagement bait) receives scores of 1-40
- Positive but low-effort content scores 40-60
- Positive, substantive, high-quality content scores 75-100
If a user's post doesn't meet the quality threshold, they will see an error message: "This post doesn't meet the quality requirement. Try creating a new post with better content and submit again."
- **Minimum X followers (optional)**: When "Only reward quality posts" is enabled, you can optionally set a minimum follower count requirement for users to be eligible for rewards.
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Add Text to X Username
*X username text loyalty rule. Reward users for including specific text in their X username.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for posting a specific text in their X username. This rule is useful for getting more engagement on their X account.

## Prerequisites
- The user must have connected their X account to Snag.
- The user must have a specific text in their X username and submit the claim.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: Points for this rule will be rewarded immediately on user claim after they follow the defined X account.
- **User Reward Frequency**: This defines how often the user will be rewarded. You can choose between one time, daily, weekly, or monthly.
### **Custom Settings**
- **Text to check in X Username**: This lets you define the text that the user must have in their X username.
- Skip the multiplier for this rule
Text matching is case-insensitive and handles line breaks automatically. Both the expected text and the user's username are normalized to handle different line break formats (e.g., `\n`, `\r\n`), ensuring consistent matching regardless of how the text is formatted.
**Call to Action (Optional)**
This will show a call to action in our loyalty rule.
- **Link**: Provide a URL for users to take further action.
- **Button Text**: Define the button label.
---
## Comment on a YouTube Video
*YouTube comment loyalty rule. Reward users for commenting on YouTube videos.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for commenting on specific [YouTube](https://www.youtube.com/) videos. This rule is useful for increasing engagement with your YouTube content and building community interaction.

## Prerequisites
- The user must have connected their Google account to Snag.
- The user must comment on the specified YouTube video.
- The user must gather the link of the comment they made on youtube video and submit the link for verification.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: The YouTube Comment rule rewards users **immediately** upon successful posting of a comment on the specified YouTube video.
- **User Reward Frequency**: This defines how often the user can earn the reward, e.g. daily, weekly, monthly, etc. If set to daily, the user can earn the reward once per day based on UTC time.
### **Custom Settings**
- Skip the multiplier for this rule
- **YouTube Video ID**: The ID of the YouTube video that users need to comment on. This can be found in the URL of your YouTube video (e.g., https://www.youtube.com/watch?v=ABC123xyz where ABC123xyz is the Video ID).
- **YouTube Video Link**: This will add a button on the rule display page to take the user directly to the YouTube video.
## Limitations
- API usage is rate-limited. To avoid restrictions, you can provide your own YouTube API key.
---
## Subscribe to a YouTube Channel
*YouTube subscriber loyalty rule. Reward users for subscribing to your YouTube channel.*
import RewardWithMultiplierFixedAmount from '/snippets/rules/reward-with-multiplier-fixed-amount.mdx'
## Overview
This loyalty rule allows admins to reward users for subscribing to specific [YouTube](https://www.youtube.com/) channel. This rule is useful for growing your YouTube presence and encouraging your community to follow your content.

## Prerequisites
- The user must have connected their Google account to Snag.
- The user must subscribe to the specified YouTube channel.
## Rule Configuration Fields
### Basic Settings
- **Update Cadence**: The YouTube Subscriber rule rewards users **immediately** upon successfully subscribing to the specified YouTube channel.
- **User Reward Frequency**: This defines how often the user can earn the reward, e.g. daily, weekly, monthly, etc. If set to daily, the user can earn the reward once per day based on UTC time.
### **Custom Settings**
- Skip the multiplier for this rule
- **YouTube Channel ID**: The ID of the YouTube channel that users need to subscribe to.
To find your YouTube channel ID:
1. Sign in to YouTube
2. Click your profile picture → Settings
3. Select Advanced settings from the left menu
4. Your channel ID will be displayed there
- **YouTube Channel Link**: This will add a button on the rule display page to take the user directly to the YouTube channel.
## Limitations
- API usage is rate-limited. To avoid restrictions, you can provide your own YouTube API key.
---
## Connect social accounts
*Learn how to integrate social media platforms with your users using Snag's OAuth flow or by passing social handles directly*
After creating users and managing user groups, the next step is to connect social media accounts. Snag provides two methods for integrating social accounts: using Snag's OAuth flow or passing social handles directly.
## Overview
Social account integration enables enhanced loyalty features like:
- Social media verification rules
- Cross-platform engagement tracking
- Enhanced user profiles
- Automated social media tasks
## Supported Social Platforms
Snag supports connecting the following social platforms:
- **Twitter/X** - Follow accounts, post tweets, verify bio content
- **Discord** - Join servers, verify roles, track messages
- **Telegram** - Join channels, verify messages
- **Epic Games** - Verify game ownership and achievements
- **Steam** - Verify game library and wishlist
- **Google** - Verify email address
- **TikTok** - Verify TikTok account
- **Email** - Verify email address
## Method 1: Snag OAuth Flow
This method lets Snag handle the entire OAuth process, providing a seamless user experience.
### How It Works
1. **Generate OAuth URL**: Call Snag's API to get an authentication URL
2. **Redirect User**: Send the user to the OAuth URL
3. **User Authorization**: User authorizes the connection on the social platform
4. **Callback Handling**: Snag redirects back to your application
5. **Verification**: Confirm the connection was successful
### Implementation
#### Step 1: Generate OAuth URL
```bash cURL
curl -X GET 'https://admin.snagsolutions.io/api/twitter/auth?userId=user-id&redirectUri=https://yourapp.com/callback' \
-H 'x-api-key: your-api-key-here'
```
```json Success
{
"url": "https://auth-provider.com/oauth/authorize?client_id=xxx&redirect_uri=xxx&state=xxx"
}
```
#### Step 2: Redirect User
```javascript
// Server-side code to initiate the flow
app.get('/connect-twitter', async (req, res) => {
try {
const userId = req.query.userId
const redirectUri = 'https://yourapp.com/callback'
const response = await fetch(
`https://admin.snagsolutions.io/api/twitter/auth?userId=${userId}&redirectUri=${redirectUri}`,
{
headers: {
'x-api-key': process.env.SNAG_API_KEY,
},
}
)
const data = await response.json()
// Redirect user to the OAuth URL
res.redirect(data.url)
} catch (error) {
console.error('Error connecting Twitter account:', error)
res.status(500).send('Error connecting Twitter account')
}
})
```
#### Step 3: Handle Callback
```javascript
// Handle the callback from Snag
app.get('/callback', async (req, res) => {
const { code, state, userId } = req.query
try {
// Verify the connection was successful
const user = await client.users.getMetadata(userId)
if (user.twitterUser) {
res.send('Twitter account connected successfully!')
} else {
res.send('Failed to connect Twitter account')
}
} catch (error) {
console.error('Error verifying connection:', error)
res.status(500).send('Error verifying connection')
}
})
```
### Client-Side Integration
```javascript
// Client-side code to initiate the flow
function connectTwitterAccount(userId) {
// Redirect to your server endpoint
window.location.href = `/connect-twitter?userId=${userId}`
}
// Example button click handler
document.getElementById('connect-twitter-btn').addEventListener('click', () => {
connectTwitterAccount('user-123')
})
```
## Method 2: Direct Social Handle Passing
If you control the OAuth flow in your application, you can pass social handles directly to Snag using the metadata endpoint.
### Implementation
```javascript
import SnagSolutions from '@snagsolutions/sdk'
const client = new SnagSolutions({
apiKey: 'your-api-key-here',
})
// After your OAuth flow completes, update user metadata
const updatedUser = await client.users.createMetadata({
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
twitterUser: 'user_twitter_handle',
twitterUserId: 'user_twitter_id',
discordUser: 'user_discord_username',
discordUserId: 'user_discord_id',
telegramUsername: 'user_telegram_username',
telegramUserId: 'user_telegram_id',
})
```
### Example: Custom OAuth Implementation
```javascript
// Your custom OAuth flow
async function handleTwitterOAuth(code) {
// Your OAuth implementation
const twitterData = await exchangeCodeForToken(code)
const userInfo = await getTwitterUserInfo(twitterData.access_token)
// Pass the Twitter handle to Snag
await client.users.createMetadata({
walletAddress: currentUser.walletAddress,
twitterUser: userInfo.username,
})
console.log('Twitter handle updated in Snag')
}
```
## Platform-Specific Endpoints
### Twitter/X
```javascript
// Generate Twitter OAuth URL
const twitterAuth = await client.social.twitter.auth({
userId: 'user-id',
redirectUri: 'https://yourapp.com/callback',
})
```
### Discord
```javascript
// Generate Discord OAuth URL
const discordAuth = await client.social.discord.auth({
userId: 'user-id',
redirectUri: 'https://yourapp.com/callback',
})
```
### Telegram
```javascript
// Generate Telegram OAuth URL
const telegramAuth = await client.social.telegram.auth({
userId: 'user-id',
redirectUri: 'https://yourapp.com/callback',
})
```
## Custom OAuth Applications
Snag allows you to use your own OAuth applications for **Twitter**, **Discord**, **Epic Games**, and **Steam** integrations. This enables you to maintain your brand identity throughout the authentication flow.
Contact the Snag team for integration support if you want to use your own OAuth applications.
### Required Redirect URLs
Add these redirect URLs to your OAuth application:
- `https://snag-render.com/api/twitter/auth/callback`
- `https://snag-render.com/api/discord/auth/callback`
- `https://snag-render.com/api/epic/auth/callback`
- `https://snag-render.com/api/steam/auth/callback`
## Error Handling
### Common Issues
- **Invalid redirect URI**: Ensure your redirect URI matches exactly
- **Expired authorization**: OAuth codes expire quickly, handle them promptly
- **User cancellation**: Handle cases where users cancel the authorization
- **Duplicate handles**: Check if a social handle is already connected to another user
- **Invalid handles**: Verify social handles are in the correct format
- **Platform-specific errors**: Handle platform-specific error responses
### Error Handling Example
```javascript
try {
const response = await client.social.twitter.auth({
userId: 'user-id',
redirectUri: 'https://yourapp.com/callback',
})
// Redirect user to OAuth URL
window.location.href = response.url
} catch (error) {
if (error.status === 400) {
console.error('Invalid parameters:', error.message)
} else if (error.status === 403) {
console.error('API key invalid or expired')
} else {
console.error('Unexpected error:', error)
}
}
```
## Best Practices
**User Experience**: Use Snag's OAuth flow for a seamless experience, or implement your own for complete control.
**Error Handling**: Always handle OAuth errors gracefully and provide clear feedback to users.
**Security**: Never expose API keys in client-side code. Always handle OAuth flows server-side.
**Rate Limits**: Be mindful of API rate limits when processing multiple social connections.
## Integration with Loyalty Rules
Once social accounts are connected, they can be used in loyalty rules:
- **Twitter Follow**: Verify users follow specific accounts
- **Discord Role**: Check if users have specific server roles
- **Telegram Join**: Verify users joined specific channels
- **Social Verification**: Use social accounts as proof of identity
For detailed information on social account management, see our [Managing
Social Accounts](/loyalty/managing-social-accounts) guide.
## Next Steps
Now that you can connect social accounts, you're ready to:
Start building loyalty programs with social media integration.
---
## Create users
*Learn how to create and manage users in Snag using the metadata endpoint, including wallet address requirements and supported wallet types*
After getting your API key, the next step is to create users in your Snag system. This guide explains how to use the metadata endpoint to add users and manage their information.
## Overview
The `POST /api/users/metadatas` endpoint allows you to create or update user objects in Snag's system. This is essential for migrating users from your existing system or adding new users to your loyalty program.
Only the `walletAddress` property is required as this is the unique identifier for the user being created.
## Wallet Address Requirements
A wallet address is **required** for creating a Snag user. We currently support the following wallet types:
- **EVM** - Ethereum and EVM-compatible chains (Polygon, BSC, etc.)
- **SVM** - Solana Virtual Machine
- **TON** - The Open Network
- **SUI** - Sui blockchain
- **Cosmos** - Cosmos ecosystem chains
The wallet address serves as the unique identifier for each user in the Snag system. If your users don't have wallet addresses, see our [wallet generation guide](/loyalty/development/generate-wallet-addresses).
## Creating a User
### Basic User Creation
Here's how to create a user with just the required wallet address:
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/users/metadatas' \
-H 'x-api-key: your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678"
}'
```
```json Success
{
"id": "user-123",
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
```
### User with Metadata
You can also include additional metadata when creating a user:
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/users/metadatas' \
-H 'x-api-key: your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678",
"discordUser": "discord-user",
"twitterUser": "twitter-user",
"emailAddress": "user@example.com",
"telegramUsername": "telegram-user",
"displayName": "John Doe",
"logoUrl": "https://example.com/avatar.png",
"externalIdentifier": "your-internal-user-id"
}'
```
```json Success
{
"id": "user-123",
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678",
"discordUser": "discord-user",
"twitterUser": "twitter-user",
"emailAddress": "user@example.com",
"telegramUsername": "telegram-user",
"displayName": "John Doe",
"logoUrl": "https://example.com/avatar.png",
"externalIdentifier": "your-internal-user-id",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
```
## Using the Snag SDK
```javascript
import SnagSolutions from '@snagsolutions/sdk'
const client = new SnagSolutions({
apiKey: 'your-api-key-here',
})
// Create a basic user
const user = await client.users.createMetadata({
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
})
// Create a user with metadata
const userWithMetadata = await client.users.createMetadata({
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
discordUser: 'discord-user',
discordUserId: 'discord-user-id',
twitterUser: 'twitter-user',
twitterUserId: 'twitter-user-id',
telegramUsername: 'telegram-user',
telegramUserId: 'telegram-user-id',
displayName: 'John Doe',
externalIdentifier: 'your-internal-user-id',
})
```
## External Identifier
The `externalIdentifier` field allows you to maintain your own user ID system while using Snag. This is particularly useful when migrating from an existing system:
```javascript
// Map your internal user ID to Snag
const user = await client.users.createMetadata({
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
externalIdentifier: 'your-internal-user-id-12345',
})
```
Use the external identifier to maintain a mapping between your internal user IDs and Snag user IDs for easier integration.
## Updating User Metadata
The same endpoint can be used to update existing user metadata. Simply provide the wallet address and the fields you want to update:
```javascript
// Update user metadata
const updatedUser = await client.users.createMetadata({
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
displayName: 'Updated Name',
emailAddress: 'newemail@example.com',
})
```
The endpoint is idempotent - calling it multiple times with the same wallet address will update the user rather than create duplicates.
## Next Steps
Now that you can create users, learn how to:
Learn how to programmatically generate wallet addresses for users who don't have them.
Step-by-step guide for migrating your existing user base to Snag.
Learn how to connect multiple wallets to a single user using user groups.
Integrate social media platforms with your users for enhanced loyalty features.
---
## Generate wallet addresses
*Learn how to programmatically generate wallet addresses for your users using viem library or smart contract wallet providers*
If your users don't have wallet addresses yet, you can generate them programmatically. This guide covers two main approaches for creating wallet addresses for your users.
Wallet addresses are required to create users in Snag. If you're migrating existing users, see our [migration guide](/loyalty/development/migrate-users) for step-by-step instructions.
## Option 1: Generate with viem Library
For backend wallet generation, you can use the [viem](https://viem.sh) library to create new EVM wallet addresses. This approach gives you full control over wallet generation and is ideal for server-side implementations.
### Installation
```bash
npm install viem
```
### Generate a Wallet
```typescript
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
// Generate a new wallet
const privateKey = generatePrivateKey()
const account = privateKeyToAccount(privateKey)
const walletAddress = account.address
// Use walletAddress to create the Snag user
await client.users.createMetadata({
walletAddress: walletAddress,
externalIdentifier: 'your-internal-user-id',
})
```
### Complete Integration Example
Here's a full example showing how to generate a wallet and create a Snag user:
```typescript
import SnagSolutions from '@snagsolutions/sdk'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
const client = new SnagSolutions({
apiKey: 'your-api-key-here',
})
async function createUserWithWallet(userId: string, email: string) {
// Generate new wallet
const privateKey = generatePrivateKey()
const account = privateKeyToAccount(privateKey)
// Store private key securely in your database
await yourDatabase.savePrivateKey({
userId: userId,
encryptedPrivateKey: await encryptPrivateKey(privateKey),
})
// Create Snag user with the generated wallet address
const user = await client.users.createMetadata({
walletAddress: account.address,
externalIdentifier: userId,
emailAddress: email,
})
return user
}
```
**Critical Security Requirements:**
- Store private keys in an encrypted database or Key Management Service (KMS)
- Never expose private keys in client-side code or logs
- Use AWS KMS, Google Cloud KMS, or HashiCorp Vault for production
- Never commit private keys to version control
- Implement proper access controls for private key retrieval
## Option 2: Smart Contract Wallet Providers
For a managed solution, use embedded wallet providers that handle wallet creation and key management automatically. These providers abstract away the complexity of private key management and provide a better user experience.
### Recommended Providers
Easy-to-integrate embedded wallets with email/social login support
Embedded wallets with excellent UX for Web2 users transitioning to Web3
Smart account infrastructure with gasless transactions and account abstraction
Comprehensive wallet SDK with smart accounts and social login
### Advantages of Smart Wallet Providers
These providers offer several advantages over self-managed wallet generation:
- **Automatic key management** - No need to handle private keys yourself
- **Social login integration** - Email or social login → wallet address mapping
- **Better UX** - Seamless onboarding for Web2 users
- **Built-in security** - Industry best practices implemented out of the box
- **Account recovery** - Users can recover access without managing seed phrases
- **Multi-chain support** - Easy support for multiple blockchain networks
Smart contract wallet providers are recommended if you want to abstract wallet complexity from your users and provide a Web2-like experience.
### Integration Example with Dynamic
Here's a basic example of how you might integrate Dynamic's embedded wallets:
```typescript
import { DynamicContextProvider } from '@dynamic-labs/sdk-react-core'
// On your frontend
function App() {
return (
{/* Your app */}
)
}
// After user logs in, get their wallet address
const walletAddress = user.verifiedCredentials[0].address
// Then create the Snag user on your backend
await client.users.createMetadata({
walletAddress: walletAddress,
externalIdentifier: user.id,
})
```
## Best Practices
Regardless of which approach you choose, follow these best practices:
Always use the `externalIdentifier` field to link Snag users to your internal user ID system. This makes it easy to query and manage users across your systems.
For self-managed wallets, implement a secure backup and recovery mechanism. Consider using deterministic wallet generation (HD wallets) for easier recovery.
Separate wallet generation between development, staging, and production environments. Never use production keys in development.
Log and monitor all access to private keys. Implement alerts for unusual access patterns.
Be aware of regulatory requirements in your jurisdiction regarding custody of user assets and key management.
## Next Steps
Learn how to migrate your existing users to Snag with wallet addresses
Return to the user creation guide to create users with your generated wallet addresses
---
## Getting started
*Learn how to sign up, access the admin dashboard, and create your first API key to start integrating Snag loyalty rules*
import AiAssistantCallout from '/snippets/ai-assistant-callout.mdx'
Welcome to Snag! This guide will walk you through the initial setup process to get your API key and start integrating loyalty rules into your application.
## Sign Up and Access
Visit [Snag Solutions](https://www.snagsolutions.io) and sign up for an account.
Once your account is verified, you can access the admin dashboard at [admin.snagsolutions.io](https://admin.snagsolutions.io).
The admin dashboard is where you'll manage your loyalty programs, and configure your API keys.
From the admin dashboard, navigate to the **API Keys** page using the left sidebar navigation.
Click the "Create New API Key" button to generate your first API key.
Copy your API key and store it in a secure location like an environment variable.
## Using Your API Key
Your API key is the foundation for all Snag API interactions. Here's how to use it:
Use a Website API key
to access loyalty APIs (rules, transactions, entries, users, and related endpoints). A Stratus API key
is used to secure Stratus relayer and Stratus functions; these keys allow you to submit and start onchain transactions. Do not use Stratus keys with loyalty endpoints.
### With Snag SDK
```javascript
import SnagSolutions from '@snagsolutions/sdk'
const client = new SnagSolutions({
apiKey: 'your-api-key-here',
})
// Now you can use the client to interact with Snag APIs
const users = await client.users.get()
```
### With HTTP Requests
For direct HTTP calls, include your API key in the `x-api-key` header:
```bash
curl -X GET 'https://admin.snagsolutions.io/api/users' \
-H 'x-api-key: your-api-key-here'
```
```javascript
const response = await fetch('https://admin.snagsolutions.io/api/users', {
headers: {
'x-api-key': 'your-api-key-here',
'Content-Type': 'application/json',
},
})
```
Keep your API key secure and never expose it in client-side code. Always use environment variables or secure configuration management.
## Next Steps
Now that you have your API key, you're ready to:
1. **Create users** - Learn how to add users to your Snag system
2. **Manage user groups** - Understand how to handle multiple wallets per user
3. **Connect social accounts** - Integrate social media platforms with your users
Learn how to add users to your Snag system using the metadata endpoint.
{' '}
---
## List loyalty rules and sections
*Render rule sections (rule groups) and their rules in your frontend using official APIs with pagination.*
This guide shows you how to list loyalty rule sections (rule groups) and the
rules under each section in your frontend. For all parameters, filters, and
full response schemas, see the API references: -{' '}
Get Loyalty Rule Groups
-{' '}
Get Loyalty Rules
## Prerequisites
- You can make authenticated requests with the `X-API-KEY` header.
- You have your base URL (e.g., `$BASE_URL`).
## How it works (at a glance)
- Fetch sections via Rule Groups.
- For each section, fetch its rules.
- Use cursor-style pagination with `startingAfter` and stop when `hasNextPage` is false.
```bash cURL
curl -X GET "$BASE_URL/api/loyalty/rule-groups?limit=20" \
-H "X-API-KEY: $API_KEY"
```
See full request/response details: Get Loyalty Rule Groups.
```bash cURL
curl -X GET "$BASE_URL/api/loyalty/rules?loyaltyRuleGroupId=&limit=20" \
-H "X-API-KEY: $API_KEY"
```
See full request/response details: Get Loyalty Rules.
```typescript React
// Minimal fetchers (client-side or server-side)
async function listRuleGroups(baseUrl: string, apiKey: string, startingAfter?: string) {
const url = new URL('/api/loyalty/rule-groups', baseUrl);
url.searchParams.set('limit', '20');
if (startingAfter) url.searchParams.set('startingAfter', startingAfter);
const res = await fetch(url.toString(), { headers: { 'X-API-KEY': apiKey } });
if (!res.ok) throw new Error(`rule-groups failed: ${res.status}`);
return res.json() as Promise<{ data: Array<{ id: string; name: string; slug?: string; description?: string }>; hasNextPage: boolean }>;
}
async function listRulesForGroup(baseUrl: string, apiKey: string, groupId: string, startingAfter?: string) {
const url = new URL('/api/loyalty/rules', baseUrl);
url.searchParams.set('limit', '20');
url.searchParams.set('loyaltyRuleGroupId', groupId);
if (startingAfter) url.searchParams.set('startingAfter', startingAfter);
const res = await fetch(url.toString(), { headers: { 'X-API-KEY': apiKey } });
if (!res.ok) throw new Error(`rules failed: ${res.status}`);
return res.json() as Promise<{ data: Array<{ id: string; name: string; description?: string }>; hasNextPage: boolean }>;
}
// Example usage:
// 1) const { data: groups, hasNextPage } = await listRuleGroups(BASE_URL, API_KEY);
// 2) For each group.id, call listRulesForGroup(BASE_URL, API_KEY, group.id)
// 3) Append results while hasNextPage is true (see next step)
````
You can see rule sections rendered and, on expanding a section, the rules
under it. Additional pages load until `hasNextPage` is false.
## Next Steps
Now that you can list loyalty rules and sections, you're ready to:
Learn how to create and configure loyalty rules for your program.
Understand how to configure rule parameters and settings.
---
## Manage user groups
*Learn how to handle multiple wallet addresses per user using Snag's user group feature for seamless multi-wallet support*
In Snag, each wallet address represents an individual user. To support users with multiple wallets, we provide a **User Group** feature that allows you to connect multiple wallet addresses together, treating them as a single user across your loyalty system.
## Understanding User Groups
A user group can contain multiple users (wallet addresses) and syncs all user metadata, including social handles, across all connected wallets.
### Why User Groups?
- **Multiple Wallets**: Users often have wallets on different chains (Ethereum, Polygon, Solana, etc.)
- **Shared Identity**: All wallets in a group share the same social accounts and metadata
- **Unified Loyalty**: Points, badges, and rewards are tracked across all wallets in the group
- **Seamless Experience**: Users don't need to reconnect social accounts for each wallet
**Enable User Groups First**: Before using the user group feature, you must enable it in your Snag dashboard. Navigate to **Customisation → User Profiles** and enable multiple wallet connections. If this feature is not enabled, the `POST /api/users/connect` endpoint will return an error: "This website does not support multiple wallet connections, please contact support".
## Creating and Managing User Groups
### Connecting a User to a Group
Use the `POST /api/users/connect` endpoint to add a wallet address to a user group:
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/users/connect' \
-H 'x-api-key: your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{
"organizationId": "123e4567-e89b-12d3-a456-426614174001",
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678",
"walletType": "evm",
"websiteId": "123e4567-e89b-12d3-a456-426614174000"
}'
```
```json Success
{
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678",
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2023-10-01T12:34:56Z",
"updatedAt": "2023-10-05T15:30:00Z"
}
```
### Using the Snag SDK
```javascript
import SnagSolutions from '@snagsolutions/sdk'
const client = new SnagSolutions({
apiKey: 'your-api-key-here',
})
// Connect a user to a group
const response = await client.users.connect({
organizationId: '123e4567-e89b-12d3-a456-426614174001',
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
walletType: 'evm',
websiteId: '123e4567-e89b-12d3-a456-426614174000',
})
console.log('User connected to group:', response.id)
```
### Disconnecting a User from a Group
To remove a wallet address from a user group, use the `POST /api/users/disconnect` endpoint:
```bash cURL
curl -X POST 'https://admin.snagsolutions.io/api/users/disconnect' \
-H 'x-api-key: your-api-key-here' \
-H 'Content-Type: application/json' \
-d '{
"organizationId": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
"userId": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
"websiteId": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e"
}'
```
```json Success
{
"message": "Success"
}
```
```javascript
// Disconnect a user from a group
const response = await client.users.disconnect({
organizationId: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
userId: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
websiteId: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
})
console.log('User disconnected:', response.message)
```
## Supported Wallet Types
When connecting users to groups, specify the wallet type:
- `evm` - Ethereum and EVM-compatible chains
- `svm` - Solana Virtual Machine
- `ton` - The Open Network
- `sui` - Sui blockchain
- `cosmos` - Cosmos ecosystem chains
## Metadata Synchronization
When a user/wallet address is connected to a group, all user metadata including social handles gets synced across all users in that group:
```javascript
// Example: User has wallets on Ethereum and Polygon
const ethereumWallet = '0x1234567890abcdef1234567890abcdef12345678'
const polygonWallet = '0xabcdef1234567890abcdef1234567890abcdef12'
// Connect both wallets to the same group
await client.users.connect({
organizationId: 'your-org-id',
walletAddress: ethereumWallet,
walletType: 'evm',
websiteId: 'your-website-id',
})
await client.users.connect({
organizationId: 'your-org-id',
walletAddress: polygonWallet,
walletType: 'evm',
websiteId: 'your-website-id',
})
// Now both wallets share the same social accounts and metadata
```
## User Group Workflow
Enable the user group feature in your Snag dashboard by navigating to **Customisation → User Profiles** and enabling multiple wallet connections.
Create individual users for each wallet address using the metadata endpoint.
```javascript
await client.users.createMetadata({
walletAddress: '0x1234567890abcdef1234567890abcdef12345678'
});
```
Connect each wallet to the appropriate user group.
```javascript
await client.users.connect({
organizationId: 'your-org-id',
walletAddress: '0x1234567890abcdef1234567890abcdef12345678',
walletType: 'evm',
websiteId: 'your-website-id'
});
```
Check that metadata is synced across all wallets in the group.
## Best Practices
**Group Management**: Create user groups based on your application's needs - typically one group per user identity.
**Wallet Types**: Always specify the correct wallet type when connecting users to ensure proper chain support.
**Metadata Updates**: When updating user metadata, the changes will automatically sync across all wallets in the group.
**Disconnection Impact**: When disconnecting a wallet from a group, the wallet becomes a standalone user and loses access to shared metadata and loyalty benefits.
## Integration with Loyalty System
User groups work seamlessly with Snag's loyalty system:
- **Points Tracking**: Loyalty points are tracked across all wallets in a group
- **Rule Completion**: Social account connections and other rule completions apply to all wallets
- **Rewards**: Badges and rewards are shared across the entire group
- **Leaderboards**: Users appear as a single entity regardless of which wallet they use
For more details on how user groups work with the loyalty system, see our [Multi Wallet Support](/loyalty/multi-wallet-support) guide.
## Troubleshooting
If you receive this error when calling the `POST /api/users/connect` endpoint, it means the user group feature is not enabled for your website.
**Solution:**
1. Navigate to your Snag dashboard
2. Go to **Customisation → User Profiles**
3. Enable the multiple wallet connections feature
4. Retry your API call
Once enabled, you'll be able to connect multiple wallet addresses to user groups without encountering this error.
## Next Steps
Now that you understand user groups, learn how to:
Integrate social media platforms with your users for enhanced loyalty features.
{' '}
---
## Migrate users
*Step-by-step guide for migrating your existing users to Snag, including handling users without wallet addresses*
This guide walks you through migrating your existing user base to Snag's loyalty system. Whether you're moving from another platform or setting up Snag for the first time, this guide will help you successfully migrate your users.
## Prerequisites
Before starting your migration:
Have your Snag API key ready (see [Getting Started](/loyalty/development/getting-started))
Export your user data from your existing system
Ensure all users have wallet addresses (or [generate them](/loyalty/development/generate-wallet-addresses) first)
## Migration Strategy
Follow these steps to migrate your users systematically:
Export your user data and ensure you have wallet addresses for each user.
Users without wallet addresses cannot be created in Snag. If your users don't have wallet addresses, see our [wallet generation guide](/loyalty/development/generate-wallet-addresses) for guidance on how to create them.
### Required Data
- `walletAddress` (required) - The unique blockchain wallet address
### Optional Metadata
- `externalIdentifier` - Your internal user ID (highly recommended)
- `displayName` - User's display name
- `emailAddress` - User's email address
- `discordUser` - Discord username
- `discordUserId` - Discord user ID
- `twitterUser` - Twitter/X username
- `twitterUserId` - Twitter/X user ID
- `telegramUsername` - Telegram username
- `telegramUserId` - Telegram user ID
- `logoUrl` - URL to user's avatar/profile picture
Create a script to process your users in batches. Here's a complete example:
```typescript
import SnagSolutions from '@snagsolutions/sdk'
const client = new SnagSolutions({
apiKey: process.env.SNAG_API_KEY,
})
interface YourUser {
id: string
walletAddress: string
name: string
email: string
// ... other fields
}
async function migrateUsers(users: YourUser[]) {
const results = {
successful: 0,
failed: 0,
errors: [] as Array<{ userId: string; error: string }>,
}
for (const user of users) {
try {
await client.users.createMetadata({
walletAddress: user.walletAddress,
externalIdentifier: user.id,
displayName: user.name,
emailAddress: user.email,
})
results.successful++
console.log(`✓ Migrated user ${user.id}`)
} catch (error) {
results.failed++
results.errors.push({
userId: user.id,
error: error.message,
})
console.error(`✗ Failed to migrate user ${user.id}:`, error.message)
}
}
return results
}
// Usage
const users = await fetchYourUsers()
const results = await migrateUsers(users)
console.log(`Migration complete: ${results.successful} successful, ${results.failed} failed`)
```
For large migrations, process users in batches to avoid rate limits and make the process more manageable:
```typescript
async function migrateInBatches(
users: YourUser[],
batchSize: number = 100,
delayMs: number = 1000
) {
const totalBatches = Math.ceil(users.length / batchSize)
for (let i = 0; i < totalBatches; i++) {
const start = i * batchSize
const end = start + batchSize
const batch = users.slice(start, end)
console.log(`Processing batch ${i + 1}/${totalBatches}`)
await migrateUsers(batch)
// Add delay between batches to respect rate limits
if (i < totalBatches - 1) {
await new Promise(resolve => setTimeout(resolve, delayMs))
}
}
}
```
Start with a small batch (10-50 users) to verify your migration script works correctly before processing your entire user base.
After migration, verify that all users were created successfully:
```typescript
async function verifyMigration(externalIds: string[]) {
const notFound: string[] = []
for (const externalId of externalIds) {
try {
// Query by external identifier
const users = await client.users.list({
externalIdentifier: externalId,
})
if (users.length === 0) {
notFound.push(externalId)
}
} catch (error) {
console.error(`Error checking user ${externalId}:`, error)
notFound.push(externalId)
}
}
if (notFound.length > 0) {
console.log(`Missing users: ${notFound.length}`)
console.log(notFound)
} else {
console.log('✓ All users verified successfully')
}
return notFound
}
```
Review and retry failed migrations:
```typescript
async function retryFailed(errors: Array<{ userId: string; error: string }>) {
console.log(`Retrying ${errors.length} failed migrations...`)
const usersToRetry = await fetchUsersById(errors.map(e => e.userId))
const results = await migrateUsers(usersToRetry)
return results
}
```
Common errors and solutions:
- **Invalid wallet address** - Verify the wallet address format
- **Duplicate user** - User already exists, use update instead
- **Rate limit** - Increase delay between batches
- **Authentication error** - Check your API key
## Migration Best Practices
Before migrating your entire user base, test with a small subset (10-100 users) to identify any issues with your data format or migration script.
Always include the `externalIdentifier` field mapping to your internal user ID. This makes it easy to:
- Query Snag users from your system
- Link Snag data back to your users
- Debug migration issues
- Avoid duplicate migrations
Log all migration attempts with timestamps, user IDs, and results. This helps with debugging and provides an audit trail.
Have a rollback strategy in case something goes wrong. Keep track of which users were migrated so you can clean up if needed.
The metadata endpoint is idempotent - calling it multiple times with the same wallet address updates the user. This means you can safely re-run your migration script.
Watch for errors, rate limits, and performance issues during migration. Be prepared to pause and adjust your approach if needed.
## Handling Users Without Wallet Addresses
If some of your users don't have wallet addresses, you have several options:
Generate wallet addresses programmatically using viem or smart wallet providers
Ask users to connect their wallets during their next login
Migrate users as they connect wallets, rather than all at once
Use email-to-wallet services to create wallets tied to email addresses
## Complete Migration Example
Here's a complete, production-ready migration script:
```typescript
import SnagSolutions from '@snagsolutions/sdk'
import { writeFileSync } from 'fs'
const client = new SnagSolutions({
apiKey: process.env.SNAG_API_KEY,
})
interface MigrationResult {
timestamp: string
totalUsers: number
successful: number
failed: number
errors: Array<{ userId: string; error: string }>
}
async function runMigration(): Promise {
console.log('Starting user migration...')
// Fetch your users
const users = await fetchYourUsers()
console.log(`Found ${users.length} users to migrate`)
const result: MigrationResult = {
timestamp: new Date().toISOString(),
totalUsers: users.length,
successful: 0,
failed: 0,
errors: [],
}
// Process in batches
const batchSize = 100
const totalBatches = Math.ceil(users.length / batchSize)
for (let i = 0; i < totalBatches; i++) {
const start = i * batchSize
const end = start + batchSize
const batch = users.slice(start, end)
console.log(`\nProcessing batch ${i + 1}/${totalBatches} (${batch.length} users)`)
for (const user of batch) {
try {
await client.users.createMetadata({
walletAddress: user.walletAddress,
externalIdentifier: user.id,
displayName: user.name,
emailAddress: user.email,
discordUser: user.discordUsername,
twitterUser: user.twitterUsername,
})
result.successful++
process.stdout.write('.')
} catch (error) {
result.failed++
result.errors.push({
userId: user.id,
error: error.message,
})
process.stdout.write('✗')
}
}
// Rate limit protection
if (i < totalBatches - 1) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
// Save results
const filename = `migration-${Date.now()}.json`
writeFileSync(filename, JSON.stringify(result, null, 2))
console.log(`\n\nMigration complete!`)
console.log(`✓ Successful: ${result.successful}`)
console.log(`✗ Failed: ${result.failed}`)
console.log(`Results saved to: ${filename}`)
return result
}
// Run migration
runMigration()
.then(() => process.exit(0))
.catch(error => {
console.error('Migration failed:', error)
process.exit(1)
})
```
## Next Steps
Learn how to generate wallet addresses for users who don't have them
Connect multiple wallets to a single user using user groups
Integrate social media platforms with your users
Learn more about creating and managing individual users
---